/*******************************************************************************
 * shelloutprocessing.cpp
 *
 * ---------------------------------------------------------------------------
 * 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/source/frontend/shelloutprocessing.cpp $
 * $Revision: #1 $
 * $Change: 6069 $
 * $DateTime: 2013/11/06 11:59:40 $
 * $Author: chrisc $
 *******************************************************************************/

#include <string>
#include <cctype>
#include <boost/format.hpp>
#include <boost/algorithm/string.hpp>

// configbase.h must always be the first POV file included within base *.cpp files
#include "base/configbase.h"
#include "base/types.h"
#include "base/povmscpp.h"
#include "base/povmsgid.h"

#include "frontend/configfrontend.h"
#include "frontend/shelloutprocessing.h"

// this must be the last file included
#include "base/povdebug.h"

namespace pov_frontend
{

using namespace pov_base;

ShelloutAction::ShelloutAction(const ShelloutProcessing *sp, unsigned int attribID, POVMS_Object& opts): returnAction(ignore), returnNegate(false), isSet(false)
{
	if (opts.Exist(attribID))
	{
		POVMS_Object attr;

		opts.Get(attribID, attr);
		if (attr.Exist(kPOVAttrib_CommandString))
		{
			rawCommand = attr.GetString(kPOVAttrib_CommandString);
			if (sp->ExtractCommand(rawCommand, command, rawParameters))
			{
				int action = attr.TryGetInt(kPOVAttrib_ReturnAction, (int) ignore);
				switch (tolower(action))
				{
					case ignore:
					case skipOne:
					case skipAll:
					case quit:
					case abort:
					case fatal:
						returnAction = (Action) tolower(action);
						returnNegate = isupper(action);
						isSet = true;
						break;

					default:
						break;
				}
			}
		}
	}
}

// expand the parameters in the rawParameters member into the parameters member, applying the escapes
// documented below. scene is the scene name, ofn the output file name, w/h the width/height, clock the
// current animation clock value, and frame the current frame number.
void ShelloutAction::ExpandParameters(const string& scene, const string& ofn, unsigned int w, unsigned int h, float clock, unsigned int frame)
{
	// %o: output file name in full
	// %s: scene name without path or extension
	// %n: frame number
	// %k: clock value
	// %h: height
	// %w: width
	// %%: percent character

	parameters.clear();
	for (const char *cmd = rawParameters.c_str(); *cmd; cmd++)
	{
		if (*cmd != '%')
		{
			parameters.push_back(*cmd);
			continue;
		}
		switch (*++cmd)
		{
			case '%':
				parameters.append("%%");
				break;

			case 'o':
				parameters.append(ofn);
				break;

			case 's':
				parameters.append(scene);
				break;

			case 'n':
				parameters.append(str(boost::format("%u") % frame));
				break;

			case 'k':
				parameters.append(str(boost::format("%#.6f") % clock));
				break;

			case 'h':
				parameters.append(str(boost::format("%u") % h));
				break;

			case 'w':
				parameters.append(str(boost::format("%u") % w));
				break;

			default:
				parameters.push_back('%');
				parameters.push_back(*cmd);
				break;
		}
	}
}

// ------------------------------------------------------------------

ShelloutProcessing::ShelloutProcessing(POVMS_Object& opts, const string& scene, unsigned int width, unsigned int height): sceneName(scene), imageWidth(width), imageHeight(height)
{
	skipReturn = cancelReturn = exitCode = 0;
	clockVal = 0.0;
	frameNo = 0;
	hadPreScene = hadPostScene = killRequested = skipCallouts = cancelRender = skipNextFrame = skipAllFrames = false;
	commandProhibited = processStartRequested = hadUserAbort = hadFatalError = false;
	postProcessEvent = lastShelloutEvent;
	cancelFormat.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit);
	skipFormat.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit);
	shellouts[preFrame].reset(new ShelloutAction(this, kPOVAttrib_PreFrameCommand, opts));
	shellouts[postFrame].reset(new ShelloutAction(this, kPOVAttrib_PostFrameCommand, opts));
	shellouts[preScene].reset(new ShelloutAction(this, kPOVAttrib_PreSceneCommand, opts));
	shellouts[postScene].reset(new ShelloutAction(this, kPOVAttrib_PostSceneCommand, opts));
	shellouts[userAbort].reset(new ShelloutAction(this, kPOVAttrib_UserAbortCommand, opts));
	shellouts[fatalError].reset(new ShelloutAction(this, kPOVAttrib_FatalErrorCommand, opts));
}

