//----------------------------------------------------------------------------------
// <copyright file="CommonSendCommand.cpp" company="LiquidRobotics">
//
// Copyright (c) 2014 Liquid Robotics, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// This software is associated with a release.  Liquid Robotics makes no
// representation that the design, interfaces or component distribution will
// remain unchanged in future releases.
//
// </copyright>
//
// <summary>
// 	Implementation of CommonSendCommand, which handles most of the SendCommand CORBA
//  method
// </summary>
//
// <owner>Mike Cookson</owner>
//----------------------------------------------------------------------------------

#include "CommonSendCommand.h"
#include "LqrSMACPipes.h"
#include "LqrSMACByteArray.h"
#include "LqrSMACDetermineFileExtension.h"
#include "LqrSMACCmdLineParser.h"
#include "LqrSMACLexicalCast.h"
#include "nsextend.h"
#include "SendCommandCompleteEvent.h"

using namespace std;
using namespace LiquidRobotics::SMACFramework;
using namespace LiquidRobotics::EventLib;

namespace LiquidRobotics {
namespace PayloadInterface {

CommonSendCommand::CommonSendCommand(
		IObjectBroker& _broker,
		EventTransceiver& _et,
		Logger& _logger,
		LocalTransactionCounter& _ltc,
		const PayloadInterfaceController_Config& _config,
		PayloadManagerClient& _pmClient)
: AlarmSubsystem(_et, _logger, _config, _broker, _pmClient)
, broker(_broker)
, et(_et)
, logger(_logger)
, ltc(_ltc)
, config(_config)
, pmClient(_pmClient)
{
}

CommonSendCommand::~CommonSendCommand()
{
}

void CommonSendCommand::SendCommand(const ::CORBA::Any& _command, const char* requester, const char* transactionID, ::CORBA::Any_OUT_arg result)
{
	using namespace LiquidRobotics::SMAC;
	ByteArrayPtr incomingData;
	const char* cmdParam = "_command";

	try
	{
		EventPropertyValue epv(_command, broker);

		switch(epv.GetStorageType())
		{
		case EventPropertyValue::ST_STRING:
			{
				incomingData.reset(new ByteArray);
				epv.ToBinary(*incomingData);
				break;
			}

		case EventPropertyValue::ST_UINT8_VECTOR:
			{
				incomingData = epv.GetByteArray();
				break;
			}

		default:
			{
				stringstream msg;
				msg << "SendCommand invoked with unsupported data type " << epv;
				Logger::Locked(logger)->Error << msg.str() << endl;
				ThrowCORBAException2<BadParameter>(cmdParam, msg.str());
			}
		}
	}
	catch(exception& ex)
	{
		stringstream msg;
		msg << "Caught " << typeid(ex).name() << " '" << ex.what() << "' while unpacking command in SendCommand";
		Logger::Locked(logger)->Error << msg.str() << endl;
		ThrowCORBAException2<BadParameter>(cmdParam, msg.str());
	}

	if (!apePtr)
	{
		apePtr.reset(new AsynchronousProcessExecutor(logger));
		connectionToAPEExit = apePtr->SubscribeExitSignal(bind(&CommonSendCommand::APEExit, this, _1, _2));
		connectionToAPEReport = apePtr->SubscribeReportSignal(bind(&CommonSendCommand::APEReport, this, _1, _2, _3));
	}

	if (IsTarFormat(*incomingData)) // this could be too slow
	{
		if (!config.GetTarProcessingCommand().length())
		{
			stringstream msg;
			msg << "Command in tarball format received, but tar processing is disabled";

			Logger::Locked(logger)->Error << msg.str() << endl;
			ThrowCORBAException2<BadParameter>(cmdParam, msg.str());
		}

		ThrowIfEmptyOrNull("requester", requester);
		ThrowIfEmptyOrNull("transactionID", transactionID);

		AsynchronousProcessExecutor::Lock apeLock(*apePtr);
		ThrowIfCommandExecuting();

		backgroundProcessData.reset(requester, transactionID);

		try
		{
			backgroundProcessData.tempFile.reset(new TempFile);
			backgroundProcessData.tempFile->Write(*incomingData);
			backgroundProcessData.tempFile->Close();

			stringstream cmdLine;
			cmdLine << config.GetTarProcessingCommand() << " '" << backgroundProcessData.tempFile->GetName() << "'";

			string cmdLineStr = cmdLine.str();

			apePtr->RunAsynch(cmdLineStr.c_str(), true, true);
		}
		catch(exception& ex)
		{
			backgroundProcessData.reset();

			stringstream msg;
			msg << "Caught " << typeid(ex).name() << " '" << ex.what() << "' while attempting to execute user tarball command";

			Logger::Locked(logger)->Error << msg.str() << endl;
			ThrowCORBAException2<BadParameter>(cmdParam, msg.str());
		}

		throw WillCompleteAsynchronously();
	}
	else
	{
		string stringCmd;
		stringCmd.assign((const char*) &incomingData->front(), incomingData->size());

		CmdLineParser parser(stringCmd);
		const int argCount = parser.Count();

		if (!argCount)
		{
			stringstream msg;
			msg << "Nothing to process";

			Logger::Locked(logger)->Error << msg.str() << endl;

			ThrowCORBAException2<BadParameter>(cmdParam, msg.str());
		}
		else if (boost::iequals(parser[0], "GETMODULES"))
		{
			if (argCount != 1)
			{
				stringstream msg;
				msg << "GETMODULES does not take any arguments";

				Logger::Locked(logger)->Error << msg.str() << endl;
				ThrowCORBAException2<BadParameter>(cmdParam, msg.str());
			}

			ThrowIfCommandExecuting();
			ThrowIfEmptyOrNull("requester", requester);
			ThrowIfEmptyOrNull("transactionID", transactionID);
			backgroundProcessData.reset(requester, transactionID);
			boost::thread thd(boost::bind(&CommonSendCommand::DumpPayloadManagerConfig, this));
			throw WillCompleteAsynchronously();
		}
		else if (boost::iequals(parser[0], "RUNSHELL"))
		{
			if (argCount < 2)
			{
				stringstream msg;
				msg << "RUNSHELL must be followed by at least one token";

				Logger::Locked(logger)->Error << msg.str() << endl;
				ThrowCORBAException2<BadParameter>(cmdParam, msg.str());
			}

			ThrowIfEmptyOrNull("requester", requester);
			ThrowIfEmptyOrNull("transactionID", transactionID);

			AsynchronousProcessExecutor::Lock apeLock(*apePtr);
			ThrowIfCommandExecuting();

			backgroundProcessData.reset(requester, transactionID);

			try
			{
				string cmdLine = boost::trim_copy(parser.GetTail(0));
				apePtr->RunAsynch(cmdLine, true, true);
			}
			catch(exception& ex)
			{
				backgroundProcessData.reset();

				stringstream msg;
				msg << "Caught " << typeid(ex).name() << " '" << ex.what() << "' while attempting to execute user command";

				Logger::Locked(logger)->Error << msg.str() << endl;
				ThrowCORBAException2<BadParameter>(cmdParam, msg.str());
			}

			throw WillCompleteAsynchronously();
		}
		else
		{
			bool validNotification = true;
			uint8_t notificationType;

			if (boost::iequals(parser[0], "SendAlarmSet"))
				notificationType = 1;
			else if (boost::iequals(parser[0], "SendAlarmCleared"))
				notificationType = 0;
			else if (boost::iequals(parser[0], "SendAlarmEvent"))
				notificationType = 2;
			else if (boost::iequals(parser[0], "SendEvent"))
				notificationType = 3;
			else
				validNotification = false;

			if (validNotification)
			{
				uint16_t senderAddress = 0;
				bool senderAddressSet = false;
				uint32_t reporterTypeID = 0;
				uint32_t notificationID = 0;
				uint32_t enumeratedType = 0;
				string freeFormText;
				bool freeFormTextSpecified = false;
				ByteArray freeFormData;
				const size_t tokenCount = parser.GetTokens().size();
				const string sa = "sender=";
				const string rtid = "reporter=";
				const string nid = "notification=";
				const string et = "enum=";
				const string text = "text=";
				const string data = "data=";

				for (unsigned idx = 1; idx < tokenCount; ++idx)
				{
					const string& tok = parser[idx];

					if (boost::istarts_with(tok, sa))
					{
						try { senderAddress = LexicalCast<uint32_t>(tok.substr(sa.length())); }
						catch(...) { ThrowCORBAException2<BadParameter>(cmdParam, "Invalid assignment for sender address"); }
						senderAddressSet = true;
					}
					else if (boost::istarts_with(tok, rtid))
					{
						try { reporterTypeID = LexicalCast<uint32_t>(tok.substr(rtid.length())); }
						catch(...) { ThrowCORBAException2<BadParameter>(cmdParam, "Invalid assignment for reporter type ID"); }
					}
					else if (boost::istarts_with(tok, nid))
					{
						try { notificationID = LexicalCast<uint32_t>(tok.substr(nid.length())); }
						catch(...) { ThrowCORBAException2<BadParameter>(cmdParam, "Invalid assignment for notification ID"); }
					}
					else if (boost::istarts_with(tok, et))
					{
						try { enumeratedType = LexicalCast<uint32_t>(tok.substr(et.length())); }
						catch(...) { ThrowCORBAException2<BadParameter>(cmdParam, "Invalid assignment for enumerated type"); }
					}
					else if (boost::istarts_with(tok, text))
					{
						try { freeFormText = tok.substr(text.length()); }
						catch(...) { ThrowCORBAException2<BadParameter>(cmdParam, "Invalid assignment for free form text"); }
						freeFormTextSpecified = true;
					}
					else if (boost::istarts_with(tok, data))
					{
						try { freeFormData.FromHexString(tok.substr(data.length())); }
						catch(...) { ThrowCORBAException2<BadParameter>(cmdParam, "Invalid assignment for free form data"); }
					}
					else
					{
						stringstream msg;
						msg << "Unexpected parameter: " << tok;
						ThrowCORBAException2<BadParameter>(cmdParam, msg.str());
					}
				}

				if (!senderAddressSet)
					ThrowCORBAException2<BadParameter>(cmdParam, "Sender address must be set (use \"sender=\")");

				if (!freeFormTextSpecified)
					ThrowCORBAException2<BadParameter>(cmdParam, "Text must be set (use \"text=\")");

				ByteArray level1;

				FormatLevel1(senderAddress, reporterTypeID, notificationType, enumeratedType, notificationID,
						freeFormText, freeFormData, level1);
				ReportAlarm(senderAddress, level1);

				EventPropertyValue epv(broker);
				epv.SetString("OK");

				result.ptr() = new Any;
				epv.CopyOut(*result.ptr());
			}
			else
			{
				//void (CommonSendCommand& sender, const string& command, const string& requester, const string& transactionID, const string& result, bool& handled)>
				//void CommonSendCommand::SendCommand(const ::CORBA::Any& _command, const char* requester, const char* transactionID, ::CORBA::Any_OUT_arg result)
				const string tmpCommand(incomingData->begin(), incomingData->end());
				const string tmpRequester = requester;
				const string tmpTransactionID = transactionID;
				string tmpResult;

				// if not handled, throw an exception explaining that there was a bad command; otherwise,
				// return the result
				if (!HandleAdditionalCommands(tmpCommand, tmpRequester, tmpTransactionID, tmpResult))
				{
					stringstream msg;
					msg << "Unrecognized command";

					Logger::Locked(logger)->Error << msg.str() << endl;

					ThrowCORBAException2<BadParameter>(cmdParam, msg.str());
				}
				else
				{
					EventPropertyValue epv(broker);
					epv.SetString(tmpResult);

					result.ptr() = new Any;
					epv.CopyOut(*result.ptr());
				}
			}
		}
	}
}

void CommonSendCommand::APEExit(AsynchronousProcessExecutor& sender, int resultCode)
{
	try
	{
		Logger::Locked(logger)->Debug << "APEExit with resultCode=0x" << itos(resultCode, 16) << endl;

		SendCommandCompleteEvent evt(config.GetCommandCompleteEventSettings(), broker);
		evt.Complete(true);
		evt.Requester(backgroundProcessData.requester);
		evt.Successful(WIFEXITED(resultCode) && (WEXITSTATUS(resultCode) == 0));
		evt.TransactionID(backgroundProcessData.transactionID);

		BackgroundProcessData::ByteDQ & q = backgroundProcessData.resultingData;
		ByteArray temp;

		temp.resize(q.size());
		copy(q.begin(), q.end(), temp.begin());

		EventPropertyValue epv(broker);
		epv.SetByteArray(temp);

		Any resultingAny;
		epv.CopyOut(resultingAny);

		evt.Result(resultingAny);

		et.TransmitEvent(evt);
	}
	catch(...)
	{
		backgroundProcessData.reset();
	}
}

void CommonSendCommand::APEReport(AsynchronousProcessExecutor& sender, const ByteArray& dataReceived, bool fromStdErr)
{
	Logger::Locked(logger)->Debug << "APEReport with " << dataReceived.size() << " bytes fromStdErr=" << fromStdErr << endl;

	BackgroundProcessData::ByteDQ& q = backgroundProcessData.resultingData;
	size_t numBytes = dataReceived.size();
	size_t newSize = q.size()+numBytes;

	if (q.size() >= config.GetMaxCommandOutput())
		numBytes = 0;
	else if (newSize > config.GetMaxCommandOutput())
		numBytes = config.GetMaxCommandOutput()-q.size();

	if (numBytes)
		q.insert(q.end(), dataReceived.begin(), dataReceived.begin()+numBytes);

	if ((q.size() >= config.GetMaxCommandOutput()) && apePtr && apePtr->ProcessExecuting())
	{
		Logger::Locked(logger)->Error << "Output queue is now " << q.size() << " bytes, triggering overflow condition and killing process" << endl;
		apePtr->Kill();
	}
}

void CommonSendCommand::ThrowIfCommandExecuting()
{
	using namespace LiquidRobotics::SMAC;

	if (!apePtr)
		apePtr.reset(new AsynchronousProcessExecutor());

	else if (apePtr->ProcessExecuting())
	{
		stringstream msg;
		msg << "There is already a command executing: cannot start another command";

		Logger::Locked(logger)->Error << msg.str() << endl;;
		ThrowCORBAException<TooManyConcurrentCommands>(msg.str());
	}
}

void CommonSendCommand::ThrowIfEmptyOrNull(const char* paramName, const char* ptr)
{
	using namespace LiquidRobotics::SMAC;

	if (!ptr || !*ptr)
	{
		stringstream msg;
		msg << "command must be executed in the background, but the caller does not support asynchronous completion";
		Logger::Locked(logger)->Error << msg.str() << endl;
		ThrowCORBAException2<BadParameter>(paramName, msg.str());
	}
}

void CommonSendCommand::DumpPayloadManagerConfig()
{
	stringstream stm;

	try
	{
		PayloadManagerClient::PayloadDescriptionList::Ptr descriptions = pmClient.GetPayloadDescriptions();

		if (!descriptions)
			stm << "No result was returned from GetPayloadDescriptions" << endl;
		else
		{
			stm << "Address  Readable Name" << endl
			    << "-------  -------------" << endl;

			for (PayloadManagerClient::PayloadDescriptionList::const_iterator it = descriptions->begin(); it != descriptions->end(); ++it)
				stm << "0x" << utos((*it)->GetBoardID(), 16, 2) << utos((*it)->GetTaskID(), 16, 2) << "   " << (*it)->GetReadableName() << endl;

			stm << endl
			    << "My board address = 0x" << utos(pmClient.GetMyBoardAddress(), 16, 4) << endl
			    << "Default board ID = 0x" << utos(pmClient.GetDefaultBoardID(), 16, 2) << endl;
		}
	}
	catch(...)
	{
		Logger::Locked(logger)->Error << "DumpPayloadManagerConfig caught exception (1)" << endl;
		stm << "Caught exception while getting payload manager configuration" << endl;
	}

	string result = stm.str();

	Logger::Locked(logger)->Debug << "Output of DumpPayloadManagerConfig:" << endl << result << endl;

	try
	{
		SendCommandCompleteEvent evt(config.GetCommandCompleteEventSettings(), broker);
		evt.Complete(true);
		evt.Requester(backgroundProcessData.requester);
		evt.TransactionID(backgroundProcessData.transactionID);

		EventPropertyValue epv(broker);
		epv.SetString(result);

		Any resultingAny;
		epv.CopyOut(resultingAny);
		evt.Result(resultingAny);

		et.TransmitEvent(evt);
	}
	catch(...)
	{
		Logger::Locked(logger)->Error << "DumpPayloadManagerConfig caught exception (2)" << endl;
	}

	backgroundProcessData.reset();
}

bool CommonSendCommand::HandleAdditionalCommands(const string& command, const string& requester, const string& transactionID, string& result)
{
	return false;
}

}
}



