/*******************************************************************************
 * vfeplatform.cpp
 *
 * This module contains *nix platform-specific support code for the VFE.
 *
 * Based on vfe/win/vfeplatform.cpp by Christopher J. Cason
 *
 * ---------------------------------------------------------------------------
 * Persistence of Vision Ray Tracer ('POV-Ray') version 3.7.
 * Copyright 1991-2013 Persistence of Vision Raytracer Pty. Ltd.
 *
 * POV-Ray is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * POV-Ray is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * ---------------------------------------------------------------------------
 * POV-Ray is based on the popular DKB raytracer version 2.12.
 * DKBTrace was originally written by David K. Buck.
 * DKBTrace Ver 2.0-2.12 were written by David K. Buck & Aaron A. Collins.
 * ---------------------------------------------------------------------------
 * $File: //depot/public/povray/3.x/vfe/unix/vfeplatform.cpp $
 * $Revision: #1 $
 * $Change: 6069 $
 * $DateTime: 2013/11/06 11:59:40 $
 * $Author: chrisc $
 *******************************************************************************/

// must come first
#include "syspovconfig.h"

#include <pthread.h>
#include <stdlib.h>
#include <string.h>

#ifdef HAVE_TIME_H
# include <time.h>
#endif

#ifdef HAVE_SYS_TIME_H
# include <sys/time.h>
#endif

#include "vfe.h"
#include "unixoptions.h"

namespace vfePlatform
{
	using namespace vfe;

	bool gShelloutsPermittedFixThis = false;

	/////////////////////////////////////////////////////////////////////////
	// return a number that uniquely identifies the calling thread amongst
	// all other running threads in the process (and preferably in the OS).
	POVMS_Sys_Thread_Type GetThreadId ()
	{
		return (POVMS_Sys_Thread_Type) pthread_self();
	}

	//////////////////////////////////////////////////////////////
	// User-interface functions
	//////////////////////////////////////////////////////////////

	vfeUnixSession::vfeUnixSession(int id) :
		m_LastTimestamp(0), m_TimestampOffset(0), vfeSession(id)
	{
		m_OptionsProc = boost::shared_ptr<UnixOptionsProcessor>(new UnixOptionsProcessor(this));
	}

	/////////////////////////////////////////////////////////////////////////
	// this method will get called when a render completes and the image writing
	// code is unable to write the requested output file for some reason (e.g.
	// disk full, existing output file is read-only, or whatever). the return
	// value can determine any one of several possible actions the code will take.
	// it's up to you what you do here, but a typical example would be to display
	// an error dialog showing the reason (which will be a short string) and the old
	// path, and allow the user to browse for a new path (you may want to auto-
	// suggest one). it is allowable to perform tests yourself on the path to
	// obtain some OS-specific information as to why it failed in order to better
	// determine what to suggest as the new path (e.g. the the output path is
	// not writable due to insufficient privileges, you may want to default the
	// new path to the user's home directory).
	//
	// any new path that is to be returned should be placed in NewPath. the parameter
	// CallCount will initially be 0, and represents the number of times the
	// method has been called for a given rendering; this allows you for example
	// to auto-default on the first call and prompt the user on subsequent ones.
	// (note that if you do this and the auto-default succeeds, it's up to you to
	// notify the user that the output file is not what they originally asked for).
	//
	// you may want to place a timeout on this dialog if the session is running
	// an animation and return in the case of a timeout a default value.
	//
	// the return values can be any of the following:
	//
	//   0 : don't try again, leave the render without an output file but preserve
	//       the state file (so a render with +c will reload the image data).
	//   1 : reserved for later support
	//   2 : don't try again and delete the state file (rendered data can't be
	//       recovered).
	//   3 : try again with the same file (NewPath is ignored).
	//   4 : try again with the new path returned in NewPath.
	//
	// note that if you choose to specify any of the "don't try again" options,
	// and the session happens to be an animation, and it's likely the same
	// error will occur again (e.g. invalid output path or something), then
	// you may want to call the render cancel API so the user isn't bombarded
	// with an error message for each frame of the render.
	int vfeUnixSession::RequestNewOutputPath(int CallCount, const string& Reason, const UCS2String& OldPath, UCS2String& NewPath)
	{
		// TODO: print warning and cancel?
		return 0;
	}