ShelloutProcessing::~ShelloutProcessing()
{
}

// called by the internal parser during construction to separate commands from parameters.
// given a raw string in the form returned from the POV INI file, extract the command and any parameters.
// the default version of this method should suffice for most implementations, however some platforms
// may need different treatment of quotes or escapes. for example, the windows platform may wish to
// provide a special-case for strings that look like windows paths, and exempt them from escaping of
// the backslash.
//
// the default method will trim the source and then search it for the first whitespace character
// that is both outside of a quoted string and not escaped. this forms the boundary between the
// command and the parameters (if not found, the entire source is considered the command). the code
// accepts single and double quotes as acceptable delimiters. if quotes are found around the command,
// they are removed. no other quotes (including any within the parameters) will be altered.
//
// when parsing the command portion of the string, any backslashes found are considered to be escapes
// which remove the special meaning of the following character. the escape is removed and the next
// character will not be subject to special interpretation (this affects single quote, double quote,
// backslashes, and any whitespace characters. any escapes in the text after the point where the
// parameters start will not be removed.
//
// this method should return true if the command is non-empty upon completion.
bool ShelloutProcessing::ExtractCommand(const string& src, string& command, string& parameters) const
{
	bool inSQ = false;
	bool inDQ = false;
	bool hadEscape = false;
	const char *s;

	command.clear();
	parameters.clear();
	string str = boost::trim_copy(src);
	for (s = str.c_str(); *s; *s++)
	{
		if (hadEscape)
		{
			hadEscape = false;
			command.push_back(*s);
		}
		else if (*s == '\\')
		{
			hadEscape = true;
		}
		else
		{
			if (*s == '"')
				inDQ = !inDQ;
			else if (*s == '\'')
				inSQ = !inSQ;
			else if (isspace(*s) && !inSQ && !inDQ)
				break;
			command.push_back(*s);
		}
	}

	boost::trim(command);
	if (command.empty())
		return false;

	if (command.size() > 1)
	{
		char ch1 = command[0];
		char ch2 = command[command.size() - 1];
		if ((ch1 == '\'' || ch1 == '"') && ch1 == ch2)
		{
			command = boost::trim_copy(command.substr(1, command.size() - 2));
			if (command.empty())
				return false;
		}
	}

	parameters = boost::trim_copy(string(s));
	return true;
}

string ShelloutProcessing::GetPhaseName(shelloutEvent event)
{
	switch (event)
	{
		case preScene:
			return "pre-scene";

		case postScene:
			return "post-scene";

		case preFrame:
			return "pre-frame";

		case postFrame:
			return "post-frame";

		case userAbort:
			return "user abort";

		case fatalError:
			return "fatal error";

		default:
			return "";
	}
}

bool ShelloutProcessing::HandleProcessEvent(shelloutEvent event, bool internalCall)
{
	if (hadPostScene)
		if (event < postScene)
			return cancelRender;
	if (!hadPreScene)
		event = preScene;

	bool skipWasSet(skipCallouts);
	string phaseName(GetPhaseName(event));
	ShelloutPtr sh(shellouts[event]);

	switch (event)
	{
		case preScene:
			if (hadPreScene)
				return cancelRender;
			hadPreScene = true;
			phaseName = "pre-scene";
			break;

		case postScene:
			if (hadPostScene)
				return cancelRender;
			hadPostScene = true;
			phaseName = "post-scene";
			break;

		case preFrame:
			if (skipNextFrame || skipAllFrames)
				return cancelRender;
			phaseName = "pre-frame";
			break;

		case postFrame:
			if (skipNextFrame || skipAllFrames)
				return cancelRender;
			phaseName = "post-frame";
			break;

		case userAbort:
			if (hadUserAbort)
				return cancelRender;
			hadUserAbort = true;
			phaseName = "user abort";
			break;

		case fatalError:
			// if cancel render is already set, we return unless this is an internal call
			if (cancelRender && !internalCall)
				return true;
			cancelRender = true;
			skipCallouts = true;
			exitCode = 1;
			phaseName = "fatal error";
			break;

		default:
			return cancelRender;
	}

	// skipCallouts is set if an event return indicated we should exit with no further shellouts.
	// skipWasSet is initialized to its value before the above switch
	if (skipWasSet)
		return cancelRender;

	// if there's no command set or shellouts are not supported, just return
	if (!sh->IsSet() || !ShelloutsSupported())
		return cancelRender;

	processStartRequested = true;
	runningProcessName = sh->Command();
	postProcessEvent = event;
	commandProhibited = false;

	// perform the substitutions on the parameter list
	sh->ExpandParameters(sceneName, outputFile, imageWidth, imageHeight, clockVal, frameNo);

	// make sure the command is allowed to run
	if (!CommandPermitted(sh->Command(), sh->Parameters()))
	{
		commandProhibited = true;
		throw POV_EXCEPTION(kCannotOpenFileErr, str(boost::format("Execution of shellout '%1%' prohibited") % sh->Command()));
	}

	try
	{
		ExecuteCommand(sh->Command(), sh->Parameters());
	}
	catch(...)
	{
		skipCallouts = true;
		throw;
	}

	return cancelRender;
}

