//-------------------------------------------------------------------------------------------------
// <copyright file="main.cpp" company="LiquidRobotics">
//  Copyright (c) Liquid Robotics Corporation, confidential and proprietary.  All rights reserved. 
// </copyright>
//
// <summary>
//   Implementation of the main() function for OneTimeCommand
// </summary>
//
// <owner>Mike Cookson</owner>
//-------------------------------------------------------------------------------------------------

#include "OneTimeCommandController.h"
#include "OneTimeCommandSettings.h"
#include "CRC16.h"
#include "LqrSMACStdOutLogger.h"
#include "PayloadMessage.h"
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <fstream>
#include <iostream>
#include <boost/algorithm/string.hpp>
#include <boost/thread.hpp>

using namespace std;
using namespace LiquidRobotics::OneTimeCommand;
using namespace LiquidRobotics::PayloadInterface;
using namespace LiquidRobotics::SMACFramework;

template<typename T>
void ConvertIntegerType(const string& input, T& result)
{
	int base;
	string tail;

	if (boost::istarts_with(input, "0x"))
	{
		tail = input.substr(2);
		base = 16;
	}
	else
	{
		tail = input;
		base = 10;
	}

	const T zero = 0;
	const T notZero = (T) ~zero;
	const bool isUnsigned = zero < notZero;
	char* endptr = NULL;

	if (isUnsigned)
		result = (T) strtoul(tail.c_str(), &endptr, base);
	else
		result = (T) strtol(tail.c_str(), &endptr, base);

	if (!endptr || *endptr)
	{
		stringstream stm;
		stm << "Failed to convert " << input << " to " << (isUnsigned ? "unsigned" : "signed") << " integer (base=" << base << ")";
		throw invalid_argument(stm.str());
	}
}

bool DifferentTag(const string& tag)
{
	stringstream filePathStm;
	const char* homePtr = getenv("HOME");

	if (homePtr)
		filePathStm << homePtr << "/";

	filePathStm << ".OneTimeCommandTag";

	const string filePath = filePathStm.str();
	ifstream ifs(filePath.c_str(), ifstream::in | ifstream::binary);

	if (ifs.good())
	{
		ifs.seekg(0, ios::end);
		const int flen = ifs.tellg();
		ifs.seekg(0, ios::beg);
		string buf(flen, ' ');
		ifs.read(&buf.at(0), flen);
		ifs.close();
		if (!tag.compare(buf))
			return false;
	}

	ofstream ofs(filePath.c_str(), ofstream::out | ofstream::binary | ofstream::trunc);

	if (ofs.good())
	{
		ofs.write(tag.data(), tag.length());
		ofs.close();
	}

	return true;
}

