//-------------------------------------------------------------------------------------------------
// <copyright file="LWController.cpp" company="LiquidRobotics">
//  Copyright (c) Liquid Robotics Corporation, confidential and proprietary.  All rights reserved. 
// </copyright>
//
// <summary>
//	The controller implementation for the LuaWrapper (the methods that theoretically
//	are not reliant on CORBA, though in fact they provide some translations between
//	CORBA and lua.)
// </summary>
//
// <owner>Mike Cookson</owner>
//-------------------------------------------------------------------------------------------------

#include "all.hh"
#include "LqrSMACLogger.h"
#include "LqrSMACGetCmdInfo.h"
#include "LqrSMACIObjectBroker-inline.h"
#include "LqrSMACPayloadDescription.h"
#include "LqrSMACPayloadDescriptionList.h"
#include "LqrSMACUptime.h"
#include "LWController.h"
#include "LWControllerLua.h"
#include "Luna2.h"
#include "LWAsynchScriptContextLua.h"
#include "LWCloseLuaInternalCmd.h"
//#include "LWEventLua.h"
#include "LWForwardAnyEventReceivedInternalCmd.h"
#include "LWForwardLocationInternalCmd.h"
#include "LWForwardSerialDataInternalCmd.h"
#include "LWForwardTimeoutInternalCmd.h"
#include "LWInvokeCmd.h"
#include "LWListCmd.h"
#include "LWLoadLuaCmd.h"
#include "LWOpenLuaInternalCmd.h"
#include "LWSerialPortLua.h"
#include "LWSetTraceOptionsInternalCmd.h"
#include "LWTimerLua.h"
#include "LWUnloadLuaCmd.h"
#include "nsextend.h"
#include "EventSettings.h"
#include "EventTransceiver.h"
#include "PayloadManagerClientSettings.h"
#include "SendCommandCompleteEvent.h"
#include "SensorControllerRunFileCmd.h"
#include "TelemetryEventBase.h"

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

static string GetTimestamp()
{
	stringstream result;

	timeval tv;
	if (gettimeofday(&tv, NULL) == -1)
		tv.tv_sec = tv.tv_usec = 0;

	tm comp;
	localtime_r(&tv.tv_sec, &comp);

	result << ultos(comp.tm_year+1900,10,4) << "/" << ultos(comp.tm_mon+1,10,2) << "/" << ultos(comp.tm_mday,10,2) << " "
	       << ultos(comp.tm_hour,10,2) << ":" << ultos(comp.tm_min,10,2) << ":" << ultos(comp.tm_sec,10,2)
	       << "." << ultos(tv.tv_usec,10,6);

	string returnValue = result.str();
	return returnValue;
}