bool ShelloutProcessing::PostProcessEvent(void)
{
	int             ret;
	string          output;
	shelloutEvent   event = postProcessEvent;
	string          phaseName(GetPhaseName(event));

	processStartRequested = false;
	runningProcessName.clear();
	postProcessEvent = lastShelloutEvent;

	if (event == lastShelloutEvent)
		return cancelRender; // nothing to do

	// we always call collect, even if commandProhibited is true
	// if collect returns 0 and the command was a prohibited one, we set it to 1
	if ((ret = CollectCommand(output)) == 0)
		if (commandProhibited)
			ret = 1;

	// no need to do anything more if a kill was requested
	if (killRequested)
		return true;

	// if the return action had the '!' or '-' prefix, we toggle the meaning of the result.
	bool execResult = ret == 0;
	ShelloutPtr sh(shellouts[event]);
	if (sh->ReturnNegate())
		execResult = !execResult;
	if (execResult == false)
	{
		ShelloutAction::Action action = sh->ReturnAction();
		if (action != ShelloutAction::ignore)
		{
			if (event < userAbort)
			{
				// common case for preScene, postScene, preFrame and postFrame
				switch (action)
				{
					case ShelloutAction::quit:
						// stop render immediately, exit code is 0
						skipCallouts = cancelRender = true;
						cancelPhase = phaseName;
						cancelReason = "quit rendering";
						cancelReturn = ret;
						cancelCommand = sh->Command();
						cancelParameters = sh->Parameters();
						cancelOutput = LastShelloutResult();
						exitCode = 0;
						return true;

					case ShelloutAction::abort:
						// call abort, exit code is 2
						cancelRender = true;
						exitCode = 2;
						HandleProcessEvent(userAbort, true);
						cancelPhase = phaseName;
						cancelReason = "generate a user abort";
						cancelReturn = ret;
						cancelCommand = sh->Command();
						cancelParameters = sh->Parameters();
						cancelOutput = LastShelloutResult();
						return true;

					case ShelloutAction::fatal:
						// call fatal error, exit code is 1
						cancelRender = true;
						exitCode = 1;
						HandleProcessEvent(fatalError, true);
						cancelPhase = phaseName;
						cancelReason = "generate a fatal error";
						cancelReturn = ret;
						cancelCommand = sh->Command();
						cancelParameters = sh->Parameters();
						cancelOutput = LastShelloutResult();
						return true;
				}
			}

			switch (event)
			{
				case preScene:
					switch (action)
					{
						case ShelloutAction::skipOne:
						case ShelloutAction::skipAll:
							// stop render immediately, exit code is 0
							cancelRender = true;
							exitCode = 0;
							skipPhase = cancelPhase = phaseName;
							skipReason = cancelReason = "skip all frames";
							skipReturn = cancelReturn = ret;
							skipCommand = cancelCommand = sh->Command();
							skipParameters = cancelParameters = sh->Parameters();
							skipOutput = cancelOutput = LastShelloutResult();

							// if result was skipAll, don't call post-scene
							if (action == ShelloutAction::skipAll)
								skipCallouts = true;

							return true;
					}
					break;

				case preFrame:
					switch (action)
					{
						case ShelloutAction::skipOne:
						case ShelloutAction::skipAll:
							skipPhase = phaseName;
							skipReturn = ret;
							skipCommand = sh->Command();
							skipParameters = sh->Parameters();
							skipOutput = LastShelloutResult();
							if (action == ShelloutAction::skipAll)
							{
								cancelRender = skipAllFrames = true;
								skipReason = "skip all remaining frames";
								exitCode = 0;
								cancelPhase = phaseName;
								cancelReason = "skip all frames";
								cancelReturn = ret;
								cancelCommand = sh->Command();
								cancelParameters = sh->Parameters();
								cancelOutput = LastShelloutResult();
							}
							else
							{
								skipNextFrame = true;
								skipReason = str(boost::format("skip frame %1%") % (frameNo + 1));
							}
							break;
					}
					break;

				case postFrame:
					switch (action)
					{
						case ShelloutAction::skipOne:
						case ShelloutAction::skipAll:
							cancelRender = skipAllFrames = true;
							cancelPhase = skipPhase = phaseName;
							cancelReturn = skipReturn = ret;
							cancelCommand = skipCommand = sh->Command();
							cancelParameters = skipParameters = sh->Parameters();
							cancelOutput = skipOutput = LastShelloutResult();
							cancelReason = skipReason = "skip all remaining frames";
							break;
					}
					break;

				case userAbort:
					cancelRender = true;
					exitCode = 2;
					cancelPhase = phaseName;
					cancelReason = "generate a user abort";
					cancelCommand = sh->Command();
					cancelParameters = sh->Parameters();
					cancelOutput = LastShelloutResult();
					if (action == ShelloutAction::fatal)
					{
						// call fatal error
						exitCode = 1;
						HandleProcessEvent(fatalError, true);
						cancelReason = "generate a fatal error";
					}
					cancelReturn = ret;
					return true;
			}
		}
	}

	return cancelRender;
}

