//------------------------------------------------------------------------------
// <copyright file="VirtualRelayController.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 VirtualRelayController class. This class is responsible
//  for managing a single channel (ie, Cell or WiFi) and the connection to
//  WGMS. It can receive outgoing FCC messages from VirtualRelayProgram, and
//  will send them over the channel. It will also fire off signals when it
//  receives a WGMS command, when the connection times out (based off telemetry)
//  and when it wants a channel request to be sent.
// </summary>
//
// <owner>Arash Ushani</owner>
//------------------------------------------------------------------------------

#include "VirtualRelayController.h"
#include "VirtualRelayControllerSettings.h"
#include "VirtualRelayMessageManager.h"
#include "LqrSMACGetErrorDescription.h"
#include "LqrSMACLogger.h"
#include "nsextend.h"
#include <vector>
#include <errno.h>
#include <stdint.h>
#include <iostream>
#include <sstream>
#include <boost/array.hpp>
#include <boost/asio.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/foreach.hpp>

using namespace std;
using namespace boost;
using namespace boost::asio;
using namespace CORBA;
using namespace LiquidRobotics::SMACFramework;

namespace LiquidRobotics {
namespace VirtualRelay {

VirtualRelayController::VirtualRelayController(VirtualRelayControllerSettings& _settings, Logger& _logger)
: settings(_settings)
, logger(_logger)
{
	Logger::Locked(logger)->Debug << Describe() << " Constructor" << endl;

	messageManagerPtr.reset(new VirtualRelayMessageManager(settings.GetMessageManagerSettings(), logger));

	incomingMessageSignalPtr.reset(new IncomingMessageSignalType());
	channelRequestSignalPtr.reset(new ChannelRequestSignalType());
	telemetryTimeoutSignalPtr.reset(new TelemetryTimeoutSignalType());

	connectionToFCCIncomingMessageEvent = messageManagerPtr->SubscribeToIncomingMessages(boost::bind(&VirtualRelayController::HandleIncomingMessageFromShore, this, _1, _2));

	lastSocketSessionTime = -settings.GetSleepTime(); //Do this so we start a socket session right away;
}

VirtualRelayController::~VirtualRelayController()
{
	Logger::Locked(logger)->Debug << Describe() << " Destructor..." << endl;
}

VirtualRelayMessageManager::ScopedConnectionPtr VirtualRelayController::SubscribeToIncomingMessages(IncomingMessageSignalType::slot_type slot)
{
	ScopedConnectionPtr incomingMessageConnectionPtr;
	incomingMessageConnectionPtr.reset(new ScopedConnection(incomingMessageSignalPtr->connect(slot)));
	return incomingMessageConnectionPtr;
}

VirtualRelayMessageManager::ScopedConnectionPtr VirtualRelayController::SubscribeToChannelRequests(ChannelRequestSignalType::slot_type slot)
{
	ScopedConnectionPtr channelRequestConnectionPtr;
	channelRequestConnectionPtr.reset(new ScopedConnection(channelRequestSignalPtr->connect(slot)));
	return channelRequestConnectionPtr;
}

VirtualRelayMessageManager::ScopedConnectionPtr VirtualRelayController::SubscribeToTelemetryTimeout(TelemetryTimeoutSignalType::slot_type slot)
{
	ScopedConnectionPtr telemetryTimeoutConnectionPtr;
	telemetryTimeoutConnectionPtr.reset(new ScopedConnection(telemetryTimeoutSignalPtr->connect(slot)));
	return telemetryTimeoutConnectionPtr;
}

void VirtualRelayController::Start()
{
	boost::thread(boost::bind(&VirtualRelayController::Run, this));
}

void VirtualRelayController::UpdateChannelState(bool state)
{
	channelOn = state;
}

void VirtualRelayController::UpdateSignalStrength(int signalStrength)
{
	messageManagerPtr->SetSignalStrength(signalStrength);
	this->signalStrength = signalStrength;
}

void VirtualRelayController::SetVehicleUniqueID(unsigned int uniqueID)
{
	messageManagerPtr->SetUniqueID(uniqueID);
}

void VirtualRelayController::SetSleepTime(int val)
{
	Logger::Locked(logger)->Info << Describe() << ": Setting sleep time to " << std::dec << val << " seconds" << endl;
	settings.SetSleepTime(val);
}

void VirtualRelayController::SetRetryRate(int val)
{
	Logger::Locked(logger)->Info << Describe() << ": Setting retry rate to " << std::dec << val  << " seconds" << endl;
	settings.SetRetryRate(val);
}

void VirtualRelayController::SetRetries(int val)
{
	Logger::Locked(logger)->Info << Describe() << ": Setting number of retries to " << val << endl;
	settings.SetRetries(val);
}

void VirtualRelayController::SetTimeout(int val)
{
	Logger::Locked(logger)->Info << Describe() << ": Setting timeout to " << val  << " seconds" << endl;
	settings.SetTimeoutSeconds(val);
}

void VirtualRelayController::SetTelemetryInterrupt(bool val)
{
	Logger::Locked(logger)->Info << Describe() << ": Setting telemetry interrupt " << (val ? "ON":"OFF") << endl;
	settings.GetMessageManagerSettings().SetTelemetryInterrupt(val);
}

void VirtualRelayController::SetAckInterrupt(bool val)
{
	Logger::Locked(logger)->Info << Describe() << ": Setting ack interrupt " << (val ? "ON":"OFF") << endl;
	settings.GetMessageManagerSettings().SetAckInterrupt(val);
}

void VirtualRelayController::Shutdown()
{
	messageManagerPtr->DumpMessages();
}

void VirtualRelayController::SetBoardID(int val)
{
	if (boardID==0)
	{
		boardID = val;

		// Set the session ID

		srand((unsigned)time(0));
		int random_integer = rand();

		unsigned int sessionID = GetAddress();
		sessionID <<= 16;
		sessionID += random_integer & 0xFFFF; // Use this line to create a semi-random session ID

		messageManagerPtr->SetSessionID(sessionID);
	}
}

int VirtualRelayController::GetAddress() const
{
	return settings.GetCommsTaskID() + (boardID<<8);
}

int VirtualRelayController::GetProtocolRevision() const
{
	return settings.GetMessageManagerSettings().GetProtocolRevision();
}

VirtualRelayPacketComparision::Priority VirtualRelayController::HighestPriorityMessage()
{
	return messageManagerPtr->HighestPriority();
}

std::vector<unsigned char> VirtualRelayController::PopMessage()
{
	return messageManagerPtr->PopMessage();
}

// Returns true if successful looking up host (not necessarily connecting), false otherwise
bool VirtualRelayController::StartSocketTransaction()
{
	successfulTransaction = false;

	if (!channelOn) {
//Logger::Locked(logger)->Info << Describe() << ": StartSocketTransaction: channelOn==false" << endl;
//TODO: consider putting a wait here; otherwise, we spin wait...
		return false;
	}

	io_service.reset(new asio::io_service());
	socket.reset(new ip::tcp::socket(*io_service));

	std::string host = settings.GetHost();
	Logger::Locked(logger)->Info << Describe() << ": Connecting to: " << host << endl;

	ip::tcp::resolver resolver(*io_service);
	ip::tcp::resolver::query query(host, "http");
	ip::tcp::resolver::iterator iter;

	try
	{
		iter = resolver.resolve(query);
	}
	catch (std::exception& ex)
	{
		// Keep track of when we last tried to communicate to WGMS
		lastSocketSessionTime = GetSystemUpTimeSeconds();

		Logger::Locked(logger)->Error << Describe() << ": Exception during query: " << typeid(ex).name() << "\n" << ex.what() << endl;
		Logger::Locked(logger)->Error << Describe() << ": Could not connect to WGMS. Ending socket session." << endl;
		CloseSocket();
		return false;
	}

	ip::tcp::endpoint endpoint = *iter;
	endpoint.port(settings.GetHostPort());

	Logger::Locked(logger)->Info << Describe() << ": Connecting to: " << std::dec << endpoint << endl;
	socket->async_connect(endpoint, boost::bind(&VirtualRelayController::HandleRelayConnection, this, placeholders::error));

	if (!IOServiceIsRunning)
		boost::thread(boost::bind(&VirtualRelayController::RunService, this));
	else
		Logger::Locked(logger)->Error << Describe() << ": IO Service already running" << endl;

	boost::asio::deadline_timer timer(*io_service);
	timer.expires_from_now(boost::posix_time::seconds(settings.GetTimeoutSeconds()));
	timer.wait();

	if (IOServiceIsRunning)
	{
		Logger::Locked(logger)->Error << Describe() << ": Connection to WMGS Timed out after " << std::dec << settings.GetTimeoutSeconds() << " seconds!" << endl;
		CloseSocket();
	}

	// Keep track of when we last tried to communicate to WGMS
	lastSocketSessionTime = GetSystemUpTimeSeconds();

	return successfulTransaction;
}

void VirtualRelayController::CloseSocket()
{
	try
	{
		Logger::Locked(logger)->Debug << Describe() << ": Closing Socket..." << endl;

		if (socket->is_open())
		{
			try
			{
				socket->shutdown(ip::tcp::socket::shutdown_both);
			}
			catch (std::exception& ex)
			{
				Logger::Locked(logger)->Debug << Describe() << ": Exception during socket shutdown: " << typeid(ex).name() << "\n" << ex.what() << endl;
			}

			try
			{
				socket->cancel();
			}
			catch (std::exception& ex)
			{
				Logger::Locked(logger)->Debug << Describe() << ": Exception during socket cancel: " << typeid(ex).name() << "\n" << ex.what() << endl;
			}

			try
			{
				socket->close();
			}
			catch (std::exception& ex)
			{
				Logger::Locked(logger)->Debug << Describe() << ": Exception during socket close: " << typeid(ex).name() << "\n" << ex.what() << endl;
			}
		}

		runIOService = false;
		io_service->stop();
	}
	catch (std::exception& ex)
	{
		Logger::Locked(logger)->Error << Describe() << ": Exception closing socket." << endl;
		Logger::Locked(logger)->Error << Describe() << ": Caught exception " << typeid(ex).name() << "\n" << ex.what() << endl;
	}
}

void VirtualRelayController::RunService()
{
	runIOService = true;
	IOServiceIsRunning = true;

	try
	{
		while(runIOService)
		{
			size_t numberRan = io_service->run_one();
			if (numberRan==0)
			{
				runIOService = false;
				Logger::Locked(logger)->Info << Describe() << ": IO Service stopped" << endl;
				break;
			}
		}
		IOServiceIsRunning = false;
	}
	catch (std::exception& ex)
	{
		Logger::Locked(logger)->Error << Describe() << ": Exception running IO Service." << endl;
		Logger::Locked(logger)->Error << Describe() << ": Caught exception " << typeid(ex).name() << ":\t" << ex.what() << endl;

		runIOService = false;
		IOServiceIsRunning = false;
	}
}

void VirtualRelayController::HandleRelayConnection(const boost::system::error_code& error)
{
	if (!error)
	{
		socket->async_receive(buffer(socketBuffer, SOCKETBUFFERSIZE), boost::bind(&VirtualRelayController::HandleIncomingRelayMessages, this, _1, _2));
		Logger::Locked(logger)->Info << Describe() << ": Connected successfully!" << endl;

		UInt8Vector toSend;
		messageManagerPtr->GetOutgoingPacket(SOCKETBUFFERSIZE, toSend);

		Logger::Locked(logger)->Debug << Describe() << ": Outgoing relay message: ";
		for (unsigned int i=0; i<toSend.size(); i++)
			Logger::Locked(logger)->Debug << utos(toSend[i], 16, 2) << " ";
		Logger::Locked(logger)->Debug << endl;

		size_t sentBytes = socket->write_some(boost::asio::buffer(toSend));
		Logger::Locked(logger)->Info << Describe() << ": Sent " << std::dec << sentBytes << " bytes" << endl;

		if (sentBytes!=toSend.size())
			Logger::Locked(logger)->Error << Describe() << ": Error sending messages." << endl;
	}
	else
	{
		if (error==error::operation_aborted)
		{
			Logger::Locked(logger)->Error << Describe() << ": Async connect has been aborted" << endl;
			CloseSocket();
		}
		else
		{
			Logger::Locked(logger)->Error << Describe() << ": Error making connection: " << error.message() << endl;
		}
	}
}

void VirtualRelayController::HandleIncomingRelayMessages(const boost::system::error_code& error, std::size_t bytes)
{
	if (error || bytes==0)
	{
		// If we get an error, the server has probably closed the socket
		if (error==error::eof)
			Logger::Locked(logger)->Info << Describe() << ": Socket has been closed by server." << endl;
		else if (error)
			Logger::Locked(logger)->Error << Describe() << ": Error encountered on async read with " << std::dec << bytes << " bytes. Error: " << error.message() << endl;

		CloseSocket();
		return;
	}

	socket->async_receive(buffer(socketBuffer, SOCKETBUFFERSIZE), boost::bind(&VirtualRelayController::HandleIncomingRelayMessages, this, _1, _2));

	try
	{
		UInt8Vector incomingMsg(socketBuffer, socketBuffer + bytes);
		Logger::Locked(logger)->Debug << Describe() << ": Incoming Relay Message (" << std::dec << bytes << " bytes) Received: ";
		for (unsigned int i=0; i<incomingMsg.size(); i++)
			Logger::Locked(logger)->Debug << utos(incomingMsg[i], 16, 2) << " ";
		Logger::Locked(logger)->Debug << endl;

		messageManagerPtr->ProcessIncomingPacket(incomingMsg);
		successfulTransaction = true;
	}
	catch (std::exception& ex)
	{
		Logger::Locked(logger)->Error << Describe() << ": Exception when handling incoming relay message." << endl;
		Logger::Locked(logger)->Error << Describe() << ": Caught exception " << typeid(ex).name() << "\n" << ex.what() << endl;
	}
}

void VirtualRelayController::HandleIncomingMessageFromShore(const VirtualRelayMessageManager& sender, const UInt8Vector& incomingMessage)
{
	SendIncomingFCCMessage(incomingMessage);
}

void VirtualRelayController::OutgoingMessage(std::vector<byte>& message)
{
	if (messageManagerPtr->HandleOutgoingMessage(message))
		SocketSession();
}

void VirtualRelayController::SendIncomingFCCMessage(const UInt8Vector& message)
{
	(*incomingMessageSignalPtr)(*this, message);
}

void VirtualRelayController::ChannelRequest(bool state)
{
	(*channelRequestSignalPtr)(*this, state);
}

void VirtualRelayController::SignalTelemetryTimeout()
{
	Logger::Locked(logger)->Error << Describe() << ": Telemetry Timeout" << endl;
	(*telemetryTimeoutSignalPtr)(*this);
}

void VirtualRelayController::Run()
{
	Logger::Locked(logger)->Debug << Describe() << ": Starting..." << endl;

	while(true)
	{
//PBW: extra debugging...  remove when satisfied that it works as desired.
//Logger::Locked(logger)->Info << Describe() << ": settings.GetRetryRate(): " << std::dec << settings.GetRetryRate() << endl;
//Logger::Locked(logger)->Info << Describe() << ": settings.GetSleepTime(): " << std::dec << settings.GetSleepTime() << endl;
//Logger::Locked(logger)->Info << Describe() << ": TimeToNextRelayStatus(): " << std::dec << TimeToNextRelayStatus() << endl;

//Logger::Locked(logger)->Info << Describe() << ": GetSystemUpTimeSeconds(): " << std::dec << GetSystemUpTimeSeconds() << endl;
//Logger::Locked(logger)->Info << Describe() << ": lastSocketSessionTime + settings.GetSleepTime(): " << std::dec << lastSocketSessionTime + settings.GetSleepTime() << endl;
//Logger::Locked(logger)->Info << Describe() << ": settings.GetSleepTime(): " << std::dec << settings.GetSleepTime() << endl;
//Logger::Locked(logger)->Info << Describe() << ": lastSocketSessionTime: " << std::dec << lastSocketSessionTime << endl;


		if (TimeToNextRelayStatus()<=0)
		{
			Logger::Locked(logger)->Info << Describe() << ": Starting new session." << endl;

			if (!SocketSession())
			{
				boost::this_thread::sleep(boost::posix_time::milliseconds(ceil(settings.GetRetryRate()*1000)));
			}
		}

		Logger::Locked(logger)->Info << Describe() << ": Thread sleeping." << endl;

		while(TimeToNextRelayStatus()>0)
		{
			// Wait in small chunks in case the sleep time changes

			int waitTime = (unsigned) ceil(1000*TimeToNextRelayStatus());

			if (waitTime > settings.GetRetryRate() || settings.GetSleepTime()==0)
				waitTime = settings.GetRetryRate();

			boost::this_thread::sleep(boost::posix_time::milliseconds(waitTime));
		}
	}

	Logger::Locked(logger)->Error << Describe() << ": Exiting Relay Status thread" << endl;
}

// Returns false is there is an existing socket session active, true otherwise
bool VirtualRelayController::SocketSession()
{
	if (!socketSessionMutex.try_lock())
	{
		Logger::Locked(logger)->Debug << Describe() << ": Socket Session already running" << endl;
		return false;
	}

	try
	{
		// Send the connection request. Even if we did not send the original request,
		// we don't want the connection suddenly closed on us
		ChannelRequest(true);

		int retries = settings.GetRetries();
		int retryRate = settings.GetRetryRate();

		bool success = false;

		float nextSocketTransaction = 0;

		// Try to connect to WGMS until successful
		while (!success && retries-->0)
		{
//Logger::Locked(logger)->Debug << Describe() << ": SocketSession: GetSystemUpTimeSeconds()=" << std::dec << GetSystemUpTimeSeconds() << endl;
//Logger::Locked(logger)->Debug << Describe() << ": SocketSession: retryRate=" << std::dec << retryRate << endl;
			success = StartSocketTransaction();

			nextSocketTransaction = GetSystemUpTimeSeconds() + retryRate;
//Logger::Locked(logger)->Debug << Describe() << ": SocketSession: nextSocketTransaction=" << std::dec << nextSocketTransaction << endl;

			if (!success) {
				boost::this_thread::sleep(boost::posix_time::milliseconds((nextSocketTransaction - GetSystemUpTimeSeconds())*1000));
		}
		}

		// Keep talking to WGMS as long as the connection is up and there are messages going back and forth
		if (success && (messageManagerPtr->RequestKeepConnectionActive() || settings.GetSleepTime()==0) )
		{
			// Start transactions with WGMS until we have a couple of transactions in which we had nothing to send
			int pendingConnections = 2;
			boost::this_thread::sleep(boost::posix_time::milliseconds((nextSocketTransaction - GetSystemUpTimeSeconds())*1000));

			while (pendingConnections>0)
			{
				Logger::Locked(logger)->Debug << Describe() << ": " << std::dec << pendingConnections << " Pending Connections" << endl;
				pendingConnections--;

//PBW: should "nextSocketTransaction" be redeclared here???
				float nextSocketTransaction = GetSystemUpTimeSeconds() + retryRate;
				success = StartSocketTransaction();

				// If we're still sending stuff back, keep the connection open
				if (success && (messageManagerPtr->RequestKeepConnectionActive() || settings.GetSleepTime()==0) )
					pendingConnections = 2;

				// If we're about to close the session, don't bother waiting
				if (pendingConnections>0)
					boost::this_thread::sleep(boost::posix_time::milliseconds((nextSocketTransaction - GetSystemUpTimeSeconds())*1000));
			}
		}

		// We are now done, so cancel the request
		ChannelRequest(false);

		// If we have too many telemetry messages queued that we haven't yet, fire timeout signal
		if (messageManagerPtr->TelemetryTimeout())
			SignalTelemetryTimeout();

		socketSessionMutex.unlock();

		return true;
	}
	catch(std::exception& ex)
	{
		Logger::Locked(logger)->Alert << "Socket Session caught " << typeid(ex).name() << " '" << ex.what() << "'" << endl;

		// End anything that's still pending
		if (IOServiceIsRunning)
		{
			Logger::Locked(logger)->Alert << "Stopping IO Service and Closing Socket." << endl;
			CloseSocket();
		}

		// Cancel our request so the channel doesn't stay on
		ChannelRequest(false);

		// In case the mutex was locked, unlock it
		socketSessionMutex.unlock();

		return true;
	}
}

float VirtualRelayController::TimeToNextRelayStatus()
{
	float now = GetSystemUpTimeSeconds();
	float nextCheckIn = lastSocketSessionTime + settings.GetSleepTime();

	return nextCheckIn - now;
}

std::string VirtualRelayController::Describe()
{
	std::stringstream ss;
	ss << "[" << GetSystemUpTimeSeconds() << "]";
	ss << " VR-Controller ";
	ss << std::dec << settings.GetMessageManagerSettings().GetProtocolRevision();
	ss << " Address 0x" << std::hex << GetAddress() << std::dec;
	return ss.str();
}

}
}