	//////////////////////////////////////////////////////////////
	// File-system related support functions
	//////////////////////////////////////////////////////////////

	/////////////////////////////////////////////////////////////////////////
	// return an absolute path including trailing path separator.
	// *nix platforms might want to just return "/tmp/" here.
	UCS2String vfeUnixSession::GetTemporaryPath(void) const
	{
		return ASCIItoUCS2String (m_OptionsProc->GetTemporaryPath().c_str());
	}

	/////////////////////////////////////////////////////////////////////////
	// might be better called 'CreateTemporaryFileName()' since pov
	// doesn't actually want the file opened; just the full path and
	// name to one that it can use.
	UCS2String vfeUnixSession::CreateTemporaryFile(void) const
	{
		char str [FILE_NAME_LENGTH] = "";
		snprintf(str, FILE_NAME_LENGTH, "%spov%d", m_OptionsProc->GetTemporaryPath().c_str(), getpid ());
		DELETE_FILE (str);

		return ASCIItoUCS2String (str);
	}

	/////////////////////////////////////////////////////////////////////////
	// you could check that the path given lies within the paths that
	// your platform gives out for temporary files if you want; this
	// example doesn't do that but it's not a bad idea to add.
	void vfeUnixSession::DeleteTemporaryFile(const UCS2String& filename) const
	{
		DELETE_FILE (UCS2toASCIIString (filename).c_str());
	}

	//////////////////////////////////////////////////////////////
	// vfe or POVMS related support functions
	//////////////////////////////////////////////////////////////

	/////////////////////////////////////////////////////////////////////////
	// This method will get called on POVMS critical errors (e.g. cannot
	// allocate memory, amongst other things). Note that you should not
	// assume that you can e.g. allocate memory when processing this call.
	//
	// Before calling this function vfe sets a flag that prevents the worker
	// thread from processing any more messages; it will wait for you to
	// call Shutdown(). Your UI thread can find out if this method has been
	// called by checking for the presence of the stCriticalError status bit
	// returned from vfeSession::GetStatus() (note that this bit is not
	// necessarily already set at the time the method is called though).
	//
	// If you are running a genuine virtual frontend (e.g. stateless HTTP
	// interface), we may want to set a flag to display the message later
	// rather than pop up a messagebox on the local windowstation. Otherwise
	// you would probably display the message immediately.
	void vfeUnixSession::NotifyCriticalError (const char *message, const char *filename, int line)
	{
		fprintf (stderr, "POV-Ray Critical Error: %s", message);
	}

	////////////////////////////////////////////////////////////////////////
	// Return a timestamp to be used internally for queue sorting etc. The
	// value returned must be 64-bit and in milliseconds; the origin of the
	// count is not important (e.g. milliseconds since 1/1/1970, or whatever
	// it doesn't matter), as long as it is consistent value (milliseconds
	// since system boot is NOT a valid value since it will change each boot).
	// Also please don't call time() and multiply by 1000 since vfe wants at
	// least 100ms precision (so it can do sub-one-second event timing).
	//
	// It's also important that the count not go backwards during the life
	// of a vfeSession instance; this means you should attempt to detect wall
	// clock changes by caching the last value your implementation returns
	// and adding an appropriate offset if you calculate a lower value later
	// in the session.
	POV_LONG vfeUnixSession::GetTimestamp(void) const
	{
		POV_LONG timestamp = 0;  // in milliseconds
#ifdef HAVE_CLOCK_GETTIME
		struct timespec ts;
		if (clock_gettime(CLOCK_REALTIME, &ts) == 0)
			timestamp = (POV_LONG) (1000)*ts.tv_sec + ts.tv_nsec/1000000;
		else
#endif
#ifdef HAVE_GETTIMEOFDAY
		{
			struct timeval tv;  // seconds + microseconds since the Epoch (1970-01-01)
			if (gettimeofday(&tv, NULL) == 0)
				timestamp = (POV_LONG) (1000)*tv.tv_sec + tv.tv_usec/1000;
		}
#endif
// FIXME: add fallback using boost::xtime()
		timestamp += m_TimestampOffset;
		if (timestamp < m_LastTimestamp)
		{
			// perhaps the system clock has been adjusted?
			m_TimestampOffset += m_LastTimestamp - timestamp;
			timestamp = m_LastTimestamp;
		}
		else
			m_LastTimestamp = timestamp;
		return timestamp;
	}