// returns true if a shellout is currently running. if this method is being called in an
// event loop to wait until a shellout has completed, it is the responsibility of the event
// loop to perform appropriate sleeps to avoid wasting CPU time.
bool ShelloutProcessing::ShelloutRunning(void)
{
	bool result = CommandRunning();

	if (processStartRequested == true && result == false)
		PostProcessEvent();
	return result;
}

// the message is constructed as per the documentation for the boost::format class.
// the positional parameters are as follows:
//   1: the event causing the cancel (as a string), e.g. "pre-scene"
//   2: the POV-Ray return code (as an integer)
//   3: the return code (as an upper-case string), e.g. "USER ABORT"
//   4: the return code (as a lower-case string).
//   5: the reason for the cancel (as a string), e.g. "generate a user abort"
//   6: the command name that generated the cancel
//   7: the command parameters (CAUTION: may contain escape codes)
//   8: the command return code (as an integer)
//   9: output text from the command, as returned by LastShelloutResult()
string ShelloutProcessing::GetCancelMessage(void)
{
	return str(
		cancelFormat % cancelPhase
		             % exitCode
		             % ExitDesc()
		             % boost::to_lower_copy(ExitDesc())
		             % cancelReason
		             % cancelCommand
		             % cancelParameters
		             % cancelReturn
		             % cancelOutput);
}

// the message is constructed as per the documentation for the boost::format class.
// the positional parameters are as follows:
//   1: the event causing the skip (as a string), e.g. "pre-scene"
//   2: the type of the skip (as a string); e.g. "skip frame 11" or "skip all frames"
//   3: the command name that generated the skip
//   4: the command parameters (CAUTION: may contain escape codes)
//   5: the command return code (as an integer)
//   6: output text from the command, as returned by LastShelloutResult()
string ShelloutProcessing::GetSkipMessage(void)
{
	return str(
		skipFormat % skipPhase
		           % skipReason
		           % skipCommand
		           % skipParameters
		           % skipReturn
		           % skipOutput);
}

// shutdown any currently-running shellouts. if force is true, force them to exit.
// in either case, don't wait more than timeout seconds. return true if there are
// no more processes running afterwards.
bool ShelloutProcessing::KillShellouts(int timeout, bool force)
{
	killRequested = true;
	return KillCommand(timeout, force);
}

// execute the given command with the supplied parameters, which have already
// been expanded as per the docs, and immediately return true without waiting
// for completion of the process. if the command can't be run other than for
// one of the reasons documented below, return false (in which case CollectCommand
// should return -2 if called later on).
//
// if shellouts are not supported, or access to the executable is prohibited by
// POV-Ray internal rules (not the OS), throw a kCannotOpenFile exception with an
// appropriate message. you should also throw a (different) exception if a process
// is still running. any exception thrown will cancel a render (including remaining
// frames of an animation) and the fatal error shellout (if defined) will not be
// called.
//
// you should reap any processes in your destructor in case CollectCommand doesn't
// get called.
//
// if the platform implemeting a subclass of this method has the equivalent of a
// system log (e.g. syslog on unix, event log on windows), the implementation should
// consider providing a user-controllable option to log any commands using such.
bool ShelloutProcessing::ExecuteCommand(const string& cmd, const string& params)
{
	throw POV_EXCEPTION(kCannotOpenFileErr, "Shellouts not implemented on this platform");
}

}
