//---------------------------------------------------------------------------------
// <copyright file="DataSetEvent.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 DataSetEvent, a telemetry type of event that carries a named
//   data set.  This format can be used to send whole files to remote destinations.
// </summary>
//
// <owner>Mike Cookson</owner>
//---------------------------------------------------------------------------------

#include "DataSetEvent.h"
#include "LqrSMACDetermineFileExtension.h"
#include "LqrSMACGetErrorDescription.h"
#include "LqrSMACIdl.h"
#include "nsextend.h"
#include <fcntl.h>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <sys/time.h>
#include <typeinfo>
#include <unistd.h>
#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>

using namespace std;
using namespace boost;
using namespace boost::filesystem;
using namespace LiquidRobotics::SMACFramework;

#ifdef NEW_FILESYSTEM
#undef NEW_FILESYSTEM
#endif
#ifdef BOOST_FILESYSTEM_VERSION
#if BOOST_FILESYSTEM_VERSION == 3
#define NEW_FILESYSTEM
#endif
#endif

namespace LiquidRobotics {
namespace EventLib {

#define EVENT_PREFIX "DataSetEvent_"

const DataSetEvent::PropertyNameType DataSetEvent::DataSetNameTag = EVENT_PREFIX "DataSetName";
const DataSetEvent::PropertyNameType DataSetEvent::ContentTag = EVENT_PREFIX "Content";
const DataSetEvent::PropertyNameType DataSetEvent::ContentFileNameTag = EVENT_PREFIX "ContentFileName";
const DataSetEvent::PropertyNameType DataSetEvent::ReceiverOwnsContentTag = EVENT_PREFIX "ReceiverOwnsContent";

DataSetEvent::DataSetEvent(IObjectBroker& _broker)
: Base(_broker)
{
}

DataSetEvent::DataSetEvent(const EventSettings& _eventSettings, IObjectBroker& _broker)
: Base(_eventSettings, _broker)
{
}

DataSetEvent::DataSetEvent(const EventName& _eventName, const EventType& _eventType, IObjectBroker& _broker)
: Base(_eventName, _eventType, _broker)
{
}

DataSetEvent::DataSetEvent(const EventName& _eventName, const EventType& _eventType, const IOR& _senderIOR, IObjectBroker& _broker)
: Base(_eventName, _eventType, _senderIOR, _broker)
{
}

DataSetEvent::DataSetEvent(const DataSetEvent& other)
: Base(other)
{
}

ostream& DataSetEvent::Describe(ostream& stm) const
{
	Base::Describe(stm);

	if (HasDataSetName())
		stm << " dataSetName='" << GetDataSetName() << "'";

	if (ContainsProperty(ContentFileNameTag))
	{
		stm << " contentFileName='" << GetProperty(ContentFileNameTag).GetString() << "'";

		if (ContainsProperty(ReceiverOwnsContentTag))
			stm << " receiverOwnsContent=" << (GetProperty(ReceiverOwnsContentTag).GetBoolean() ? "true" : "false");
		else
			stm << " **does not contain " << ReceiverOwnsContentTag << " tag**";
	}
	else if (ContainsProperty(ContentTag))
	{
		stringstream msg;
		msg << *GetProperty(ContentTag).GetByteArray();

		string msgStr = msg.str();

		stm << " content=";

		if (msgStr.length() >= 60)
			stm << msgStr.substr(0,28) << " ... " << msgStr.substr(msgStr.length()-27,27);
		else
			stm << msgStr;
	}
	else
		stm << " **contains neither " << ContentTag << " nor " << ContentFileNameTag << " **";

	return stm;
}

IEventBase::Ptr DataSetEvent::Duplicate() const
{
	IEventBase::Ptr result;
	result.reset(new DataSetEvent(*this));

	return result;
}

void DataSetEvent::ClearDataSetName()
{
	if (ContainsProperty(DataSetNameTag))		ClearProperty(DataSetNameTag);
}

EventPropertyValue::String DataSetEvent::GetDataSetName() const
{
	return GetProperty(DataSetNameTag).GetString();
}

bool DataSetEvent::HasDataSetName() const
{
	return ContainsProperty(DataSetNameTag);
}

void DataSetEvent::SetDataSetName(const EventPropertyValue::String& newValue)
{
	EventPropertyValue epv(broker);

	epv.SetString(newValue);
	SetProperty(DataSetNameTag, epv);
}

void DataSetEvent::GetContent(ByteArray& content, size_t startOffset, size_t length) const
{
	if (HasContentFileName())
	{
		EventPropertyValue::String fileName = "(not found)";

		try
		{
			content.resize(length);

			if (length)
			{
				fileName = GetContentFileName();
				fstream stm(fileName.c_str(), ios_base::in | ios_base::binary);

				stm.seekg(startOffset, ios_base::beg);
				stm.read((char*) &content.front(), length);
			}
		}
		catch(CORBA::Exception& ex)
		{
			content.clear();

			stringstream msg;
			msg << "Caught " << DecodeCORBAException(ex) << " while attempting to read " << length << " bytes at offset " << startOffset << " in file " << fileName;

			throw runtime_error(msg.str());
		}
		catch(std::exception& ex)
		{
			content.clear();

			stringstream msg;
			msg << "Caught " << typeid(ex).name() << " '" << ex.what() << "' while attempting to read " << length << " bytes at offset " << startOffset << " in file " << fileName;

			throw runtime_error(msg.str());
		}
	}
	else
	{
		string numBytes = "(not found)";

		try
		{
			ByteArray contentField;
			GetNonFileContent(contentField);

			if (startOffset > contentField.size())
			{
				stringstream msg;
				msg << "starting point " << startOffset << " begins after the end of the content";

				throw runtime_error(msg.str());
			}

			numBytes = ultos(contentField.size());
			numBytes += " bytes";

			if (length > (contentField.size()-startOffset))
				length = contentField.size()-startOffset;

			content.resize(length);

			if (length)
				copy(contentField.begin()+startOffset, contentField.begin()+startOffset+length, content.begin());
		}
		catch(CORBA::Exception& ex)
		{
			content.clear();

			stringstream msg;
			msg << "Caught " << DecodeCORBAException(ex) << " while attempting to read " << length << " bytes at offset " << startOffset << " in event content field of " << numBytes;

			throw runtime_error(msg.str());
		}
		catch(std::exception& ex)
		{
			content.clear();

			stringstream msg;
			msg << "Caught " << typeid(ex).name() << " '" << ex.what() << "' while attempting to read " << length << " bytes at offset " << startOffset << " in event content field of " << numBytes;

			throw runtime_error(msg.str());
		}
	}
}

string DataSetEvent::GetContentFileName() const
{
	return GetProperty(ContentFileNameTag).GetString();
}

size_t DataSetEvent::GetContentLength() const
{
	size_t result = 0;

	if (ContainsProperty(ContentFileNameTag))
	{
		EventPropertyValue::String fileName = "(not found)";

		try
		{
			fileName = GetProperty(ContentFileNameTag).GetString();
			fstream stm(fileName.c_str(), ios_base::in | ios_base::binary);

			stm.seekg(0, ios_base::end);
			result = stm.tellg();
		}
		catch(CORBA::Exception& ex)
		{
			stringstream msg;
			msg << "Caught " << DecodeCORBAException(ex) << " while attempting to determine the length of event content file " << fileName;

			throw runtime_error(msg.str());
		}
		catch(std::exception& ex)
		{
			stringstream msg;
			msg << "Caught " << typeid(ex).name() << " '" << ex.what() << "' while attempting to determine the length of event content file " << fileName;

			throw runtime_error(msg.str());
		}
	}
	else
	{
		try
		{
			ByteArray::Ptr contentField = GetProperty(ContentTag).GetByteArray();

			result = contentField->size();
		}
		catch(CORBA::Exception& ex)
		{
			stringstream msg;
			msg << "Caught " << DecodeCORBAException(ex) << " while attempting to determine the length of event content";

			throw runtime_error(msg.str());
		}
		catch(std::exception& ex)
		{
			stringstream msg;
			msg << "Caught " << typeid(ex).name() << " '" << ex.what() << "' while attempting to determine the length of event content";

			throw runtime_error(msg.str());
		}
	}

	return result;
}

void DataSetEvent::GetNonFileContent(ByteArray& content) const
{
	ByteArray::Ptr contentField = GetProperty(ContentTag).GetByteArray();

	content.resize(contentField->size());

	if (content.size())
		content.assign(contentField->begin(), contentField->end());
}

bool DataSetEvent::HasContentFileName() const
{
	return ContainsProperty(ContentFileNameTag) && ContainsProperty(ReceiverOwnsContentTag);
}

bool DataSetEvent::HasNonFileContent() const
{
	return ContainsProperty(ContentTag);
}

bool DataSetEvent::ReceiverOwnsFile() const
{
	return GetProperty(ReceiverOwnsContentTag).GetBoolean();
}

class AutoCloseFd
{
public:

