//-------------------------------------------------------------------------------------------------
// <copyright file="main.cpp" company="LiquidRobotics">
//  Copyright (c) Liquid Robotics Corporation, confidential and proprietary.  All rights reserved. 
// </copyright>
//
// <summary>
//   Implementation of the 'timelimit2' program, which can be used to run a command
//   for up to some maximum number of seconds, and then kill it if it hasn't completed
//   within the time limit.  The invocation format is:
//
//		timelimit2 --timeout=nnn {--sigal=nnn} {--debug} ... command line parameters ...
//
//   The options are as follows:
//
//	 --timeout sets the number of seconds to run for; this is a required setting.
//
//   --signal sets the signal to use when killing the process, which is SIGKILL (9)
//   by default
//
//   --debug, if specified, turns on debug output
// </summary>
//
// <owner>Mike Cookson</owner>
//-------------------------------------------------------------------------------------------------

#include <iostream>
#include <sstream>
#include <string>
#include <stdexcept>
#include <vector>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <boost/algorithm/string.hpp>
#include <boost/lexical_cast.hpp>
#include "LqrSMACAsynchronousProcessExecutor.h"
#include "LqrSMACLogger.h"
#include "LqrSMACSemaphore.h"
#include "LqrSMACStdOutLogger.h"
#include "LqrSMACUptime.h"

namespace LiquidRobotics {
namespace timelimit2 {

class TimeLimitProgram
{
public:

	typedef LiquidRobotics::SMACFramework::AsynchronousProcessExecutor AsynchronousProcessExecutor;
	typedef LiquidRobotics::SMACFramework::ByteArray ByteArray;
	typedef LiquidRobotics::SMACFramework::Semaphore Semaphore;
	typedef std::string string;
	typedef std::vector<string> StringArray;

	static void display(const StringArray& ar, const string& name)
	{
#if 0
		using namespace std;

		cout << name << " with " << ar.size() << " elements: " << endl;

		for (unsigned idx = 0; idx < ar.size(); ++idx)
			cout << "(" << idx << ") " << ar[idx] << endl;
#endif
	}

	TimeLimitProgram(int argc, char* argv[])
	: debug(false)
	, exitSem(0)
	, sig(SIGKILL)
	, debugTag("--debug")
	, signalTag("--signal=")
	, timeoutTag("--timeout=")
	{
		using namespace std;

		progArgs.resize(argc);

		for (int idx = 0; idx < argc; ++idx)
			progArgs[idx] = argv[idx];

		display(progArgs, "progArgs");
	}

	int Run()
	{
		SetOptions(progArgs);
		return RunProgram();
	}

	void SetOptions(const StringArray& args)
	{
		using namespace std;
		using namespace boost;

		StringArray& dest = runParams;
		dest.assign(args.begin(), args.end());

		display(dest, "dest/runParams");

		bool debugSet = false;
		bool signalSet = false;
		bool timeoutSet = false;

		if (dest.size())
			dest.erase(dest.begin());

		while(dest.size() && starts_with(dest[0], "--"))
		{
			const string& s = dest[0];

			if (iequals(s, debugTag))
			{
				ThrowDoubleSpec(debugTag, debugSet);
				debug = true;
			}
			else if (istarts_with(s, signalTag))
			{
				ThrowDoubleSpec(signalTag, signalSet);
				const string tail = s.substr(signalTag.length());
				sig = lexical_cast<int>(tail);
			}
			else if (istarts_with(s, timeoutTag))
			{
				ThrowDoubleSpec(timeoutTag, timeoutSet);
				string tail = s.substr(timeoutTag.length());
				timeout = lexical_cast<float>(tail);
				if (timeout < 0) throw invalid_argument("Cannot specify a negative timeout value");
			}
			else
			{
				stringstream msg;
				msg << "Unknown option " << s << endl;
				throw invalid_argument(msg.str());
			}

			dest.erase(dest.begin());
		}

		if (!timeoutSet)
			throw invalid_argument("Missing timeout specification");

		display(dest, "dest/runParams (2)");
	}

	int RunProgram()
	{
		using namespace std;
		using namespace LiquidRobotics::SMACFramework;

		Logger::Ptr logger;

		if (debug)
			logger.reset(new StdOutLogger(progArgs.size() ? progArgs[0].c_str() : "---", 1, Logger::LT_DEBUG));

		AsynchronousProcessExecutor ape(logger, sig, true);
		AsynchronousProcessExecutor::ScopedConnectionPtr exitSubscription =
			ape.SubscribeExitSignal(bind(&TimeLimitProgram::OnExit, this, _1, _2));
		AsynchronousProcessExecutor::ScopedConnectionPtr rptSubscription =
			ape.SubscribeReportSignal(bind(&TimeLimitProgram::OnReport, this, _1, _2, _3));

		ape.RunAsynch(runParams, true, true);

		const unsigned timeoutVal = (unsigned) ceil(1000.0*timeout);
		const bool timedOut = !exitSem.TimedWait(timeoutVal);
		int result;

		if (timedOut)
		{
			ape.Kill();
			sleep(1);
			//cout << "executing = " << ape.ProcessExecuting() << " exited = " << ape.ProcessExited() << endl;
			result = 1;
		}
		else
		{
			const int resultCode = ape.GetResultCode();
			result =  WIFEXITED(resultCode) ? WEXITSTATUS(resultCode) : 1;
		}

		return result;
	}

private:


	void OnExit(AsynchronousProcessExecutor& sender, int resultCode)
	{
		exitSem.Post();
	}

	void OnReport(AsynchronousProcessExecutor& sender, const ByteArray& dataReceived, bool fromStdErr)
	{
		if (dataReceived.size())
		{
			const int fd = fromStdErr ? 2 : 1;
			const ssize_t status = write(fd, &dataReceived.front(), dataReceived.size());
			if (status) { } // avoid var not used or return value not used
		}
	}

	static void ThrowDoubleSpec(const string& opt, bool& spec)
	{
		using namespace std;

		if (spec)
		{
			stringstream msg;
			msg << opt << " was specified more than once";
			throw invalid_argument(msg.str());
		}
		else
			spec = true;
	}

	bool debug;
	Semaphore exitSem;
	int sig;
	StringArray progArgs;
	StringArray runParams;
	float startTime;
	float timeout;
	const string debugTag;
	const string signalTag;
	const string timeoutTag;
};

}
}

using namespace std;
using namespace LiquidRobotics::timelimit2;

int main(int argc, char* argv[])
{
	int result = 0;
	TimeLimitProgram program(argc, argv);

	try
	{
		result = program.Run();
	}
	catch(exception& ex)
	{
		cerr << endl
		     << argv[0] << " caught unhandled exception " << typeid(ex).name() << ": " << ex.what() << endl;

		result = 1;
	}
	catch(...)
	{
		cerr << endl
		     << argv[0] << " caught unhandled generic exception" << endl;

		result = 1;
	}

	return result;
}
