//---------------------------------------------------------------------------------
// <copyright file="PayloadManagerClient.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 the PayloadManagerClient class, which wraps registration and
//   de-registration of objects that implement the IPayloadDevice interface and
//   listens for changes in registration.
// </summary>
//
// <owner>Mike Cookson</owner>
//---------------------------------------------------------------------------------

#include "PayloadManagerClient.h"
#include "LqrSMACIObjectBroker-inline.h"
#include "LqrSMACPayloadDescription.h"
#include "PayloadRegistrationEvent.h"
#include <boost/filesystem.hpp>
#include <fstream>
#include <sstream>
#include <string>

using namespace std;
using namespace LiquidRobotics::SMACFramework;

namespace LiquidRobotics {
namespace EventLib {

PayloadManagerClient::PayloadManagerClient(const PayloadManagerClientSettings& _settings, IObjectBroker& _broker, EventTransceiver& _eventTrans)
: payloadAddedSubscriber(payloadAddedSignal, firingMutex)
, payloadChangeSubscriber(payloadChangedSignal, firingMutex)
, payloadRemovedSubscriber(payloadRemovedSignal, firingMutex)
, broker(_broker)
, eventTrans(_eventTrans)
, settings(_settings)
, myBoardID(0)
, myTaskID(0)
, commonEventType(_settings.GetCommonEventType(), _settings.GetCommonEventDomain())
{
}

PayloadManagerClient::~PayloadManagerClient()
{
}

// Retrieve the default board ID
PayloadManagerClient::byte PayloadManagerClient::GetDefaultBoardID()
{
	IPayloadManager_var mgr = GetPM();
	byte result = (byte) mgr->defaultBoardID();

	return result;
}

// Retrieve the description for this payload
PayloadDescription::Ptr PayloadManagerClient::GetPayloadDescription(bool translateDefaultBoardID)
{
	PayloadDescriptionList::Ptr descListPtr = GetPayloadDescriptions(false);

	IPayloadManager_var mgr = GetPM();
	const byte defaultBoardID = (byte) mgr->defaultBoardID();
	PayloadDescription::Ptr result;

	for (PayloadDescriptionList::iterator it = descListPtr->begin(); !result && (it != descListPtr->end()); ++it)
		if ((TranslateBoardID((*it)->GetBoardID(), defaultBoardID) == myBoardID) && ((*it)->GetTaskID() == myTaskID)) {
			result = (*it);
			if (translateDefaultBoardID)
				result->SetBoardID(myBoardID);
		}

	return result;
}

// Retrieve the list of payload descriptions
PayloadManagerClient::PayloadDescriptionList::Ptr PayloadManagerClient::GetPayloadDescriptions(bool translateDefaultBoardID)
{
	IPayloadManager_var mgr = GetPM();
	CORBAList_var corbaList = mgr->payloadDescriptions();

	PayloadDescriptionList::Ptr result;
	result.reset(new PayloadDescriptionList(corbaList.in()));

	if (translateDefaultBoardID)
	{
		const byte defaultBoardID = (byte) mgr->defaultBoardID();

		for (PayloadDescriptionList::iterator it = result->begin(); it != result->end(); ++it)
			(*it)->SetBoardID(TranslateBoardID((*it)->GetBoardID(), defaultBoardID));
	}

	return result;
}

// Register with PayloadManager using device data in settings
void PayloadManagerClient::Register(Object_ptr server, bool overwrite)
{
	// sets up a callback on ORB shutdown so that registrations are removed
	if (!shutdownTask)
	{
		shutdownTask.reset( new ShutdownTask(*this) );
		shutdownTask->connect(broker);
	}

	// get the universal locator for the server object
	string ior = broker.GetStringizedReference(server);

	// get description from settings as a copy and overwrite the IOR with the one above
	PayloadDescription desc = settings.GetPayloadDescription();
	desc.SetIOR(ior);

	// if there's a "server name".json file, then load the content
	try
	{
		const string fileName = DetermineRegFilename();
		ifstream ifs;
		ifs.open(fileName.c_str());
		string fileContent;

		if (ifs != NULL) {
			stringstream sstm;
			sstm << ifs.rdbuf();
			fileContent = sstm.str();
		} else
			fileContent = "{}";

		desc.SetDescriptiveJSON(fileContent);
	}
	catch(...)
	{
		desc.SetDescriptiveJSON("{}");
	}

	// pull out the CORBA version of the entity
	PayloadDescription::CORBAEntity_var entity;
	entity = desc.GetEntity();

	// get a reference on the payload manager
	IPayloadManager_var mgr = GetPM();
	bool success;

	try
	{
		// register the instance
		mgr->Register(entity.in(), overwrite);
		success = true;

		// get my board ID and task ID for later retreival
		myBoardID = TranslateBoardID(desc.GetBoardID());
		myTaskID = desc.GetTaskID();
		cout << "PayloadManagerClient::Register 1: BoardID " << int(myBoardID) << endl;
	}
	catch(...)
	{
		success = false;
	}

	// save the registration so it can be automatically removed later
	if (success)
		registeredPtrs[server] = ior;
}

// Register with PayloadManager using ad hoc created PayloadDescription
void PayloadManagerClient::Register(Object_ptr server, const PayloadDescription& description, bool overwrite)
{
	// sets up a callback on ORB shutdown so that registrations are removed
	if (!shutdownTask)
	{
		shutdownTask.reset( new ShutdownTask(*this) );
		shutdownTask->connect(broker);
	}

	// get the universal locator for the server object
	string ior = broker.GetStringizedReference(server);

	// get description from settings as a copy and overwrite the IOR with the one above
	PayloadDescription desc = description;
	desc.SetIOR(ior);

	// pull out the CORBA version of the entity
	PayloadDescription::CORBAEntity_var entity;
	entity = desc.GetEntity();

	// get a reference on the payload manager
	IPayloadManager_var mgr = GetPM();
	bool success;

	try
	{
		// register the instance
		mgr->Register(entity.in(), overwrite);
		success = true;

		// get my board ID and task ID for later retreival
		myBoardID = TranslateBoardID(desc.GetBoardID());
		myTaskID = desc.GetTaskID();
		cout << "PayloadManagerClient::Register 2: BoardID " << int(myBoardID) << endl;
	}
	catch(...)
	{
		success = false;
	}

	// save the registration so it can be automatically removed later
	if (success)
		registeredPtrs[server] = ior;
}

// Unsubscribe from EventManager's signals (no further events will be fired)
void PayloadManagerClient::RevokeCallbacks()
{
	addCallback.reset();
	changeCallback.reset();
	removeCallback.reset();
}

// Subscribe to EventManager's callbacks (begin firing events from this object's signals)
void PayloadManagerClient::SubscribeCallbacks(EventTransceiver& eventTrans)
{
	RevokeCallbacks();

	const string& addEventName = settings.GetDeviceAddedEventName();
	const string& changeEventName = settings.GetDeviceChangedEventName();
	const string& removeEventName = settings.GetDeviceRemovedEventName();

	if (commonEventType.GetTypeName().length()
	 && (addEventName.length() || changeEventName.length() || removeEventName.length()))
	{
		if (!classFact)
		{
			classFact.reset(new EventTransceiver::TEventClassFactory<PayloadRegistrationEvent>);
			eventTrans.RegisterEventClassFactory(commonEventType, classFact);
		}

		if (addEventName.length())
		{
			addCallback.reset(new CallbackHandler(*this, &PayloadManagerClient::DeviceAddedHandler, commonEventType));
			addCallback->init(eventTrans.SignalForEventName(addEventName));
		}

		if (changeEventName.length())
		{
			changeCallback.reset(new CallbackHandler(*this, &PayloadManagerClient::DeviceChangedHandler, commonEventType));
			changeCallback->init(eventTrans.SignalForEventName(changeEventName));
		}

		if (removeEventName.length())
		{
			removeCallback.reset(new CallbackHandler(*this, &PayloadManagerClient::DeviceRemovedHandler, commonEventType));
			removeCallback->init(eventTrans.SignalForEventName(removeEventName));
		}
	}
}

// Translate a board adddress that contain a default board ID to the actual board address
uint16_t PayloadManagerClient::TranslateBoardAddress(uint16_t boardAddress, byte defaultBoardID)
{
	const byte boardID = TranslateBoardID(boardAddress >> 8, defaultBoardID);
	const uint16_t result = (((uint16_t) boardID) << 8) | (boardAddress & 0xFF);

	return result;
}

// Translate a board ID that may point to the default to its actual board ID
PayloadManagerClient::byte PayloadManagerClient::TranslateBoardID(byte boardID, byte defaultBoardID)
{
	byte result = boardID;

	if (result == PayloadManagerClientDetail::defaultBoardIDProxy)
	{
		if (defaultBoardID == PayloadManagerClientDetail::defaultBoardIDProxy)
			defaultBoardID = GetDefaultBoardID();

		result = defaultBoardID;
	}

	return result;
}

// Unregister from PayloadManager and remove all servers
void PayloadManagerClient::Unregister()
{
	IPayloadManager_var mgr;
	bool set = false;

	while(!registeredPtrs.empty())
	{
		PtrToIORMap::iterator it = registeredPtrs.begin();

		try
		{
			if (!set)
			{
				mgr = GetPM();
				set = true;
			}

			mgr->Unregister(it->second.c_str());
		}
		catch(...)
		{
		}

		registeredPtrs.erase(it);
	}
}

// Unregister from PayloadManager and delist objects for given server
void PayloadManagerClient::Unregister(Object_ptr server)
{
	PtrToIORMap::iterator it = registeredPtrs.find(server);

	if (it != registeredPtrs.end())
	{
		try
		{
			IPayloadManager_var mgr = GetPM();
			mgr->Unregister(it->second.c_str());
		}
		catch(...)
		{
		}

		registeredPtrs.erase(it);
	}
}

PayloadManagerClient::IPayloadManager_var PayloadManagerClient::GetPM()
{
	return broker.GetNamedInstance<IPayloadManager_var, IPayloadManager>(settings.GetServiceName().c_str(), settings.GetServiceKind().c_str());
}

void PayloadManagerClient::DeviceAddedHandler(const PayloadDescription& desc)
{
	payloadAddedSignal(*this, desc);
}

void PayloadManagerClient::DeviceChangedHandler(const PayloadDescription& desc)
{
	payloadChangedSignal(*this, desc);
}

void PayloadManagerClient::DeviceRemovedHandler(const PayloadDescription& desc)
{
	payloadRemovedSignal(*this, desc);
}

PayloadManagerClient::CallbackHandler::CallbackHandler(PayloadManagerClient& _client, HandlerType _handler, const EventType& _evtType)
: client(_client)
, handler(_handler)
, evtType(_evtType)
{
}

void PayloadManagerClient::CallbackHandler::init(EventTransceiver::InformSignalType& sig)
{
	scopedConnection.reset(	new ScopedConnection(sig.connect(*this)) );
}

void PayloadManagerClient::CallbackHandler::operator()(EventTransceiver& sender, const IEventBase& receivedEvent)
{
	const PayloadRegistrationEvent& regEvent =
		EventTransceiver::UpcastEventRef<PayloadRegistrationEvent>(receivedEvent, evtType,
				"PayloadManagerClient::CallbackHandler::operator()");

	PayloadDescription::Ptr descPtr = regEvent.Get();

	if (descPtr)
		(client.*handler)(*descPtr);
}

PayloadManagerClient::ShutdownTask::ShutdownTask(PayloadManagerClient& _client)
: client(_client)
{
}

PayloadManagerClient::ShutdownTask::~ShutdownTask()
{
}

void PayloadManagerClient::ShutdownTask::connect(IObjectBroker& broker)
{
	ScopedConnection* sc = new ScopedConnection(broker.ShutdownSignal.connect(*this));
	connection.reset(sc);
}

void PayloadManagerClient::ShutdownTask::operator()(IObjectBroker& sender)
{
	// not a good idea to throw during shutdown operations
	try
	{
		client.Unregister();
	}
	catch(...)
	{
	}

	// disconnect signal callback
	connection.reset();
}

// Retrieve registered board address
uint16_t PayloadManagerClient::GetMyBoardAddress() const
{
	uint16_t result = (((uint16_t) myBoardID) << 8) + myTaskID;
	return result;
}

// Retrieve registered board ID
PayloadManagerClient::byte PayloadManagerClient::GetMyBoardID() const
{
	return myBoardID;
}

// Retrieve registered task ID
PayloadManagerClient::byte PayloadManagerClient::GetMyTaskID() const
{
	return myTaskID;
}

string PayloadManagerClient::GetJSON()
{
	PayloadDescription::Ptr descPtr = GetPayloadDescription(false);
	if (!descPtr) throw runtime_error("Unable to locate address in payload descriptions");
	return descPtr->GetDescriptiveJSON();
}

bool PayloadManagerClient::UpdateJSON(const string& newJSON)
{
	bool result = false;

	// read the old payload description
	PayloadDescription::Ptr descPtr = GetPayloadDescription(false);
	if (!descPtr) throw runtime_error("Unable to locate address in payload descriptions");

	// if the descriptive JSON changed ...
	if (!boost::iequals(descPtr->GetDescriptiveJSON(), newJSON)) {

		// update the payload description with new JSON
		descPtr->SetDescriptiveJSON(newJSON);

		// rewrite the file
		try {
			string regFileName = DetermineRegFilename();
			ofstream f;
			f.open(regFileName.c_str(), ios::out);
			f << newJSON;
			f.close();
		} catch(...) { }

		// re-register the payload with the JSON
		PayloadDescription::CORBAEntity_var entity;
		entity = descPtr->GetEntity();
		IPayloadManager_var mgr = GetPM();
		mgr->Register(entity.in(), true);
		result = true;
	}

	return result;		// return is true if a change was made, false if no change was made
}

bool PayloadManagerClient::UpdateJSON(
		const SV3RemotingLib::RegulusREST::PRHardware::Vector& hardwareInfo)
{
	const string json = GetJSON();
	const string newJSON = SV3RemotingLib::RegulusREST::PRHardware::UpdateJSON(json, hardwareInfo);
	return UpdateJSON(newJSON);
}

bool PayloadManagerClient::UpdateJSON(
		const SV3RemotingLib::RegulusREST::PRInterface& interfaceInfo)
{
	const string json = GetJSON();
	const string newJSON = interfaceInfo.UpdateJSON(json);
	return UpdateJSON(newJSON);
}

bool PayloadManagerClient::UpdateJSON(
		const SV3RemotingLib::RegulusREST::PRHardware::Vector& hardwareInfo,
		const SV3RemotingLib::RegulusREST::PRInterface& interfaceInfo)
{
	const string json = GetJSON();
	const string newJSON = interfaceInfo.UpdateJSON(SV3RemotingLib::RegulusREST::PRHardware::UpdateJSON(json, hardwareInfo));
	return UpdateJSON(newJSON);
}

string PayloadManagerClient::DetermineRegFilename() const
{
	using namespace boost::filesystem;
	boost::system::error_code ec1, ec2;
	const PayloadDescription desc = settings.GetPayloadDescription();
	const string specificFileName = desc.GetReadableName()+".json";
	const string genericFileName = "reg.json";
	const string result = ((!exists(specificFileName, ec1) || ec1) && (exists(genericFileName, ec2) && !ec2)) ? genericFileName : specificFileName;
	return result;
}

}
}