void ProcessArgs(int argc, char* argv[], PayloadMessage::BoardAddress& address, ByteArray& message, PayloadMessage::BoardAddress& destination, string& portName, string& tag, bool& tagSet)
{
	static option opts[] =
	{
			{ "address",		required_argument,		0, 'a' },
			{ "crc",			optional_argument,		0, 'c' },
			{ "dest",			required_argument,		0, 'D' },
			{ "msg",			required_argument,		0, 'm' },
			{ "port",			required_argument,		0, 'p' },
			{ "tag",			required_argument,		0, 't' },
			{ 0, 				0, 						0, 0 }
	};

	string optStr;

	for (unsigned idx = 0; opts[idx].name != NULL; ++idx)
		if (opts[idx].val)
		{
			optStr.push_back((char) opts[idx].val);

			if (opts[idx].has_arg != no_argument)
			{
				optStr.push_back(':');

				if (opts[idx].has_arg == optional_argument)
					optStr.push_back(':');
			}
		}

	bool processing = true;
	bool gotMessage = false;
	bool addCRC = false;
	unsigned crcStart = 0;

	do
	{
		int argIdx = 0;
		int item = getopt_long(argc, argv, optStr.c_str(), opts, &argIdx);

		processing = (item != -1);

		if (processing)
		{
			switch(item)
			{
			case 'c':
				{
					if (optarg)
					{
						try
						{
							ConvertIntegerType(optarg, crcStart);
						}
						catch(invalid_argument& ex)
						{
							stringstream msgstr;
							msgstr << "Failed to convert CRC start argument: " << ex.what();

							string msg = msgstr.str();
							throw runtime_error(msg);
						}
					}
					else
						crcStart = 0;

					addCRC = true;
					break;
				}

			case 'D':
				{
					try
					{
						ConvertIntegerType(optarg, destination);
					}
					catch(invalid_argument& ex)
					{
						stringstream msgstr;
						msgstr << "Failed to convert destination argument: " << ex.what();

						string msg = msgstr.str();
						throw runtime_error(msg);
					}

					break;
				}

			case 'm':
				{
					try
					{
						message.FromSerialization(optarg);
						gotMessage = true;
					}
					catch(exception& ex)
					{
						stringstream msgstr;
						msgstr << "Failed to convert message: " << ex.what();

						string msg = msgstr.str();
						throw runtime_error(msg);
					}

					break;
				}

			case 'p':
				{
					portName = optarg;
					break;
				}

			case 't':
				{
					tag = optarg;
					tagSet = true;
					break;
				}

			default:

				throw runtime_error("Unknown option specified");
			}
		}
	} while(processing);

	if (optind < argc)
	{
		stringstream msg;
		msg << "Unexpected token '" << argv[optind] << "'";
		string s = msg.str();
		throw runtime_error(s);
	}

	if (!gotMessage)
		throw runtime_error("Missing message specification (-m or --msg)");

	if (addCRC)
	{
		if (crcStart >= message.size())
			throw runtime_error("CRC start is beyond the scope of the message");

		CRC16 crc(&message[crcStart], message.size()-crcStart);
		uint16_t resultingCRC = crc.Get();

		message.insert_type(message.end(), resultingCRC);
	}
}

int main(int argc, char* argv[])
{
	int retcode = 0;					// program exit code
	bool dorestart = false;

	// init time zone variables
	tzset();

	try
	{
		PayloadMessage::BoardAddress address = 0x2000;
		ByteArray message;
		PayloadMessage::BoardAddress destination = 1;
		string portName = "/dev/ttyS2";
		string tag;
		bool tagSet = false;

		ProcessArgs(argc, argv, address, message, destination, portName, tag, tagSet);

		OneTimeCommandSettings settings(address, message, destination, portName);

		// get the server name and kind from the setings, initialize the logger
#ifdef INCLUDE_LOGGER
		StdOutLogger logger(settings.GetServerName().c_str(), 1, Logger::LT_DEBUG);
#else
		StdOutLogger logger(settings.GetServerName().c_str(), 1, Logger::LT_NONE);
#endif

		logger.Debug
			<< "address=" << address << endl
			<< "message=" << message.ToStringSplitView() << endl
			<< "destination=" << destination << endl
			<< "portName=" << portName << endl
			<< "tag=" << tag << endl
			<< "tagSet=" << tagSet << endl;

		bool runIt;

		if (!tagSet)
		{
			runIt = true;
			logger.Debug << "No tag is set" << endl;
		}
		else if (DifferentTag(tag))
		{
			runIt = true;
			logger.Debug << "Tag is different" << endl;
		}
		else
		{
			runIt = false;
			logger.Debug << "Tag is the same" << endl;
		}

		if (runIt)
		{
			dorestart = true;

			// create a semaphore to signal the main thread to shut down
			Semaphore runSem(0);

			// construct the controller and begin running the background thread
			OneTimeCommandController controller(settings, logger, runSem);
			HDLCPort::Ptr pHDLC = controller.InitHDLC();

			// start the protocol asynchronously
			boost::thread runThread(boost::bind(&OneTimeCommandController::Run, &controller, pHDLC));

			// wait for exit
			runSem.Wait();
		}
	}
	catch(exception& ex)
	{
		cerr << argv[0] << " caught exception " << typeid(ex).name() << ": " << ex.what() << " (program will exit)" << endl;
		retcode = 1;
	}

	// restart the computer
	if (dorestart)
	{
#ifdef NO_FULL_RESTART
		cerr << "Full restart is disabled" << endl;
#else
		cerr << "Restarting the system" << endl;
		system("sudo shutdown -r now");
#endif
	}

	return retcode;
}