namespace LiquidRobotics {
namespace LuaWrapper {

LWController::NamedInstance::NamedInstance(const string& _name, LWController* pCon, Logger& logger, IObjectBroker& broker)
: instanceName(_name)
, luac(NULL)
, forwardGPS(false)
, forwardAnyEvents(false)
{
	inst.reset(new LuaInstance);

	// registers the destructor of LWControllerLua with lua,
	// but not the constructor; the object will be accessible
	// as a singleton under the name "SMC"
	Luna2<LWControllerLua>::Register(*inst, false);

	// registering other classes that can be instantiated through SMC
	Luna2<LWAsynchScriptContextLua>::Register(*inst, false);
	//Luna2<LWEventLua>::Register(*inst, false);
	Luna2<LWSerialPortLua>::Register(*inst, false);
	Luna2<LWTimerLua>::Register(*inst, false);

	// create the instance of the controller object in this
	// way so that the controller can be set
	luac = new LWControllerLua(*inst);
	luac->SetController(_name, pCon, &logger, &broker);

	// create the lua variable reference to luac and push
	// it onto the stack (i.e. as a closure), then name it
	// "SMC"
	Luna2<LWControllerLua>::Constructor(*inst, luac);
	inst->setglobal("SMC");
}

LWController::LWController(const LWControllerSettings& _settings, IObjectBroker& _broker, Logger& _logger, GPSSignalSubscriber::Ptr _gpsSubscriber, ShutdownSignalSubscriber::Ptr _shutdownSubscriber, const PayloadManagerClientSettings& _pmc, const EventSettings& _commandCompleteEventSettings)
: LWControllerBase(_broker, _logger, _gpsSubscriber, _shutdownSubscriber)
, settings(_settings)
, broker(_broker)
, lat(0)
, lon(0)
, hasGPS(false)
, pmc(_pmc)
, cmdNum(0)
, commandCompleteEventSettings(_commandCompleteEventSettings)
{
	stringstream stm;
	stm << GetCmdInfo::GetCmdName(false) << "-" << getpid() << "-" << GetSystemUpTimeSeconds();

	procTag = stm.str();
}

LWController::~LWController()
{
}

void LWController::Initialize()
{
	bool shutdownBase = false;

	try
	{
		LWControllerBase::Initialize();
		shutdownBase = true;
		luaQW.reset(new LuaQueueWorker(0));
		lqHandleWorkItemConnection = luaQW->SubscribeWorkItemProcessed(boost::bind(&LWController::OnProcessLuaQueue, this, _1, _2));
		luaQW->Initialize();
	}
	catch(...)
	{
		lqHandleWorkItemConnection.reset();
		luaQW.reset();

		if (shutdownBase)
			try { Base::Shutdown() ; } catch(...) { }

		throw;
	}

}

void LWController::Shutdown()
{
	LuaQueueWorkerPtr temp = luaQW;

	lqHandleWorkItemConnection.reset();
	luaQW.reset();

	if (temp)
	{
		try { temp->Shutdown(); } catch(...) { }
		temp.reset();
	}

	try { Base::Shutdown() ; } catch(...) { }
	throw;
}

void LWController::CloseLua(const string& instanceName)
{
	LuaCommandPtr cmdp;
	cmdp.reset(new LWCloseLuaInternalCmd(*this, "CloseLua", instanceName));

	RunLuaCommand(cmdp);
}

void LWController::CloseLuaInternal(const string& instanceName)
{
	NamedInstancePtr nip;

	{
		MutexLock lock(mutex);
		NamedInstanceMap::iterator iter = instances.find(instanceName);

		if (iter == instances.end())
		{
			stringstream msg;
			msg << "No match for lua instance name " << instanceName;

			throw invalid_argument(msg.str());
		}
		else
		{
			nip = iter->second;
			instances.erase(iter);
		}
	}

	// lua instance will be deleted when the instance is closed (after mutex is unlocked)
}

void LWController::CloseSerialPortConfig(const string& configName)
{
	MutexLock lock(mutex);
	StringSet::iterator it = usedSerialConfigs.find(configName);

	if (it != usedSerialConfigs.end())
		usedSerialConfigs.erase(it);
}

IEventBase::Ptr LWController::CreateTelemetryEvent(const string& eventName, const string& eventTypeName, const string& eventDomainName)
{
	IEventBase::Ptr result;
	EventType etype(eventTypeName, eventDomainName);
	result.reset(new TelemetryEventBase(eventName, etype, broker));
	return result;
}

void LWController::ForwardSerialData(const string& instanceName, const string& portInstanceName, const ByteArray& data)
{
	LuaCommandPtr cmdp;
	cmdp.reset(new LWForwardSerialDataInternalCmd(*this, "ForwardSerial", instanceName, portInstanceName, data));

	RunLuaCommand(cmdp, false);
}

//void LWController::ForwardSerialDataImmediate(const string& instanceName, const string& portInstanceName, const ByteArray& data)
//{
//	// TODO
//}

void LWController::ForwardTimeout(const string& instanceName, const string& timerName)
{
	LuaCommandPtr cmdp;
	cmdp.reset(new LWForwardTimeoutInternalCmd(*this, "ForwardTimeout", instanceName, timerName));

	RunLuaCommand(cmdp, false);
}

bool LWController::GetLocation(double& latVar, double& lonVar) const
{
	if (hasGPS)
	{
		latVar = lat;
		lonVar = lon;
		return true;
	}
	else
	{
		latVar = lonVar = 0;
		return false;
	}
}

LWController::LuaInstancePtr LWController::GetLuaInstance(const string& instanceName)
{
	MutexLock lock(mutex);
	NamedInstanceMap::iterator iter = instances.find(instanceName);

	if (iter == instances.end())
	{
		stringstream msg;
		msg << "No match for lua instance name " << instanceName;

		throw invalid_argument(msg.str());
	}

	return iter->second->inst;
}

void LWController::GetLuaInstanceReport(StringArray& result)
{
	MutexLock lock(mutex);
	const size_t numInstances = instances.size();
	size_t idx = 0;

	result.resize(numInstances);

	for (NamedInstanceMap::const_iterator ci = instances.begin(); ci != instances.end(); ++ci)
	{
		stringstream temp;
		const string& name = ci->second->instanceName;
		const string& fname = ci->second->fileName;
		temp << name;

		if (fname.length())
		{
			if (name.length() < 9) temp << string(9-name.length(), ' ');
			temp << " " << fname;
		}

		result[idx++] = temp.str();
	}
}

void LWController::OnGPSSampleEvent(const GPSSampleEvent& sample)
{
	if (sample.HasGPS())
	{
		sample.GetGPS(lat, lon);
		hasGPS = true;

		// getting the list of instance names for which GPS forwarding is enabled
		typedef deque<string> StringQueue;

		StringQueue names;

		{
			MutexLock lock(mutex);

			for (NamedInstanceMap::const_iterator ci = instances.begin(); ci != instances.end(); ++ci)
				if (ci->second->forwardGPS)
					names.push_back(ci->first);
		}

		// creating a callback for each instance name
		while(!names.empty())
		{
			LuaCommandPtr cmdp;
			string instance = names.front();

			names.pop_front();
			cmdp.reset(new LWForwardLocationInternalCmd(*this, "GpsSample", instance, lat, lon));

			RunLuaCommand(cmdp, false);
		}
	}
}

void LWController::OnProcessLuaQueue(LuaQueueWorker& sender, const LuaCommandPtr& item)
{
	__Msg __msg(logger);
	__msg.head << "OnProcessLuaQueue " << typeid(*item).name();
	__msg.PrintFront();

	bool caughtException = false;
	string result;

	try
	{
		item->Execute();
		item->GetResult();
	}
	catch(exception& ex)
	{
		__msg.tail << "Caught " << typeid(ex).name() << " (" << ex.what() << ")";
		result = __msg.tail.str();
		caughtException = true;
	}
	catch(...)
	{
		__msg.tail << "Caught unknown exception";
		result = __msg.tail.str();
		caughtException = true;
	}

	if (!caughtException)
		__msg.tail << "Completed without exception";

	item->SignalComplete();
}

void LWController::OnShutdownEvent()
{
	Base::OnShutdownEvent();
}

void LWController::OpenLua(const string& instanceName, const string& luaFileName, int traceOpt)
{
	LuaCommandPtr cmdp;
	cmdp.reset(new LWOpenLuaInternalCmd(*this, "OpenLua", instanceName, luaFileName, traceOpt));

	RunLuaCommand(cmdp);
}

void LWController::OpenLuaInternal(const string& instanceName, const string& luaFileName, int traceOpt)
{
	MutexLock lock(mutex);

	if (instances.count(instanceName))
	{
		stringstream msg;
		msg << "There is already an instance called '" << instanceName << "'";
		throw invalid_argument(msg.str());
	}

	NamedInstancePtr nip;

	nip.reset(new NamedInstance(instanceName, this, logger, broker));
	nip->fileName = luaFileName;
	instances[instanceName] = nip;

	try
	{
		//nip->inst->L_dofile(luaFileName,true);
		nip->inst->L_loadfile(luaFileName, true);
		SetTraceOptionsInternal(instanceName, traceOpt);
		nip->inst->pcall(0, LUA_MULTRET, 0, true);
		nip->forwardGPS = LWForwardLocationInternalCmd::InstanceWantsGPSForwarding(*nip->inst);
		nip->forwardAnyEvents = LWForwardAnyEventReceivedInternalCmd::InstanceWantsForwarding(*nip->inst);
	}
	catch(...)
	{
		NamedInstanceMap::iterator iter = instances.find(instanceName);

		if (iter != instances.end())
			instances.erase(iter);

		throw;
	}

	Logger::Locked(logger)->Debug << "Named instance \"" << nip->instanceName << "\""
			<< " fileName=\"" << nip->fileName << "\""
			<< " forwardGPS=" << nip->forwardGPS
			<< " forwardAnyEvents=" << nip->forwardAnyEvents
			<< endl;
}

void LWController::OpenSerialPortConfig(const string& configName, ISerialPort::Ptr& resultingPort, SerialListener::Ptr& resultingListener)
{
	MutexLock lock(mutex);

	resultingPort.reset();
	resultingListener.reset();

	if (!settings.HasSerialConfig(configName))
	{
		stringstream msg;
		msg << "No serial configuration named '" << configName << "' has been defined";
		throw invalid_argument(msg.str());
	}

	StringSet::iterator usedConfigIt = usedSerialConfigs.find(configName);

	if (usedConfigIt != usedSerialConfigs.end())
	{
		stringstream msg;
		msg << "Serial configuration named '" << configName << "' is already in use";
		throw invalid_argument(msg.str());
	}

	const LWControllerSettings::SerialConfig& cfg = settings.GetSerialConfig(configName);

	try
	{
		resultingPort = cfg.portSettings.CreatePort(logger);
		resultingListener.reset(new SerialListener(*resultingPort, cfg.listenerSettings));
		resultingListener->Initialize();
		usedSerialConfigs.insert(configName);
	}
	catch(...)
	{
		resultingListener.reset();
		resultingPort.reset();
		throw;
	}
}

void LWController::PublishDataSet(const string& instanceName, const string& eventName, const string& eventType, const string& eventDomain,
		const string& dataSetName, const string& content)
{
	publishDataSetSignal(*this, instanceName, eventName, eventType, eventDomain, dataSetName, content);
}

LWController::CommandPtr LWController::ParseCommand(ParsingContext& context, bool internalCmd)
{
	CmdLineParser parser(context.GetCmd());
	CommandPtr result;
	static const string LWM = "LWM";
	static const string LWScript = "LWSCRIPT";
	static const string InvokeOpt = "--invoke";
	static const string ListOpt = "--list";
	static const string LoadOpt = "--loadlua";
	static const string RunOpt = "--run";
	static const string UnloadOpt = "--unloadlua";

	if (!!(result = EventLib::SensorControllerRunFileCmd<TCon,TCmd,TCID,TEx>::Parse(LWM, RunOpt, *this, parser, context, logger))
	 || !!(result = LWInvokeCmd::Parse(LWScript, InvokeOpt, *this, parser, context, logger))
	 || !!(result = LWListCmd::Parse(LWM, ListOpt, *this, parser, context, logger))
	 || !!(result = LWLoadLuaCmd::Parse(LWM, LoadOpt, *this, parser, context, logger))
	 || !!(result = LWUnloadLuaCmd::Parse(LWM, UnloadOpt, *this, parser, context, logger)) )

		;

	else
	{
		stringstream msg;
		msg << "Unrecognized command '" << context.GetCmd() << "'";

		throw invalid_argument(msg.str());
	}

	return result;
}

string LWController::RunLuaCommand(CommandPtr cmdp, bool awaitResult)
{
	if (!luaQW)
		throw EventLib::SensorControllerExceptions::NotReady("The lua queue worker was not instantiated");

	luaQW->EnqueueWorkItem(cmdp);

	string result;

	if (awaitResult)
	{
		cmdp->ActionWait(result);
		result = cmdp->GetResult();
	}

	return result;
}

LWController::ScopedConnectionPtr LWController::SubscribePublishDataSet(PublishDataSetSignalType::slot_type slot)
{
	ScopedConnectionPtr result;
	result.reset(new ScopedConnection(publishDataSetSignal.connect(slot)));
	return result;
}

LWController::ScopedConnectionPtr LWController::SubscribePublishEvent(PublishEventSignalType::slot_type slot)
{
	ScopedConnectionPtr result;
	result.reset(new ScopedConnection(publishEventSignal.connect(slot)));
	return result;
}

void LWController::SetTraceOptions(const string& instanceName, int traceOpt)
{
	LuaCommandPtr cmdp;
	cmdp.reset(new LWSetTraceOptionsInternalCmd(*this, "SetTraceOptions", instanceName, traceOpt));

	RunLuaCommand(cmdp);
}

void LWController::SetTraceOptionsInternal(const string& instanceName, int traceOpt)
{
	LuaInstancePtr inst = GetLuaInstance(instanceName);

	if (inst)
		inst->sethook(&LWController::HookFunc, traceOpt);
}

void LWController::HookFunc(lua_State *L, lua_Debug *ar)
{
	stringstream ss;
	bool good = true;

	switch(ar->event)
	{
	case LUA_HOOKCALL:	ss << "CALL";		break;
	case LUA_HOOKRET:	ss << "RETN";		break;
	case LUA_HOOKCOUNT: ss << "CNTR";		break;
	case LUA_HOOKLINE:	ss << "LINE";		break;
#ifdef LUA_HOOKTAILRET
	case LUA_HOOKTAILRET: ss << "TRET";		break;
#endif
#ifdef LUA_HOOKTAILCALL
	case LUA_HOOKTAILCALL: ss << "TCAL";		break;
#endif
	default:			good = false;		break;
	}

	if (good)
	{
		ss << " ";
		LuaInstance inst(L);
		inst.getinfo("nS", *ar);
		if (ar->what && !strcmp(ar->what, "C"))
			ss << "C Function";
		else
		{
			if (ar->what && *ar->what)
				ss << ar->what << " ";
			ss << ar->short_src << ":" << ar->currentline;
		}
		if (ar->namewhat && *ar->namewhat)
			ss << " "<< ar->namewhat;
		if (ar->name && *ar->name)
			ss << " " << ar->name;
		cout << GetTimestamp() << " >> " << ss.str() << endl;
	}
}

void LWController::TransmitEvent(IEventBase& evt)
{
	publishEventSignal(*this, evt);
}

void LWController::GetServerDetails(unsigned boardAddress, string& serverName, string& serverTypeName)
{
	using namespace LiquidRobotics::SMAC;
	typedef LiquidRobotics::SMACFramework::PayloadDescriptionList PyldDescList;

	IPayloadManager_var pm;

	try
	{
		broker.GetNamedInstance<IPayloadManager_var, IPayloadManager>(pmc.GetServiceName().c_str(), pmc.GetServiceKind().c_str());
	}
	catch(...)
	{
		stringstream msg;
		msg << "Payload manager service (" << pmc.GetServiceName() << ") could not be accessed, so address lookups cannot be performed";
		string s = msg.str();
		throw runtime_error(s);
	}

	PayloadDescriptionList_var corbaList;
	PyldDescList::Ptr pyldDescList;
	string result;
	bool found = false;

	try
	{
		corbaList = pm->payloadDescriptions();

		pyldDescList.reset(new PyldDescList(corbaList.in()));
	}
	catch(...)
	{
		throw runtime_error("Error traversing payload service list");
	}


	for (PyldDescList::const_iterator ci = pyldDescList->begin(); !found && (ci != pyldDescList->end()); ++ci)
	{
		unsigned candidateAddress = (*ci)->GetBoardID();

		candidateAddress <<= 8;
		candidateAddress += (*ci)->GetTaskID();

		if (candidateAddress == boardAddress)
		{
			serverName = (*ci)->GetReadableName();
			serverTypeName.clear();
			found = true;
		}
	}

	if (!found)
	{
		stringstream msg;
		msg << "No match for board address " << boardAddress << " (0x" << utos(boardAddress, 16, 4) << ")";
		string m = msg.str();
		throw runtime_error(m);
	}
}

bool LWController::InvokeSendCommand(const string& serverName, const string& serverType, EventPropertyValue& epvCommand,
		EventPropertyValue& epvResponse, string& asynchronousResponseTag, bool asynch)
{
	using namespace LiquidRobotics::SMAC;

	MutexLock lock(mutex);
	bool result = true;
	stringstream tagstm;

	tagstm << procTag << "." << ++cmdNum;

	WaitingCommandPtr waitingCommand;

	waitingCommand.reset(new WaitingCommand(broker));
	waitingCommand->tag = tagstm.str();

	tagToWaitingCommandMap[waitingCommand->tag] = waitingCommand;
	lock.unlock();

	try
	{
		ICommandable_var commandableV;
		commandableV = broker.GetNamedInstance<ICommandable_var,ICommandable>(serverName.c_str(), serverType.c_str());

		CORBA::Any cmd;
		epvCommand.CopyOut(cmd);

		try
		{
			ICommandableInvoker<ICommandable_var> invoker(commandableV);
			invoker(cmd, procTag.c_str(), waitingCommand->tag.c_str(), waitingCommand->result);
		}
		catch(WillCompleteAsynchronously& exc)
		{
			if (asynch)
			{
				asynchronousResponseTag = waitingCommand->tag;
				result = false;
			}
			else
				waitingCommand->sem.Wait();
		}
	}
	catch(...)
	{
		lock.lock();
		tagToWaitingCommandMap.erase(waitingCommand->tag);
		throw;
	}

	if (result)
		epvResponse = EventPropertyValue(waitingCommand->result.in(), broker);
	else
		epvResponse = EventPropertyValue(broker);

	lock.lock();
	tagToWaitingCommandMap.erase(waitingCommand->tag);
	return result;
}

void LWController::ForwardAnyEventReceived(const IEventBase& receivedEvent)
{
	// if this is a "command complete" event, unblock any waiting threads
	if ((receivedEvent.GetEventName() == commandCompleteEventSettings.GetName())
	 && (typeid(receivedEvent) == typeid(SendCommandCompleteEvent)))
	{
		const SendCommandCompleteEvent& scce =
			EventTransceiver::UpcastEventRef<SendCommandCompleteEvent>(receivedEvent,
					commandCompleteEventSettings, "LWController::ForwardAnyEventReceived");

		if (scce.Requester() == procTag)
		{
			MutexLock lock(mutex);
			TagToWaitingCommandMap::iterator it = tagToWaitingCommandMap.find(scce.TransactionID());

			if (it != tagToWaitingCommandMap.end())
			{
				if (scce.HasResult())
					it->second->result = scce.Result();

				it->second->sem.Post();
				tagToWaitingCommandMap.erase(it);
			}
		}
	}

	// getting the list of instance names for which GPS forwarding is enabled
	typedef deque<string> StringQueue;

	StringQueue names;

	{
		MutexLock lock(mutex);

		for (NamedInstanceMap::const_iterator ci = instances.begin(); ci != instances.end(); ++ci)
			if (ci->second->forwardAnyEvents)
				names.push_back(ci->first);
	}

	// creating a callback for each instance name
	IEventBase::Ptr datap;
	while(!names.empty())
	{
		LuaCommandPtr cmdp;
		string instance = names.front();

		names.pop_front();

		if (!datap)
			datap = receivedEvent.Duplicate();

		cmdp.reset(new LWForwardAnyEventReceivedInternalCmd(*this, "ForwardSerial", instance, datap, broker));
		RunLuaCommand(cmdp, false);
	}
}

string LWController::GetDefaultOutputFormatter() const
{
	return settings.GetDefaultFormatter();
}

}
}
