//-------------------------------------------------------------------------------------------------
// <copyright file="LWControllerLua.cpp" company="LiquidRobotics">
//  Copyright (c) Liquid Robotics Corporation, confidential and proprietary.  All rights reserved. 
// </copyright>
//
// <summary>
//	Implementation of LWControllerLua, an implementation of metatable methods exposed
//	to lua as the global "SMC" variable.  It represents the services provided to
//	the script by the lua wrapper.
// </summary>
//
// <owner>Mike Cookson</owner>
//-------------------------------------------------------------------------------------------------

#include "LWControllerLua.h"
#include "LWConcreteCmd.h"
#include "LWEventLua.h"
#include "LWLocationTable.h"
#include "LWSerialPortLua.h"
#include "LWTimerLua.h"
#include "LqrSMACLogger.h"
#include "LqrSMACUptime.h"
#include "LqrSMACLuaEventConversion.h"
#include "LqrSMACIObjectBroker.h"
#include "TelemetryEventBase.h"
#include <sstream>

using namespace std;
using namespace LiquidRobotics::SMACFramework;

inline void LogAndThrow(stringstream& s, Logger* pLogger)
{
	string m = s.str();
	if (pLogger) Logger::Locked(*pLogger)->Error << m << endl;
	throw runtime_error(m);
}

inline string tostring(LuaInstance& L, int stackOffset)
{
	LuaInstance::StringPtr reading = L.tolstring(stackOffset);
	if (!reading)
		throw runtime_error("failed to read string at specified offset");
	return *reading;
}

template<typename T> void ValidateArrayTable(LuaInstance& L, int stackOffset)
{
	LuaInstance::StackStateHolder holder(L);
	T t(L);

	if (!L.istable(stackOffset))
		throw runtime_error("not a table");

	L.pushnil();

	if (L.next(stackOffset))
	{
		t.ValidateEntry(-1);

		const int numElems = L.objlen(stackOffset);

		do
		{
			if (!L.isnumber(-2))
				throw runtime_error("table indices may only be numeric");

			LuaInstance::Number idxNum = L.tonumber(-2);

			if ((idxNum < 1) || (idxNum > numElems))
			{
				stringstream msg;
				msg << "array index " << idxNum << " is out of range for a table of " << numElems << " elements";
				string m = msg.str();
				throw runtime_error(m);
			}

			t.ValidateEntry(-1);
			L.pop(2);

		} while(L.next(stackOffset) != 0);
	}
}

struct VATStringTable
{
	VATStringTable(LuaInstance& _L)
	: L(_L)
	{ }

	void ValidateEntry(int stackIndex)
	{
		if (L.type(stackIndex) != LUA_TSTRING)
			throw runtime_error("table may contain only strings");
	}

	LuaInstance& L;
};

