//------------------------------------------------------------------------------
// <copyright file="EPVFormatConfig.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 EPVFormatConfig class, a serializable class entity that
//  describes how to convert an EventPropertyValue to a binary sequence.
// </summary>
//
// <owner>Mike Cookson</owner>
//------------------------------------------------------------------------------

#include "EPVFormatConfig.h"
#include <boost/algorithm/string.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <boost/archive/xml_oarchive.hpp>
#include <boost/foreach.hpp>
#include "LqrSMACGetErrorDescription.h"
#include "nsextend.h"
#include <fstream>
#include <sstream>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

using namespace std;
using namespace boost::archive;
using namespace boost::serialization;
using namespace LiquidRobotics::SMACFramework;
using namespace LiquidRobotics::SMACFramework::XmlDOM;

namespace LiquidRobotics {
namespace EventLib {

static const char* const rootName = "rootLevel";


const struct ca_table_entry { EPVFormatConfig::ContentAlgorithm ca; string str; } ca_table[] =
{
		{ EPVFormatConfig::CA_LENGTH1, 	"CA_LENGTH1" },
		{ EPVFormatConfig::CA_LENGTH2, 	"CA_LENGTH2" },
		{ EPVFormatConfig::CA_LITERAL, 	"CA_LITERAL" },
		{ EPVFormatConfig::CA_RAW,		"CA_RAW" }
};

void EPVFormatConfig::FromString(const string& str, ContentAlgorithm& result)
{
	BOOST_FOREACH(const ca_table_entry& e, ca_table)
		if (!str.compare(e.str))
		{
			result = e.ca;
			return;
		}

	stringstream msg;
	msg << "No ContentAlgorithm matching '" << str << "'";
	throw invalid_argument(msg.str());
}

string EPVFormatConfig::ToString(const ContentAlgorithm& val)
{
	BOOST_FOREACH(const ca_table_entry& e, ca_table)
		if (val == e.ca)
			return e.str;

	stringstream msg;
	msg << "No ContentAlgorithm matching " << (int) val;
	throw invalid_argument(msg.str());
}

const struct endianness_table_entry { EPVFormatConfig::Endianness end; string str; } endianness_table[] =
{
		{ EPVFormatConfig::ENDIAN_DFLT, 	"ENDIAN_DFLT" },
		{ EPVFormatConfig::ENDIAN_BIG,		"ENDIAN_BIG" },
		{ EPVFormatConfig::ENDIAN_LITTLE,	"ENDIAN_LITTLE" }
};

void EPVFormatConfig::FromString(const string& str, Endianness& result)
{
	BOOST_FOREACH(const endianness_table_entry& e, endianness_table)
		if (!str.compare(e.str))
		{
			result = e.end;
			return;
		}

	stringstream msg;
	msg << "No Endianness matching '" << str << "'";
	throw invalid_argument(msg.str());
}

string EPVFormatConfig::ToString(const Endianness& val)
{
	BOOST_FOREACH(const endianness_table_entry& e, endianness_table)
		if (val == e.end)
			return e.str;

	stringstream msg;
	msg << "No Endianness matching " << (int) val;
	throw invalid_argument(msg.str());
}

EPVFormatConfig::Endianness EPVFormatConfig::DetectEndianness()
{
	unsigned short tempVal = 1;
	const byte* pByte = (const byte*) &tempVal;
	return *pByte ? ENDIAN_LITTLE : ENDIAN_BIG;
}

EPVFormatConfig::EPVFormatConfig(IObjectBroker& _broker, ContentAlgorithm _ca)
: broker(_broker)
, contentAlgorithm(_ca)
, literalContentAlgorithm(CA_RAW)
, endianness(ENDIAN_DFLT)
//, literal
, paddingEnabled(false)
, fieldPadding(0)
, fieldPaddingValue(0)
, truncationEnabled(false)
, fieldTruncation(0)
{
}

EPVFormatConfig::EPVFormatConfig(IObjectBroker& _broker, ContentAlgorithm _ca, size_t _padding, size_t _truncation)
: broker(_broker)
, contentAlgorithm(_ca)
, literalContentAlgorithm(CA_RAW)
, endianness(ENDIAN_DFLT)
//, literal
, paddingEnabled(((int) _padding) > 0)
, fieldPadding(_padding)
, fieldPaddingValue(0)
, truncationEnabled(((int) _truncation) > 0)
, fieldTruncation(_truncation)
{
}

EPVFormatConfig::EPVFormatConfig(IObjectBroker& _broker, const EventPropertyValue& _literal)
: broker(_broker)
, contentAlgorithm(CA_LITERAL)
, literalContentAlgorithm(CA_RAW)
, endianness(ENDIAN_DFLT)
//, literal
, paddingEnabled(false)
, fieldPadding(0)
, fieldPaddingValue(0)
, truncationEnabled(false)
, fieldTruncation(0)
{
	literal.reset(new EventPropertyValue(_literal));
}

EPVFormatConfig::EPVFormatConfig(const EPVFormatConfig& other)
: broker(other.broker)
, contentAlgorithm(other.contentAlgorithm)
, literalContentAlgorithm(other.literalContentAlgorithm)
, endianness(other.endianness)
//, literal
, paddingEnabled(other.paddingEnabled)
, fieldPadding(other.fieldPadding)
, fieldPaddingValue(other.fieldPaddingValue)
, truncationEnabled(other.truncationEnabled)
, fieldTruncation(other.fieldTruncation)
{
	if (other.literal)
		literal.reset(new EventPropertyValue(*other.literal));
}

EPVFormatConfig::~EPVFormatConfig()
{
}

EPVFormatConfig& EPVFormatConfig::operator=(const EPVFormatConfig& other)
{
	if (this != &other)
	{
		contentAlgorithm = other.contentAlgorithm;
		literalContentAlgorithm = other.literalContentAlgorithm;
		endianness = other.endianness;
		literal.reset();
		if (other.literal) literal.reset(new EventPropertyValue(*other.literal));
		paddingEnabled = other.paddingEnabled;
		fieldPadding = other.fieldPadding;
		fieldPaddingValue = other.fieldPaddingValue;
		truncationEnabled = other.truncationEnabled;
		fieldTruncation = other.fieldTruncation;
	}

	return *this;
}

bool EPVFormatConfig::operator==(const EPVFormatConfig& other) const
{
	bool equal =
		(contentAlgorithm == other.contentAlgorithm)
	 && (literalContentAlgorithm == other.literalContentAlgorithm)
	 && (endianness == other.endianness)
	 && (!literal == !other.literal);

	if (equal && literal)
		equal = (*literal == *other.literal);

	if (equal)
		equal = (paddingEnabled == other.paddingEnabled)
			 && (fieldPadding == other.fieldPadding)
			 && (fieldPaddingValue == other.fieldPaddingValue)
			 && (truncationEnabled == other.truncationEnabled)
			 && (fieldTruncation == other.fieldTruncation);

	return equal;
}

EPVFormatConfig::ContentAlgorithm EPVFormatConfig::GetContentAlgorithm() const
{
	return contentAlgorithm;
}

EPVFormatConfig::ContentAlgorithm EPVFormatConfig::GetLiteralContentAlgorithm() const
{
	return literalContentAlgorithm;
}

void EPVFormatConfig::SetContentAlgorithm(ContentAlgorithm newValue, ContentAlgorithm newLiteralContentValue)
{
	if ((contentAlgorithm != newValue) || (literalContentAlgorithm != newLiteralContentValue))
	{
		if ((contentAlgorithm != CA_LITERAL) && (newLiteralContentValue != CA_RAW))
			throw runtime_error("Cannot set a non-default literal content value for non-literals");

		if (newLiteralContentValue == CA_LITERAL)
			throw runtime_error("Cannot set the literal content type to 'literal'");

		contentAlgorithm = newValue;
		literalContentAlgorithm = newLiteralContentValue;

		if (contentAlgorithm == CA_LITERAL)
			literal.reset(new EventPropertyValue(broker));
	}
}

EPVFormatConfig::Endianness EPVFormatConfig::GetEndianness() const
{
	return endianness;
}

void EPVFormatConfig::SetEndianness(Endianness newValue)
{
	endianness = newValue;
}

const EventPropertyValue& EPVFormatConfig::GetLiteral() const
{
	if (!literal)
		throw runtime_error("This EPVFormatConfig has no literal");

	return *literal;
}

void EPVFormatConfig::SetLiteral(const EventPropertyValue& newValue)
{
	if (contentAlgorithm != CA_LITERAL)
		throw runtime_error("This EPVFormatConfig is not configured for literals");

	*literal = newValue;
}

bool EPVFormatConfig::PaddingEnabled() const
{
	return paddingEnabled;
}

void EPVFormatConfig::ClearPadding()
{
	paddingEnabled = false;
}

size_t EPVFormatConfig::GetFieldPadding() const
{
	if (!paddingEnabled)
		throw runtime_error("(GetFieldPadding) This EPVFormatConfig is not configured for field padding");

	return fieldPadding;
}

EPVFormatConfig::byte EPVFormatConfig::GetFieldPaddingValue() const
{
	if (!paddingEnabled)
		throw runtime_error("(GetFieldPaddingValue) This EPVFormatConfig is not configured for field padding");

	return fieldPaddingValue;
}

void EPVFormatConfig::SetFieldPadding(size_t newValue)
{
	paddingEnabled = true;
	fieldPadding = newValue;
}

void EPVFormatConfig::SetFieldPadding(size_t newValue, size_t newPaddingValue)
{
	paddingEnabled = true;
	fieldPadding = newValue;
	fieldPaddingValue = newPaddingValue;
}

void EPVFormatConfig::SetFieldPaddingValue(byte newValue)
{
	if (!paddingEnabled)
		throw runtime_error("(SetFieldPaddingValue) This EPVFormatConfig is not configured for field padding");

	fieldPaddingValue = newValue;
}

bool EPVFormatConfig::TruncationEnabled() const
{
	return truncationEnabled;
}

void EPVFormatConfig::ClearTruncation()
{
	truncationEnabled = false;
}

size_t EPVFormatConfig::GetFieldTruncation() const
{
	if (!truncationEnabled)
		throw runtime_error("(GetFieldTruncation) This EPVFormatConfig is not configured for field truncation");

	return fieldTruncation;
}

void EPVFormatConfig::SetFieldTruncation(size_t newValue)
{
	truncationEnabled = true;
	fieldTruncation = newValue;
}

void EPVFormatConfig::Load(const string& fileName)
{
	// get information about the archive
	struct stat statBuf;
	int status = stat(fileName.c_str(), &statBuf);
	int errSave = errno;

	// if the archive exists ...
	if (!status)
	{
		// if the file length is zero, then create a temporary instance and overwrite the archive
		if (!statBuf.st_size)
		{
			EPVFormatConfig temp(broker);
			temp.Save(fileName);
		}

		// create a read stream from the archive
		ifstream ifs(fileName.c_str());

		// if the read stream is no good then throw invalid argument exception
		if (!ifs.good())
		{
			stringstream msg;
			msg << "Loading " << typeid(*this).name() << ": could not open '" << fileName << "' for read";
			throw invalid_argument(msg.str());
		}

		xml_iarchive ia(ifs);						// construct an XML input archive based on the stream
		ia >> make_nvp(rootName, *this);			// read the root level item into the entity instance
	}
	else
	{
		// format the message content and throw the exception
		stringstream msg;
		msg << "Loading " << typeid(*this).name() << ": could not stat '" << fileName << "'"
		    << " (errno=" << errSave << " '" << GetErrorDescription(errSave) << "')";
		throw invalid_argument(msg.str());
	}
}

void EPVFormatConfig::Load(XmlNode& node, const XmlNVPBase& nvp)
{
	XmlAttribute contentAttr = GetAttribute("content", node);;
	XmlAttribute literalContentAttr = GetAttribute("literalContent", node, false);
	XmlAttribute endiannessAttr = GetAttribute("endian", node, false);
	XmlAttribute paddingAttr = GetAttribute("padding", node, false);
	XmlAttribute truncationAttr = GetAttribute("truncate", node, false);

	if (!contentAttr)
		throw runtime_error("EPVFormatConfig::Load did not find expected attribute in tag");

	FromString(contentAttr.value(), contentAlgorithm);

	literalContentAlgorithm = CA_RAW;

	if (literalContentAttr)
		FromString(literalContentAttr.value(), literalContentAlgorithm);

	if (contentAlgorithm == CA_LITERAL)
	{
		if (!literal)
			literal.reset( new EventPropertyValue(broker) );

		node >> XmlNVP<EventPropertyValue>("literal", *literal);
		CheckLiteralType();
	}
	else
		literal.reset();

	if (endiannessAttr)
		FromString(endiannessAttr.value(), endianness);
	else
		endianness = ENDIAN_DFLT;

	if (paddingAttr)
	{
		paddingEnabled = true;
		fieldPadding = paddingAttr.as_uint();
	}
	else
		paddingEnabled = false;

	if (truncationAttr)
	{
		truncationEnabled = true;
		fieldTruncation = truncationAttr.as_uint();
	}
	else
		truncationEnabled = false;
}

void EPVFormatConfig::Save(const string& fileName) const
{
	// create a write stream to the archive
	ofstream ofs(fileName.c_str());

	// if the write stream is no good then throw invalid argument exception
	if (!ofs.good())
	{
		stringstream msg;
		msg << "Saving " << typeid(*this).name() << ": could not open '" << fileName << "' for write";
		throw invalid_argument(msg.str());
	}

	xml_oarchive oa(ofs);						// construct an XML output archive based on the stream
	oa << make_nvp(rootName, *this);			// write the root level using the entity
}

void EPVFormatConfig::Save(XmlNode& node, const XmlNVPBase& nvp) const
{
	string contentStr = ToString(contentAlgorithm);
	SaveToNode(node, XmlNVPBase("content", "content"), contentStr);

	if ((contentAlgorithm == CA_LITERAL) && (literalContentAlgorithm != CA_RAW))
	{
		string literalContentStr = ToString(literalContentAlgorithm);
		SaveToNode(node, XmlNVPBase("literalContent", "literalContent"), literalContentStr);
	}

	//node << XmlNVP<string>("content", contentStr);

	if (contentAlgorithm == CA_LITERAL)
	{
		if (!literal)
			throw runtime_error("EPVFormatConfig::Save expected to find a valid literal pointer");

		node << XmlNVP<EventPropertyValue>("literal", *literal);
	}

	if (endianness != ENDIAN_DFLT)
	{
		string endiannessStr = ToString(endianness);
		SaveToNode(node, XmlNVPBase("endian", "endian"), endiannessStr);
		//node << XmlNVP<string>("endian", endiannessStr);
	}

	if (paddingEnabled)
		SaveToNode(node, XmlNVPBase("padding", "padding"), fieldPadding);
		//node << XmlNVP<size_t>("padding", fieldPadding);

	if (truncationEnabled)
		SaveToNode(node, XmlNVPBase("truncate", "truncate"), fieldTruncation);
		//node << XmlNVP<size_t>("truncate", fieldTruncation);
}

void EPVFormatConfig::CheckLiteralType()
{
	if (literalContentAlgorithm == CA_LITERAL)
		throw runtime_error("Contained literal type cannot also be literal");

	if (contentAlgorithm == CA_LITERAL)
	{
		if (!literal)
			throw runtime_error("No literal present for literal type");

		const EventPropertyValue::StorageType stype = literal->GetStorageType();

		switch(stype)
		{
		case EventPropertyValue::ST_UINT8:
		case EventPropertyValue::ST_INT8:
		case EventPropertyValue::ST_UINT16:
		case EventPropertyValue::ST_INT16:
		case EventPropertyValue::ST_UINT32:
		case EventPropertyValue::ST_INT32:
		case EventPropertyValue::ST_SINGLE_FLOAT:
		case EventPropertyValue::ST_DOUBLE_FLOAT:
		case EventPropertyValue::ST_STRING:
		case EventPropertyValue::ST_BOOLEAN:
		case EventPropertyValue::ST_UINT8_VECTOR:
		case EventPropertyValue::ST_INT8_VECTOR:
		case EventPropertyValue::ST_UINT16_VECTOR:
		case EventPropertyValue::ST_INT16_VECTOR:
		case EventPropertyValue::ST_UINT32_VECTOR:
		case EventPropertyValue::ST_INT32_VECTOR:
		case EventPropertyValue::ST_SINGLE_FLOAT_VECTOR:
		case EventPropertyValue::ST_DOUBLE_FLOAT_VECTOR:
		case EventPropertyValue::ST_STRING_VECTOR:
		case EventPropertyValue::ST_BOOLEAN_VECTOR:

			break;

		default:
		{
			stringstream msg;
			msg << "EPVFormatConfig does not support literal type " << EventPropertyValue::DescribeStorageType(stype);

			throw runtime_error(msg.str());
		}

		}
	}
}

std::ostream& operator<<(std::ostream& stm, const EPVFormatConfig& settings)
{
	stm << "contentAlgorithm=" << EPVFormatConfig::ToString(settings.GetContentAlgorithm())
	    << " endianness=" << EPVFormatConfig::ToString(settings.GetEndianness());

	if (settings.GetContentAlgorithm() == EPVFormatConfig::CA_LITERAL)
		stm << " literal={ " << settings.GetLiteral() << " }"
	        << " literalContent=" << EPVFormatConfig::ToString(settings.GetLiteralContentAlgorithm());

	stm << " paddingEnabled=" << settings.PaddingEnabled()
	    << " fieldPadding=" << settings.GetFieldPadding()
	    << " fieldPaddingValue=0x" << utos(settings.GetFieldPaddingValue(), 16, 2)
	    << " truncationEnabled=" << settings.TruncationEnabled()
	    << " fieldTruncation=" << settings.GetFieldTruncation();

	return stm;
}

}
}