	AutoCloseFd(const string& _fileName, int _fd = -1)
	: fd(_fd)
	, locked(false)
	, fileName(_fileName)
	{
	}

	~AutoCloseFd()
	{
		Close();
	}

	void Clear()
	{
		fd = -1;
	}

	void Close()
	{
		if (fd != -1)
		{
			close(fd);
			fd = -1;
		}
	}

	void Lock(bool writeLock)
	{
		if (fd == -1)		ThrowStatusFailure("Lock", "file is not open");
		if (locked)			ThrowStatusFailure("Lock", "file is already locked");

		flock lockStruct;
		memset(&lockStruct, '\0', sizeof(lockStruct));
		lockStruct.l_type = writeLock ? F_WRLCK : F_RDLCK;
		lockStruct.l_whence = SEEK_SET;
		lockStruct.l_pid = getpid();

		int status = fcntl(fd, F_SETLK, &lockStruct);

		if (status == -1)
			ThrowError("Lock", errno, writeLock ? "write lock failed" : "read lock failed");
	}

	void OpenForRead()
	{
		if (fd != -1)		ThrowStatusFailure("OpenForRead", "file is already open");

		fd = open(fileName.c_str(), O_RDONLY);

		if (fd == -1)
			ThrowError("OpenForRead", fd, "open failed");

		Lock(false);
	}