	/////////////////////////////////////////////////////////////////////////
	// called when the worker thread starts - you could if you like set the
	// thread priority here.
	void vfeUnixSession::WorkerThreadStartup()
	{
	}

	/////////////////////////////////////////////////////////////////////////
	// called just before the worker thread exits.
	void vfeUnixSession::WorkerThreadShutdown()
	{
	}

	/////////////////////////////////////////////////////////////////////////
	// The following methods support the I/O permissions feature
	/////////////////////////////////////////////////////////////////////////

	/////////////////////////////////////////////////////////////////////////
	// case-sensitive string comparison
	bool vfeUnixSession::StrCompare (const UCS2String& lhs, const UCS2String& rhs) const
	{
		return lhs.compare(rhs);
	}

	/////////////////////////////////////////////////////////////////////////
	// return true if the path component of file is equal to the path component
	// of path. will also return true if recursive is true and path is a parent
	// of file. does not support relative paths, and will perform case-sensitive
	// comparisons.
	bool vfeUnixSession::TestPath (const Path& path, const Path& file, bool recursive) const
	{
		// we don't support relative paths
		if (path.HasVolume() == false || file.HasVolume() == false)
			return (false);
		if (StrCompare(path.GetVolume(), file.GetVolume()) == false)
			return (false);

		vector<UCS2String> pc = path.GetAllFolders();
		vector<UCS2String> fc = file.GetAllFolders();
		if (fc.size() < pc.size())
			return (false) ;
		for (int i = 0 ; i < pc.size(); i++)
			if (StrCompare(fc[i], pc[i]) == false)
				return (false) ;
		return (fc.size() == pc.size() ? true : recursive);
	}

	/////////////////////////////////////////////////////////////////////////
	// returns true if opening the given file in the specified mode is to
	// be permitted. it is allowed to ask the user, so this method could
	// take some time to return (especially if user is not at workstation ...)
	//
	// TODO: modify this to work fully with UCS2 strings (no conversion)
	bool vfeUnixSession::TestAccessAllowed (const Path& file, bool isWrite) const
	{
		if (!m_OptionsProc->isIORestrictionsEnabled(isWrite))
			return true;

		string FullFnm = m_OptionsProc->CanonicalizePath(UCS2toASCIIString(file()));
		if(FullFnm.length() == 0)
			return false;

		Path fullPath(FullFnm);

		if (isWrite)
		{
			// we do special-case hard-coded exclusion test(s) here
			for (IOPathVector::const_iterator it = GetExcludedPaths().begin(); it != GetExcludedPaths().end(); it++)
			{
				if (TestPath(it->GetPath(), fullPath, it->IsRecursive()))
				{
					m_OptionsProc->IORestrictionsError(UCS2toASCIIString(file()), isWrite, false);
					return (false) ;
				}
			}
		}
		else
		{
			// it's a read
			for (IOPathVector::const_iterator it = GetReadPaths().begin(); it != GetReadPaths().end(); it++)
				if (TestPath(it->GetPath(), fullPath, it->IsRecursive()))
					return (true) ;

			// access for write implies access for read (this is by design).
			// so we check the write specs below instead of giving an error here.
		}

		for (IOPathVector::const_iterator it = GetWritePaths().begin(); it != GetWritePaths().end(); it++)
			if (TestPath(it->GetPath(), fullPath, it->IsRecursive()))
				return (true) ;

		m_OptionsProc->IORestrictionsError(UCS2toASCIIString(file()), isWrite, true);
		return (false);
	}

