//------------------------------------------------------------------------------
// <copyright file="HDLCPort.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>
// </summary>
//
// <owner></owner>
//------------------------------------------------------------------------------


#include "HDLCPort.h"
#include "HDLCParser.h"
#include <sstream>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/foreach.hpp>
#include "LqrSMACSerialPortSettings.h"
#include "DoThrow.h"

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

namespace LiquidRobotics {
namespace PayloadInterface {

const IHDLCPort::Timeout IHDLCPort::NO_TIMEOUT = (IHDLCPort::Timeout) 0;

static const string NoPortConfiguredMessage = "No port has been configured";

IMPLEMENT_INNER_EXCEPTION(HDLCPort, NoPortConfigured, std::runtime_error)


HDLCPort::HDLCPort(unsigned _maxLength, Logger& _logger)
	throw(invalid_argument)
: intercharacterTimeout(1000)
, receiveTimeout(NO_TIMEOUT)
, maxLength(_maxLength)
, logger(_logger)
{
	if ((maxLength < 2) || (_maxLength >= 0x8000))
		DoThrow<invalid_argument>(__FILE__, __LINE__, "maxLength range is 2..0x7FFF");
}

HDLCPort::HDLCPort(SerialPortPtr _pSerialPort, unsigned _maxLength, Logger& _logger)
	throw(invalid_argument)
: pSerialPort(_pSerialPort)
, intercharacterTimeout(1000)
, receiveTimeout(NO_TIMEOUT)
, maxLength(_maxLength)
, logger(_logger)
{
	if ((maxLength < 2) || (_maxLength >= 0x8000))
		DoThrow<invalid_argument>(__FILE__, __LINE__, "maxLength range is 2..0x7FFF");
}

HDLCPort::~HDLCPort()
{
}

void HDLCPort::FlushInput()
	throw(NoPortConfigured)
{
	if (!pSerialPort)
		DoThrow<NoPortConfigured>(__FILE__, __LINE__, NoPortConfiguredMessage);

	pSerialPort->FlushInput();
}

void HDLCPort::Receive(ByteArray& result)
	throw(NoPortConfigured)
{
	result.clear();

	if (!pSerialPort)
		throw NoPortConfigured(NoPortConfiguredMessage);

	const SerialPort::TimeSpan timeout1 =  (receiveTimeout == NO_TIMEOUT) ? SerialPort::TimeSpan_Infinite : milliseconds(receiveTimeout);
	const SerialPort::TimeSpan timeout2 = (intercharacterTimeout == NO_TIMEOUT) ? SerialPort::TimeSpan_Infinite : milliseconds(intercharacterTimeout);
	HDLCParser parser(result, maxLength);

	bool success = false;						// true if there was a block received
	bool timedOut = false;						// true if there was a timeout
	bool pass1 = true;							// first pass through?
	ByteArray dataBuffer;						// buffer that holds data

	while(!success && !timedOut)
	{
		bool readable = true;						// test for readability (flag will be set on return if readable)
		bool writeable = false;						// do not test for writeability
		const SerialPort::TimeSpan tspan = pass1 ? timeout1 : timeout2;

		logger.Debug << "HDLC waiting for readable port with timeout=" << tspan << endl;

		timedOut = !pSerialPort->WaitForPortState(tspan, readable, writeable);
		timedOut = timedOut || !readable;
		pass1 = false;

		if (!timedOut)
		{
			logger.Debug << "HDLC reading with interval timer=" << timeout2 << endl;
			pSerialPort->ReadWithIntervalTimer(dataBuffer, 2*maxLength+1, timeout2);

			//logger.Debug << "HDLC received " << dataBuffer.size() << " bytes" << endl;
			logger.Debug << "HDLC received " << dataBuffer << endl;

			const ByteArray::size_type countUsed = parser.Feed(dataBuffer);

			if (countUsed < dataBuffer.size())
			{
				logger.Debug << "HDLC unreading " << (dataBuffer.size()-countUsed) << " bytes" << endl;
				pSerialPort->Unread(dataBuffer, countUsed);
			}

			success = parser.Succeeded();
		}
	}

	if (!success && result.size())
	{
		logger.Warning << "HDLC:Receive discarding " << result.size() << " bytes because block receive was incomplete" << endl;
		result.resize(0);
	}
}

void HDLCPort::GetRawTransmitBytes(const ByteArray& bytesIn, ByteArray& bytesOut)
	throw (std::invalid_argument, NoPortConfigured)
{
	unsigned blockLength = HDLCParser::LengthFromBlock(bytesIn);

	if (blockLength != (unsigned) bytesIn.size())
		DoThrow<invalid_argument>(__FILE__, __LINE__, "data size does not match embedded block length");

	bytesOut.reserve(1+2*bytesIn.size());
	bytesOut.push_back(HDLCParser::SOF);

	BOOST_FOREACH(byte b, bytesIn)
	{
		if ((b == HDLCParser::SOF) || (b == HDLCParser::ESC))
		{
			bytesOut.push_back(HDLCParser::ESC);
			b ^= HDLCParser::ESCMask;
		}

		bytesOut.push_back(b);
	}
}

void HDLCPort::Send(const ByteArray& data)
	throw (invalid_argument, NoPortConfigured)
{
	if (!pSerialPort)
		DoThrow<NoPortConfigured>(__FILE__, __LINE__, NoPortConfiguredMessage);

	ByteArray sendBytes;
	GetRawTransmitBytes(data, sendBytes);

	SendRawBytes(sendBytes);
}

void HDLCPort::SendRawBytes(const ByteArray& data)
	throw (invalid_argument, NoPortConfigured)
{
	if (!pSerialPort)
		DoThrow<NoPortConfigured>(__FILE__, __LINE__, NoPortConfiguredMessage);

	pSerialPort->Write(data, ISerialPort::TimeSpan_Infinite);
}

unsigned HDLCPort::GetMaxLength() const
{
	return maxLength;
}

HDLCPort::Timeout HDLCPort::GetIntercharacterTimeout() const
{
	return intercharacterTimeout;
}

void HDLCPort::SetIntercharacterTimeout(Timeout newValue)
{
	intercharacterTimeout = newValue;
}

HDLCPort::Timeout HDLCPort::GetReceiveTimeout() const
{
	return receiveTimeout;
}

void HDLCPort::SetReceiveTimeout(Timeout newValue)
{
	receiveTimeout = newValue;
}

string HDLCPort::ToString() const
{
	stringstream stm;
	stm << *this;
	return stm.str();
}

ISerialPort& HDLCPort::GetSerialPort() const
{
	if (!pSerialPort)
		DoThrow<NoPortConfigured>(__FILE__, __LINE__, NoPortConfiguredMessage);

	return *pSerialPort;
}

void HDLCPort::SetSerialPort(SerialPortPtr newValue)
{
	pSerialPort = newValue;
}

bool HDLCPort::HaveSerialPort() const
{
	return !!pSerialPort;
}

ostream& operator<<(std::ostream& out, const HDLCPort& hdlcPort)
{
	out << "pSerialPort=";

	if (hdlcPort.HaveSerialPort())
	{
		SerialPortSettings settings("?", hdlcPort.GetSerialPort());

		out << settings;
	}
	else
		out << "null";

	out << " intercharacterTimeout=" << hdlcPort.GetIntercharacterTimeout()
	    << " receiveTimeout=" << hdlcPort.GetReceiveTimeout()
	    << " maxLength=" << hdlcPort.GetMaxLength();

	return out;
}

}
}