	void OpenForWrite()
	{
		if (fd != -1)		ThrowStatusFailure("OpenForWrite", "file is already open");

		fd = open(fileName.c_str(), O_WRONLY);

		if (fd == -1)
		{
			if (errno == ENOENT)
			{
				fd = open(fileName.c_str(), O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR);

				if (fd == -1)
					ThrowError("OpenForWrite", errno, "open/create failed");

				Lock(true);
			}
			else
				ThrowError("OpenForWrite", errno, "open failed");
		}
		else
			Lock(true);
	}

	int Read(void* buffer, size_t bufferLength)
	{
		if (fd == -1)		ThrowStatusFailure("Read", "file is not open");

		int status = read(fd, buffer, bufferLength);

		if (status == -1)
			ThrowError("Read", errno, "read failed");

		return status;
	}

	int Write(const void* buffer, size_t bufferLength)
	{
		if (fd == -1)		ThrowStatusFailure("Write", "file is not open");

		int status = write(fd, buffer, bufferLength);

		if (status == -1)
			ThrowError("Write", errno, "write failed");

		else if (status != (int) bufferLength)
		{
			stringstream msg;
			msg << "Write operation succeeded for only " << status << " of " << bufferLength << " bytes";

			throw runtime_error(msg.str());
		}

		return status;
	}

	void Unlock()
	{
		if (fd == -1)		ThrowStatusFailure("Unlock", "file is not open");
		if (!locked)		ThrowStatusFailure("Unlock", "file is not locked");

		flock lockStruct;
		memset(&lockStruct, '\0', sizeof(lockStruct));
		lockStruct.l_type = F_UNLCK;
		lockStruct.l_whence = SEEK_SET;
		lockStruct.l_pid = getpid();

		int status = fcntl(fd, F_SETLK, &lockStruct);
		if (status == -1)
			ThrowError("Unlock", errno, "release lock failed");
	}