namespace LiquidRobotics {
namespace LuaWrapper {

const Luna2<LWControllerLua>::RegType LWControllerLua::registrations[] =
{
	{ "BAnd",				&LWControllerLua::BAnd },
	{ "BOr",				&LWControllerLua::BOr },
	{ "BXor",				&LWControllerLua::BXor },
	{ "GetLocation",		&LWControllerLua::GetLocation },
	{ "Log", 				&LWControllerLua::Log },
	{ "NewSerialPort", 		&LWControllerLua::NewSerialPort },
	{ "NewTimer",			&LWControllerLua::NewTimer },
	{ "PublishDataSet",		&LWControllerLua::PublishDataSet },
	{ "PublishEvent",		&LWControllerLua::PublishEvent },
	{ "SendCommand",		&LWControllerLua::SendCommand },
	{ "SubscribeEvents",	&LWControllerLua::SubscribeEvents },
	{ "SystemUptime",		&LWControllerLua::SystemUptime },
	//{ "Test1",				&LWControllerLua::Test1 },
};

const size_t LWControllerLua::registrationCount = sizeof(registrations)/sizeof(registrations[0]);

LWControllerLua::LWControllerLua(LuaInstance& _L)
: pController(NULL)
, pLogger(NULL)
{
}

LWControllerLua::~LWControllerLua()
{
	if (tcol)
	{
		dynamic_cast<TimerCollection&>(*tcol).Shutdown();
		tcol.reset();
	}
}

string LWControllerLua::GetClassName()
{
	return "SMC";
}

int LWControllerLua::GetRegistrations(const Luna2<LWControllerLua>::RegType*& regBase)
{
	regBase = registrations;
	return registrationCount;
}

void LWControllerLua::PreRegistration(LuaInstance& L)
{
}

int LWControllerLua::GetLocation(LuaInstance& L)
{
	double latitude, longitude;
	const bool valid = pController->GetLocation(latitude, longitude);

	PushLocationTable(L, valid, latitude, longitude);
	return 1;
}

int LWControllerLua::Log(LuaInstance& L)
{
	try
	{
		if (!tcol)
			throw runtime_error("timer collection was not initialized");

		else if (L.gettop() < 2)
			throw runtime_error("not enough parameters");

		string logLevelStr = tostring(L, -2);
		string dataStr = tostring(L, -1);
		Logger::LogStream* pStm = NULL;

		if (boost::iequals(logLevelStr, "emergency"))
			pStm = &pLogger->Emergency;
		else if (boost::iequals(logLevelStr, "alert"))
			pStm = &pLogger->Alert;
		else if (boost::iequals(logLevelStr, "critical"))
			pStm = &pLogger->Critical;
		else if (boost::iequals(logLevelStr, "error"))
			pStm = &pLogger->Error;
		else if (boost::iequals(logLevelStr, "warning"))
			pStm = &pLogger->Warning;
		else if (boost::iequals(logLevelStr, "notice"))
			pStm = &pLogger->Notice;
		else if (boost::iequals(logLevelStr, "info"))
			pStm = &pLogger->Info;
		else if (boost::iequals(logLevelStr, "debug"))
			pStm = &pLogger->Debug;
		else
		{
			stringstream msg;
			msg << "invalid log level '" << logLevelStr << "' was specified";
			string m = msg.str();
			throw runtime_error(m);
		}

		if (pLogger && pStm)
		{
			Logger::Lock logLock(*pLogger);
			*pStm << "[" << instanceName << "] " << dataStr << endl;
		}
	}
	catch(exception& ex)
	{
		stringstream msg;
		msg << "Debug(" << instanceName << ") caught " << ex.what();
		string m = msg.str();
		throw runtime_error(m);
	}

	return 0;
}

int LWControllerLua::NewSerialPort(LuaInstance& L)
{
	LWSerialPortLua* luasp = NULL;
	int result = 0;

	try
	{
		if (L.gettop() >= 1)
		{
			LuaInstance::StringPtr configName = L.tolstring(-1);

			if (configName)
			{
				if (pLogger)
					Logger::Locked(*pLogger)->Debug << "NewSerialPort (" << instanceName << ") configName=" << *configName << endl;

				luasp = new LWSerialPortLua(L);
				luasp->Initialize(pController, *configName, instanceName, *configName);

				Luna2<LWSerialPortLua>::Constructor(L, luasp);
				luasp = NULL;
				result = 1;
			}
			else
			{
				stringstream msg;
				msg << "NewSerialPort (" << instanceName << "): configName is not a string";
				LogAndThrow(msg, pLogger);
			}
		}
		else
		{
			stringstream msg;
			msg << "NewSerialPort (" << instanceName << "): not enough parameters";
			LogAndThrow(msg, pLogger);
		}
	}
	catch(std::exception& ex)
	{
		if (luasp)
			delete luasp;

		throw;
	}
	catch(...)
	{
		if (luasp)
			delete luasp;

		throw;
	}

	return result;
}

int LWControllerLua::NewTimer(LuaInstance& L)
{
	LWTimerLua* luatp = NULL;
	int result = 0;

	try
	{
		if (tcol && (L.gettop() >= 2))
		{
			LuaInstance::StringPtr timerName = L.tolstring(-2);
			int numMS = L.tointeger(-1);

			if (timerName)
			{
				if (numMS >= 1)
				{
					if (pLogger)
						Logger::Locked(*pLogger)->Debug << "NewTimer (" << instanceName << ") timerName=" << *timerName << " numMS=" << numMS << endl;

					luatp = new LWTimerLua(L);
					luatp->Initialize(pController, tcol, instanceName, *timerName, (unsigned) numMS);

					Luna2<LWTimerLua>::Constructor(L, luatp);
					luatp = NULL;
					result = 1;
				}
				else
				{
					stringstream msg;
					msg << "NewTimer (" << instanceName << "): numMS (" << numMS << ") must be a positive value";
					LogAndThrow(msg, pLogger);
				}
			}
			else
			{
				stringstream msg;
				msg << "NewTimer (" << instanceName << "): timerName is not a string";
				LogAndThrow(msg, pLogger);
			}
		}
		else
		{
			stringstream msg;
			msg << "NewTimer (" << instanceName << "): no timer collection or not enough parameters";
			LogAndThrow(msg, pLogger);
		}
	}
	catch(...)
	{
		if (luatp)
			delete luatp;

		throw;
	}

	return result;
}

int LWControllerLua::PublishDataSet(LuaInstance& L)
{
	if (tcol && (L.gettop() >= 4))
	{
		LuaInstance::StringPtr eventName = L.tolstring(-5);
		LuaInstance::StringPtr eventType = L.tolstring(-4);
		LuaInstance::StringPtr eventDomain = L.tolstring(-3);
		LuaInstance::StringPtr dataSetName = L.tolstring(-2);
		LuaInstance::StringPtr content = L.tolstring(-1);

		if (eventName && eventType && eventDomain && dataSetName && content)
		{
			if (pLogger)
				Logger::Locked(*pLogger)->Debug << "PublishDataSet (" << instanceName << ")"
															 << " eventName=" << *eventName
															 << " eventType=" << *eventType
															 << " eventDomain=" << *eventDomain
															 << " dataSetName=" << *dataSetName
															 << " content=" << content->length() << " bytes" << endl;

			pController->PublishDataSet(instanceName, *eventName, *eventType, *eventDomain, *dataSetName, *content);
		}
		else
		{
			stringstream msg;
			msg << "PublishDataSet (" << instanceName << "): one or more parameters are not strings";
			LogAndThrow(msg, pLogger);
		}
	}
	else
	{
		stringstream msg;
		msg << "PublishDataSet (" << instanceName << "): no timer collection or not enough parameters";
		LogAndThrow(msg, pLogger);
	}

	return 0;
}

int LWControllerLua::SystemUptime(LuaInstance& L)
{
	L.pushnumber(GetSystemUpTimeSeconds());
	return 1;
}

// SendCommand(serverSpec,var,[synchronous=true]):{asynch,result or txn}
int LWControllerLua::SendCommand(LuaInstance& L)
{
	const int top = L.gettop();
	LuaEventConversion lec(L, *pBroker);
	LuaEventConversion::PVTPtr response;
	bool completed = true;

	response.reset(new LuaEventConversion::PVT(*pBroker));

	/*cout << "top=" << top;

	for (int i = 1; i <= top; ++i)
		cout << " " << i << ":" << L._typename(L.type(i));

	cout << endl;*/

	if (top >= 2)
	{
		const int offset = -3;
		string ptype;
		string serverName;
		string serverType;
		bool numeric = false;
		uint16_t boardAddress = 0;
		LuaEventConversion::PVTPtr content;
		bool synchronous = false;

		ptype = L._typename(L.type(offset));

		if (L.isnumber(offset))
		{
			boardAddress = (uint16_t) floor(L.tonumber(offset));
			numeric = true;
		}
		else if (L.isstring(offset))
			serverName = *L.tostring(offset);
		else if (L.istable(offset))
		{
			L.pushnumber(1);
			L.gettable(offset-1);

			ptype = L._typename(L.type(-1));

			if (L.isnumber(-1))
			{
				boardAddress = (uint16_t) floor(L.tonumber(-1));
				numeric = true;
				L.pop(1);
			}
			else if (L.isstring(-1))
			{
				serverName = *L.tostring(-1);
				L.pop(1);

				L.pushnumber(2);
				L.gettable(offset-1);

				if (L.isstring(-1))
					serverType = *L.tostring(-1);

				L.pop(1);
			}
			else
			{
				L.pop(1);
				stringstream msg;
				msg << "SendCommand (" << instanceName << "): server parameter table type (" << ptype << ") not supported";
				LogAndThrow(msg, pLogger);
			}
		}
		else
		{
			stringstream msg;
			msg << "SendCommand (" << instanceName << "): server parameter type (" << ptype << ") not supported";
			LogAndThrow(msg, pLogger);
		}

		content = lec.GetPropertyValue(offset+1);

		if (!content)
		{
			stringstream msg;
			msg << "SendCommand (" << instanceName << "): failed to get content value";
			LogAndThrow(msg, pLogger);
		}

		ptype = L._typename(L.type(offset+2));

		if (L.isboolean(offset+2))
			synchronous = L.toboolean(offset+2);
		else
		{
			stringstream msg;
			msg << "SendCommand (" << instanceName << "): content type (" << ptype << ") not supported";
			LogAndThrow(msg, pLogger);
		}

		if (numeric)
			pController->GetServerDetails(boardAddress, serverName, serverType);

		string asynchTag;

		completed = pController->InvokeSendCommand(serverName, serverType, *content, *response, asynchTag, !synchronous);

		if (!completed)
			response->SetString(asynchTag);
	}
	else
	{
		stringstream msg;
		msg << "SendCommand (" << instanceName << "): no timer collection or not enough parameters";
		LogAndThrow(msg, pLogger);
	}

	L.newtable();
	L.pushnumber(1);
	L.pushboolean(!completed);
	L.settable(-3);
	L.pushnumber(2);
	lec.PushPropertyValue(*response);
	L.settable(-3);
	return 1;
}

void LWControllerLua::SetController(const string& _instanceName, LWControllerBase0* _pController, Logger* _pLogger, IObjectBroker* _pBroker)
{
	instanceName = _instanceName;
	pController = _pController;
	pLogger = _pLogger;
	pBroker = _pBroker;

	tcol.reset(new TimerCollection);

	try
	{
		dynamic_cast<TimerCollection&>(*tcol).Initialize();
	}
	catch(...)
	{
		tcol.reset();
		throw;
	}
}

// PublishEvent({ header={ name=eventName, typename=typeName, domainname=domainName }
//               properties={ propName={propvalue,coercedtype}, propName=.... } })
int LWControllerLua::PublishEvent(LuaInstance& L)
{
	using namespace LiquidRobotics::EventLib;

	if (!pController)
	{
		stringstream msg;
		msg << "CreateEvent(" << instanceName << ") controller has not been initialized";
		LogAndThrow(msg, pLogger);
	}

	LuaEventConversion lec(L, *pBroker);
	IEventBase::Ptr evtp = lec.GetEvent(-1);

	if (evtp)
		pController->TransmitEvent(*evtp);

	return 0;
}

int LWControllerLua::SubscribeEvents(LuaInstance& L)	// SubscribeEvents({..eventName..} or {} or nil)
{
	return 0;
}

int LWControllerLua::BAnd(LuaInstance& L)
{
	if (!L.isnumber(-1) || !L.isnumber(-2))
		throw runtime_error("BAnd: parameters may only be numeric");

	unsigned long p1 = (unsigned long) floor(L.tonumber(-1));
	unsigned long p2 = (unsigned long) floor(L.tonumber(-2));
	LuaInstance::Number result = (LuaInstance::Number) (p1 & p2);
	L.pushnumber(result);
	return 1;
}

int LWControllerLua::BOr(LuaInstance& L)
{
	if (!L.isnumber(-1) || !L.isnumber(-2))
		throw runtime_error("BOr: parameters may only be numeric");

	unsigned long p1 = (unsigned long) floor(L.tonumber(-1));
	unsigned long p2 = (unsigned long) floor(L.tonumber(-2));
	LuaInstance::Number result = (LuaInstance::Number) (p1 | p2);
	L.pushnumber(result);
	return 1;
}

int LWControllerLua::BXor(LuaInstance& L)
{
	if (!L.isnumber(-1) || !L.isnumber(-2))
		throw runtime_error("BXor: parameters may only be numeric");

	unsigned long p1 = (unsigned long) floor(L.tonumber(-1));
	unsigned long p2 = (unsigned long) floor(L.tonumber(-2));
	LuaInstance::Number result = (LuaInstance::Number) (p1 ^ p2);
	L.pushnumber(result);
	return 1;
}

}
}