	/////////////////////////////////////////////////////////////////////////////
	// Shellout support class (UnixShelloutProcessing)
	/////////////////////////////////////////////////////////////////////////////
	// See the comments in source/frontend/shelloutprocessing.h for documentation
	// on the requirements for these methods.
	/////////////////////////////////////////////////////////////////////////////

	UnixShelloutProcessing::UnixShelloutProcessing(POVMS_Object& opts, const string& scene, uint width, uint height): ShelloutProcessing(opts, scene, width, height)
	{
		m_ProcessRunning = false;
		m_ProcessId = m_LastError = m_ExitCode = 0;
	}

	UnixShelloutProcessing::~UnixShelloutProcessing()
	{
		if (UnixShelloutProcessing::CommandRunning())
			KillShellouts(1, true);
		CollectCommand();
	}

	bool UnixShelloutProcessing::ExecuteCommand(const string& cmd, const string& params)
	{
#if 0
		if (UnixShelloutProcessing::CommandRunning())
			throw POV_EXCEPTION(kNotNowErr, "Cannot create new process as previous shellout has not terminated");

		CollectCommand();
		m_Command = cmd;
		m_Params = params;
		m_ProcessId = m_LastError = m_ExitCode = 0;

		// TODO: IMPLEMENT (see vfe/win/vfeplatform.cpp for example)
#else
		// until we get a UNIX guy to assist, we just use system to do as simple an implementation as possible.
		// no background support or output piping is done.
		m_Command = cmd;
		m_Params = params;
		m_ProcessId = m_LastError = m_ExitCode = 0;

		string command = cmd + " " + params;
		boost::trim(command);
		if (command.empty())
			throw POV_EXCEPTION(kParamErr, "Empty shellout command");
		if (*command.rbegin() == '&') // background processing not supported
			throw POV_EXCEPTION(kParamErr, "Background execution of shellout commands not currently supported");

		m_ProcessRunning = true;
		int result = system(command.c_str());
		m_ProcessRunning = false;
		if (result == -1)
		{
			m_LastError = errno;
			m_ExitCode = -1;
		}
		else
		{
			m_ExitCode = WEXITSTATUS(result);
		}
#endif

		return false;
	}

	bool UnixShelloutProcessing::CommandRunning(void)
	{
		// TODO: IMPLEMENT (see vfe/win/vfeplatform.cpp for example)
		return m_ProcessRunning;
	}

	int UnixShelloutProcessing::ProcessID(void)
	{
		if (!UnixShelloutProcessing::CommandRunning())
			return 0;
		return m_ProcessId;
	}

	bool UnixShelloutProcessing::KillCommand(int timeout, bool force)
	{
		if (!UnixShelloutProcessing::CommandRunning())
			return true;
		// TODO: IMPLEMENT (see vfe/win/vfeplatform.cpp for example)
		throw POV_EXCEPTION(kCannotHandleRequestErr, "Kill for shellout commands not currently supported");
	}

	int UnixShelloutProcessing::CollectCommand(void)
	{
		if (m_ProcessRunning == false)
			return -2;
		if (UnixShelloutProcessing::CommandRunning())
			return -1;
		// TODO: IMPLEMENT (see vfe/win/vfeplatform.cpp for example)
		return m_ExitCode;
	}

	int UnixShelloutProcessing::CollectCommand(string& output)
	{
		// TODO: IMPLEMENT IF OUTPUT COLLECTION TO BE SUPPORTED
		return CollectCommand();
	}

	bool UnixShelloutProcessing::CommandPermitted(const string& command, const string& parameters)
	{
		// until we get a unix support guy, this is just a hack: use a global
		string cmd = command + " " + parameters;
		boost::trim(cmd);
		if (command.empty() || *command.rbegin() == '&')
			return false;
		return gShelloutsPermittedFixThis;
	}
}