	operator int()
	{
		return fd;
	}

private:

	void ThrowError(const string& method, int errNum, const string& operation)
	{
		stringstream msg;
		msg << method << "(" << fileName << "): " << operation << " errno=" << errNum << " " << GetErrorDescription(errNum);

		throw runtime_error(msg.str());
	}

	void ThrowStatusFailure(const string& method, const string& operation)
	{
		stringstream msg;

		msg << method << "(" << fileName << "): " << operation;
		throw runtime_error(msg.str());
	}

	int fd;
	bool locked;
	string fileName;
};

void DataSetEvent::SaveContentToFile(const string& fileName, bool deleteFileIfOwned) const
{
	if (ContainsProperty(ContentFileNameTag))
	{
		string sourceFileName = "(not found)";

		try
		{
			sourceFileName = GetProperty(ContentFileNameTag).GetString();

			AutoCloseFd srcFd(sourceFileName);
			srcFd.OpenForRead();

			AutoCloseFd destFd(fileName);
			destFd.OpenForWrite();

			int readStatus;
			int writeStatus;
			char buffer[4096];

			do
			{
				if (!!(readStatus = srcFd.Read(buffer, sizeof(buffer))))
					writeStatus = destFd.Write(buffer, readStatus);
			} while((readStatus > 0) && (writeStatus == readStatus));

			srcFd.Close();
			destFd.Close();

			if (deleteFileIfOwned)
				unlink(sourceFileName.c_str());
		}
		catch(CORBA::Exception& ex)
		{
			stringstream msg;
			msg << "Caught " << DecodeCORBAException(ex) << " while attempting to copy event content file " << sourceFileName << " to " << fileName;

			throw runtime_error(msg.str());
		}
		catch(std::exception& ex)
		{
			stringstream msg;
			msg << "Caught " << typeid(ex).name() << " '" << ex.what() << "' while attempting to copy event content file " << sourceFileName << " to " << fileName;

			throw runtime_error(msg.str());
		}
	}
	else
	{
		try
		{
			ByteArray::Ptr contentField = GetProperty(ContentTag).GetByteArray();
			AutoCloseFd destFd(fileName);

			destFd.OpenForWrite();

			if (contentField && contentField->size())
				destFd.Write(&contentField->front(), contentField->size());
		}
		catch(CORBA::Exception& ex)
		{
			stringstream msg;
			msg << "Caught " << DecodeCORBAException(ex) << " while attempting to determine the length of event content";

			throw runtime_error(msg.str());
		}
		catch(std::exception& ex)
		{
			stringstream msg;
			msg << "Caught " << typeid(ex).name() << " '" << ex.what() << "' while attempting to determine the length of event content";

			throw runtime_error(msg.str());
		}
	}
}

void DataSetEvent::SetContent(const ByteArray& content)
{
	if (ContainsProperty(ContentFileNameTag))		ClearProperty(ContentFileNameTag);
	if (ContainsProperty(ReceiverOwnsContentTag))	ClearProperty(ReceiverOwnsContentTag);

	EventPropertyValue epv(broker);
	epv.SetByteArray(content);
	SetProperty(ContentTag, epv);
}

void DataSetEvent::SetContent(const string& fileName, bool receiverOwnsFile)
{
	if (ContainsProperty(ContentTag))				ClearProperty(ContentTag);

	EventPropertyValue epv1(broker), epv2(broker);
	epv1.SetString(fileName);
	SetProperty(ContentFileNameTag, epv1);
	epv2.SetBoolean(receiverOwnsFile);
	SetProperty(ReceiverOwnsContentTag, epv2);
}

string DataSetEvent::SuggestSaveFileName(const string& directoryName, bool useContentSpecificExtension, size_t maxFileName) const
{
	string result;						// name to be returned
	string ext;							// extension to use

	// determine if the event contains a file and get the name of the file if it exists
	const bool isFile = HasContentFileName();
	const string contentFileName = isFile ? GetContentFileName() : "";

	// the returned filename will be similar to the data set name, or if no
	// data set name is provided, will be similar to the filename if any
	if (HasDataSetName())
		result = GetDataSetName();
	else
	{
		result = contentFileName;

		// if anything is provided so far, trim off the extension
		if (result.length())
		{
			path temp = result;

#ifdef NEW_FILESYSTEM
			ext = string(".") + temp.extension().string();
#else
			ext = string(".") + temp.extension();
#endif

			if (temp.has_parent_path())
				result = (temp.parent_path()/temp.stem()).string();
			else
#ifdef NEW_FILESYSTEM
				result = temp.stem().string();
#else
			result = temp.stem();
#endif
		}
	}

	// if nothing so far, create a data set name based on time, generator and lat/lon
	if (!result.length())
	{
		stringstream name;
		bool first = true;
		string sep = "_";

		{
			if (first)
				first = false;
			else
				name << sep;

			timeval tv;

			if (HasGenerationTime())
				tv = GetGenerationTime();
			else
				gettimeofday(&tv, NULL);

			name << ultos(tv.tv_sec, 36);
		}

		if (HasGeneratorAddress())
		{
			if (first)
				first = false;
			else
				name << sep;

			EventPropertyValue::UInt8 boardID, taskID;
			GetGeneratorAddress(boardID, taskID);
			name << utos(boardID, 16, 2) << utos(taskID, 16, 2);
		}

		if (HasGenerationLocation())
		{
			if (first)
				first = false;
			else
				name << sep;

			double lat, lon;
			GetGenerationLocation(lat, lon);

			name.precision(8);
			name << fabs(lat) << ((lat < 0) ? "N" : "W")
				 << "_";
			name.precision(8);
			name << fabs(lon) << ((lon < 0) ? "W" : "E");
		}

		result = name.str();
	}

	// trim the resulting name and ensure that it fits
	trim(result);

	if (maxFileName && (result.length() > maxFileName))
		result.resize(maxFileName);

	// reassign (or assign) the extension at this point
	if (useContentSpecificExtension)
	{
		if (isFile)
			ext = DetermineFileExtension(contentFileName);
		else
		{
			ByteArray nonFileContent;
			GetNonFileContent(nonFileContent);

			ext = DetermineFileExtension(nonFileContent);
		}
	}

	// rename the file if one already exists
	int num = -1;
	path dir(directoryName);

	if (!is_directory(dir))
	{
		dir /= result;
		result = dir.string();
	}

	else
	{
		string testfn;
		bool success = false;

		while(!success && (num < 100))
		{
			if (num >= 0)
			{

				stringstream try1;
				try1 << result << "(" << num++ << ")";

				testfn = try1.str();

				if (testfn.length() > maxFileName)
				{
					stringstream try2;
					try2 << result.substr(0, maxFileName-3) << "~" << itos(num, 10, 2);

					testfn = try2.str();
				}

				testfn += ext;
			}
			else
			{
				num = 0;
				testfn = result+ext;
			}

			path newFile = path(dir) / testfn;

			testfn = newFile.string();
			success = !exists(testfn);
		}

		if (success)
			result = testfn;
		else
			result += ext;
	}

	return result;
}

string DataSetEvent::GetOriginalFileName (const string& directoryName) const
{
	string result;						// name to be returned
	string ext;							// extension to use

	// determine if the event contains a file and get the name of the file if it exists
	if (!HasContentFileName()) {
		return "";
	}

	const string contentFileName = GetContentFileName();

	string fileName;

	// Go backwards through the string looking for find a "/".
	// Get only the filename.
	size_t pos = contentFileName.rfind ("/");
	if (pos != std::string::npos) {
		fileName = contentFileName.substr (pos);
	} else {
		fileName = "/" + contentFileName;
	}

	path dir(directoryName);
	dir /= fileName;

	return dir.string();
}

}
}
