//----------------------------------------------------------------------------------
// <copyright file="SBDModem.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 SBDModem class, which sends messages too and
//   receives messages from an Iridium SBD modem (9601, 9602 or 9522B)  Applications
///  can use this class to implement communication scenarios.
// </summary>
//
// <owner>Mike Cookson</owner>
//----------------------------------------------------------------------------------

#include "SBDModem.h"
#include "SBDModemSettings.h"
#include "LqrSMACBooleanParser.h"
#include "LqrSMACTimeCounter.h"
#include "nsextend.h"
#include <sstream>
#include <stdexcept>
#include <boost/algorithm/string.hpp>
#include <boost/date_time/gregorian/gregorian.hpp>
#include <boost/make_shared.hpp>
#include <ctype.h>

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

namespace LiquidRobotics {
namespace PPPHostLib {

IMPLEMENT_INNER_EXCEPTION(SBDModem, AccessDenied, std::runtime_error);
IMPLEMENT_INNER_EXCEPTION(SBDModem, ActionFailed, std::runtime_error);
IMPLEMENT_INNER_EXCEPTION(SBDModem, BusyTryLater, std::runtime_error);
IMPLEMENT_INNER_EXCEPTION(SBDModem, NetworkError, std::runtime_error);
IMPLEMENT_INNER_EXCEPTION(SBDModem, ProtocolError, std::runtime_error);
IMPLEMENT_INNER_EXCEPTION(SBDModem, ProtocolTimeout, std::runtime_error);

SBDModem::SBDModem(Logger& _logger, SBDModemSettings& _settings)
: initialized(false)
, logger(_logger)
, settings(_settings)
{
}

SBDModem::~SBDModem()
{
	// TODO: finish this
}

void SBDModem::Intialize()
{
	LockableConceptMutex::Lock lock(mutex);
	LockableConceptMutex::Lock cmdLock(commandMutex);

	if (initialized)
		return;

	try
	{
		serialPort = settings.GetSerialPortSettings().CreatePort(logger);
		serialListener.reset(new SerialListener(*serialPort, settings.GetSerialListenerSettings()));
		listenerSubscription = serialListener->Subscribe(boost::bind(&SBDModem::OnSerialReceive, this, _1, _2));
		serialListener->Initialize();
		initialized = true;
	}
	catch(exception& ex)
	{
		listenerSubscription.reset();
		serialListener.reset();

		if (serialPort)
		{
			serialPort->Close();
			serialPort.reset();
		}

		throw;
	}

	// sending 10 carriage returns at 100ms intervals to teach the autobaud mechanism, if it's enabled
	TimeCounter tc;
	const unsigned msAmount = 100;
	const string cr = "\r";
	unsigned msto = 0;
	for (unsigned countDown = 10; countDown--; )
	{
		msto += msAmount;
		serialPort->Write(cr);
		unsigned remain;
		do
		{
			remain = tc.Remaining(msto);
			if (remain)
				usleep(1000*remain);
		} while(remain);
	}
}

void SBDModem::Shutdown()
{
	SerialListener::Ptr sl = serialListener;

	if (sl)
	{
		ILockableConcept::Lock firingLock(sl->FiringMutex());
		listenerSubscription.reset();
		firingLock.unlock();
	}

	LockableConceptMutex::Lock lock(mutex);
	sl = serialListener;

	if (sl)
	{
		try { sl->Shutdown(); }
		catch(...) { }

		sl.reset();
	}

	SerialPortPtr sp = serialPort;

	if (sp)
	{
		serialPort.reset();
		sp.reset();
	}

	initialized = false;
}

void SBDModem::ClearMOMSN()
{
    Logger::Locked(logger)->Info << "SBDModem::ClearMOSN" << endl;
    SendMessageWithStatusResponse("AT+SBDC", settings.GetAckTimeoutMS());
}

inline int GetDeleteType(bool moBuffer, bool mtBuffer)
{
    int result;

    if (moBuffer)
    {
    	if (mtBuffer)
    		result = 2;
    	else
    		result = 0;
    }
    else if (mtBuffer)
    	result = 1;
    else
    	result = -1;			// No valid delete type if moBuffer and mtBuffer are both false

    return result;
}

void SBDModem::ClearSBDBuffers(bool moBuffer, bool mtBuffer)
{
    Logger::Locked(logger)->Info << "SBDModem::ClearSBDBuffers moBuffer=" << moBuffer << " mtBuffer=" << mtBuffer << endl;

    const int deleteType = GetDeleteType(moBuffer, mtBuffer);

    if (deleteType >= 0)
    {
    	stringstream ss;
    	ss << "AT+SBDD" << deleteType;

    	const string s = ss.str();

        string responseData;
        SendMessageExpectTokenWithStatus(s, false, responseData, settings.GetAckTimeoutMS());

        unsigned responseCode;

        try
        {
            responseCode = boost::lexical_cast<unsigned>(responseData);
        }
        catch(...)
        {
            stringstream stm;
            stm << "SBDModem::ClearSBDBuffers: could not convert response code (" << responseData << ") to integer";
            const string msg = stm.str();
            Logger::Locked(logger)->Error << msg << endl;
            throw ProtocolError(msg);
        }

        if (responseCode != 0)
        {
            stringstream stm;
            stm << "SBDModem::ClearSBDBuffers: received non-zero response code (" << responseCode << "), indicating error";
            const string msg = stm.str();
            Logger::Locked(logger)->Warning << msg << endl;
            throw ActionFailed(msg);
        }
    }
    else
    	throw invalid_argument("SBDModem::ClearSBDBuffers: moBuffer or mtBuffer must be true");
}

void SBDModem::ConfigureInterface()
{
    Logger::Locked(logger)->Info << "SBDModem::ConfigureInterface" << endl;
    SendMessageWithStatusResponse("ATE0V1", 3*settings.GetAckTimeoutMS());      // using extra long timeout since this is executed during startup
}

SBDModem::TokenPtr SBDModem::AwaitASCIIToken(TimeCounter& tc, unsigned timeoutMS, TokenPtrQueue::Ptr tpq, bool lastTry, bool throwOnError)
{
	TokenPtr result = AwaitToken(tc, timeoutMS, tpq, lastTry, throwOnError);

	if (result && result->binary)
	{
		stringstream ss;
		ss << "SBDModem::AwaitASCIIToken: expected ASCII token, but received a binary token";

		const string s = ss.str();

		if (lastTry && throwOnError)
			throw ProtocolError(s);
		else
			result.reset();
	}

	return result;
}

SBDModem::TokenPtr SBDModem::AwaitBinaryToken(TimeCounter& tc, unsigned timeoutMS, TokenPtrQueue::Ptr tpq, bool lastTry, bool throwOnError)
{
	TokenPtr result = AwaitToken(tc, timeoutMS, tpq, lastTry, throwOnError);

	if (result && !result->binary)
	{
		stringstream ss;
		ss << "SBDModem::AwaitBinaryToken: expected binary token, but received an ASCII token";

		const string s = ss.str();

		if (lastTry && throwOnError)
			throw ProtocolError(s);
		else
			result.reset();
	}

	return result;
}

SBDModem::TokenPtr SBDModem::AwaitToken(TimeCounter& tc, unsigned timeoutMS, TokenPtrQueue::Ptr tpq, bool lastTry, bool throwOnError)
{
	const unsigned remaining = tc.Remaining(timeoutMS);
	TokenPtr result;

	if (!remaining)
	{
		string s = "SBDModem::AwaitToken: no time remaining";

		Logger::Locked(logger)->Warning << s << endl;

		if (lastTry && throwOnError)
			throw ProtocolTimeout(s);

		return result;
	}

    if (!tpq->ReadTest(result, remaining))
    {
    	string s = "SBDModem::AwaitToken: timeout while waiting for token";

    	Logger::Locked(logger)->Warning << s << endl;

		if (lastTry && throwOnError)
			throw ProtocolTimeout(s);

		return result;
    }

    return result;
}

bool SBDModem::Send(SerialPort::Ptr sp, MessageFormatter cmd, bool lastTry, bool throwOnError)
{
	try
	{
		Logger::Locked(logger)->Debug << "Writing command " << cmd.ToString() << endl;

		stringstream ss;
        ss << (const string&) cmd << "\r";

        const string s = ss.str();

        sp->Write(s);
	}
	catch(exception& ex)
	{
        stringstream ss;
        ss << "Caught " << typeid(ex).name() << " '" << ex.what() << "' during serial write";

        const string s = ss.str();

        Logger::Locked(logger)->Error << s << endl;

        if (lastTry && throwOnError)
        	throw runtime_error(s);
        else
        	return false;
	}

	return true;
}

bool SBDModem::SendMessageWithStatusResponse(MessageFormatter cmd, unsigned timeoutMS, bool throwOnError)
{
    ILockableConcept::Lock lock(commandMutex);
    SerialPortPtr sp = serialPort;
    TokenPtrQueueHolder holder(readResults);

    if (!sp)
    {
    	if (throwOnError)
    		throw runtime_error("Serial port is nil");
    	else
    		return false;
    }

    bool good = false;

    for (unsigned triesLeft = settings.GetMaxTries(); !good && triesLeft-- > 0; )
    {
    	if (!Send(sp, cmd, !triesLeft, throwOnError))
    		return false;

    	TimeCounter tc;
    	TokenPtr token = AwaitASCIIToken(tc, timeoutMS, holder.tpq, !triesLeft, throwOnError);

        if (token)
        {
            if (boost::equals(token->data, "OK"))
                good = true;
            else
            {
                const bool isError = boost::equals(token->data, "ERROR");
                stringstream ss;
                ss << "SBDModem::SendMessageWithStatusResponse: received " << (isError ? "error" : "unexpected") << " response (" << token->data << ")";
                string s = ss.str();
                Logger::Locked(logger)->Warning << s << endl;
                if (!triesLeft && throwOnError) throw ProtocolError(s);
            }
        }
    }

    if (!good)
    {
        stringstream ss;
        ss << "SBDModem::SendMessageWithStatusResponse: timeout in response to " << cmd.ToString() << " after " << settings.GetMaxTries() << " attempts";
        string s = ss.str();
        Logger::Locked(logger)->Warning << s << endl;
        if (throwOnError)
                throw ProtocolTimeout(s);
    }

    return good;
}

bool SBDModem::SendMessageExpectTokenWithStatus(MessageFormatter cmd, bool tokenBinary, string& response, unsigned timeoutMS, bool throwOnError)
{
    ILockableConcept::Lock lock(commandMutex);
    SerialPortPtr sp = serialPort;
    TokenPtrQueueHolder holder(readResults);

    if (!sp)
    {
    	if (throwOnError)
    		throw runtime_error("Serial port is nil");
    	else
    		return false;
    }

    bool good = false;

    for (unsigned triesLeft = settings.GetMaxTries(); !good && triesLeft-- > 0; )
    {
    	if (!Send(sp, cmd, !triesLeft, throwOnError))
    		return false;

    	TimeCounter tc;
    	TokenPtr token1;
	if (tokenBinary)
		token1 = AwaitBinaryToken(tc, timeoutMS, holder.tpq, !triesLeft, throwOnError);
	else
		token1 = AwaitASCIIToken(tc, timeoutMS, holder.tpq, !triesLeft, throwOnError);

	if (token1)
    	{
    		response = token1->data;
			tc.Reset(0, true);

			TokenPtr token2 = AwaitASCIIToken(tc, timeoutMS, holder.tpq, !triesLeft, throwOnError);

			if (token2)
			{
		    	if (boost::equals(token2->data, "OK"))
		    		good = true;
		    	else
		    	{
					const bool isError = boost::equals(token2->data, "ERROR");
					stringstream ss;
					ss << "SBDModem::SendMessageExpectStringWithStatus: received " << (isError ? "error" : "unexpected") << " response (" << token2->data << ")";
					string s = ss.str();
					Logger::Locked(logger)->Warning << s << endl;
					if (!triesLeft && throwOnError) throw ProtocolError(s);
		    	}
			}
    	}
    }

    if (!good)
    {
        stringstream ss;
        ss << "SBDModem::SendMessageExpectStringWithStatus: timeout in response to " << cmd.ToString() << " after " << settings.GetMaxTries() << " attempts";
        string s = ss.str();
        Logger::Locked(logger)->Warning << s << endl;
        if (throwOnError)
                throw ProtocolTimeout(s);
    }

    return good;
}

bool SBDModem::SendMessageExpectStringListTerminatedByStatus(MessageFormatter cmd, StringArray& responses,	unsigned timeoutMS, bool throwOnError)
{
    ILockableConcept::Lock lock(commandMutex);
    SerialPortPtr sp = serialPort;
    TokenPtrQueueHolder holder(readResults);

    if (!sp)
    {
    	if (throwOnError)
    		throw runtime_error("Serial port is nil");
    	else
    		return false;
    }

    bool good = false;

    for (unsigned triesLeft = settings.GetMaxTries(); !good && triesLeft-- > 0; )
    {
    	responses.clear();
    	responses.reserve(100);

    	if (!Send(sp, cmd, !triesLeft, throwOnError))
    		return false;

    	TimeCounter tc;
    	TokenPtr token;
    	bool err = false;
    	bool full = false;

    	while(tc.Reset(0,true),
    		  token = AwaitASCIIToken(tc, timeoutMS, holder.tpq, !triesLeft, throwOnError),
    		  good = token && boost::equals(token->data, "OK"),
    		  err = !good && token && boost::equals(token->data, "ERROR"),
    		  token && !good && !err)
    	{
    		if (!!(full = responses.size() >= responses.capacity()))
    			break;
    		else
				responses.push_back(token->data);
    	}

    	if (err || full)
    	{
    		stringstream ss;

    		if (err)
    			ss << "SBDModem::SendMessageExpectStringListTerminatedByStatus: received failing status";
    		else
    			ss << "SBDModem::SendMessageExpectStringListTerminatedByStatus: failed to store input string (" << token->data << ") due to buffer overflow";

			const string s = ss.str();
			Logger::Locked(logger)->Warning << s << endl;

			if ((full || !triesLeft) && throwOnError)
				throw ProtocolError(s);
			else if (full)
				triesLeft = 0;
    	}
    }

    if (!good)
    {
        stringstream ss;
        ss << "SBDModem::SendMessageExpectStringListTerminatedByStatus: timeout in response to " << cmd.ToString() << " after " << settings.GetMaxTries() << " attempts";
        string s = ss.str();
        Logger::Locked(logger)->Warning << s << endl;
        if (throwOnError)
                throw ProtocolTimeout(s);
    }

    return good;
}

bool SBDModem::SendMessageExpectString2TerminatedByStatus(MessageFormatter cmd, const string& expected, MessageFormatter cmd2, unsigned timeoutMS, unsigned timeout2MS, bool throwOnError)
{
    ILockableConcept::Lock lock(commandMutex);
    SerialPortPtr sp = serialPort;
    TokenPtrQueueHolder holder(readResults);

    if (!sp)
    {
    	if (throwOnError)
    		throw runtime_error("Serial port is nil");
    	else
    		return false;
    }

    bool good = false;

    for (unsigned triesLeft = settings.GetMaxTries(); !good && triesLeft-- > 0; )
    {
    	if (!Send(sp, cmd, !triesLeft, throwOnError))
			break;

    	TimeCounter tc;
    	TokenPtr token = AwaitASCIIToken(tc, timeoutMS, holder.tpq, !triesLeft, throwOnError);

    	if (token)
    	{
    		if (boost::equals(token->data, expected))
    		{
    			if (!Send(sp, cmd2, !triesLeft, throwOnError))
    				break;

    			tc.Reset(0,true);
    			TokenPtr token2 = AwaitASCIIToken(tc, timeoutMS, holder.tpq, !triesLeft, throwOnError);
                TokenPtr token3;

                if (token2)
                {
                    token3 = AwaitASCIIToken(tc, timeout2MS, holder.tpq, !triesLeft, throwOnError);

                    unsigned code;
                    bool gotCode;

                    try { code = boost::lexical_cast<unsigned>(token2->data); gotCode = true; }
                    catch(...) { gotCode = false; }

                    if (!gotCode)
                    {
                        stringstream ss;
                        ss << "SBDModem::SendMessageExpectString2TerminatedByStatus: received code (" << token2->data << ") could not be converted to integer";
                        string s = ss.str();
                        Logger::Locked(logger)->Warning << s << endl;
                        if (!triesLeft && throwOnError) throw ProtocolError(s);
                    }
                    else if (code != 0)
                    {
                        stringstream ss;
                        ss << "SBDModem::SendMessageExpectString2TerminatedByStatus: received code " << code;

                        switch(code)
                        {
                        case 1: ss << " (SBD write timeout)"; break;
                        case 2: ss << " (bad checksum)"; break;
                        case 3: ss << " (size out of range)"; break;
                        default: ss << " (unknown code)"; break;
                        }

                        string s = ss.str();
                        Logger::Locked(logger)->Warning << s << endl;

                        if (!triesLeft && throwOnError)
                            switch(code)
                            {
                            case 1: throw ProtocolTimeout(s);
                            default: throw ProtocolError(s);
                            }
                    }

    				if (boost::equals(token3->data, "OK"))
    					good = true;
    				else
    				{
    					const bool isError = boost::equals(token->data, "ERROR");
    					stringstream ss;
    					ss << "SBDModem::SendMessageExpectString2TerminatedByStatus: received " << (isError ? "error" : "unexpected") << " response (" << token3->data << ")";
    					string s = ss.str();
    					Logger::Locked(logger)->Warning << s << endl;
    					if (!triesLeft && throwOnError) throw ProtocolError(s);
    				}
                }
    		}
    	}
    }

    if (!good)
    {
        stringstream ss;
        ss << "SBDModem::SendMessageExpectString2TerminatedByStatus: timeout in response to " << cmd.ToString() << " after " << settings.GetMaxTries() << " attempts";
        string s = ss.str();
        Logger::Locked(logger)->Warning << s << endl;
        if (throwOnError)
            throw ProtocolTimeout(s);
    }

    return good;
}

SBDModem::DTERate SBDModem::GetDTERate()
{
    Logger::Locked(logger)->Info << "SBDModem::GetDTERate" << endl;
    string response;

    SendMessageExpectTokenWithStatus("AT+IPR?", false, response, settings.GetAckTimeoutMS());

    DTERate result(response);
    Logger::Locked(logger)->Info << "SBDModem::GetDTERate: " << result << endl;
    return result;
}

void SBDModem::SetDTERate(const DTERate& newRate)
{
    Logger::Locked(logger)->Info << "SBDModem::SetDTERate rateID=" << newRate.rateID << " autoBaud=" << newRate.autoBaud << endl;
    string response;

    SendMessageExpectTokenWithStatus("AT+IPR?", false, response, settings.GetAckTimeoutMS());

    stringstream ss;
    ss << "AT+IPR=" << newRate.rateID;

    if (response.find(',') != string::npos)
    	ss << "," << (newRate.autoBaud ? 1 : 0);

    const string s = ss.str();
    SendMessageWithStatusResponse(s, settings.GetAckTimeoutMS());
}

SBDModem::IndicatorEventReporting SBDModem::GetIndicatorEventReporting()
{
	Logger::Locked(logger)->Info << "SBDModem::GetIndicatorEventReporting" << endl;
	string responseData;
	SendMessageExpectTokenWithStatus("AT+CIER?", false, responseData, settings.GetAckTimeoutMS());

	if (!boost::starts_with(responseData, "+CIER:"))
	{
		stringstream stm;
		stm << "SBDModem::GetIndicatorEventReporting: AT+CIER? response (" << responseData << ") was expected to start with +CIER:";
		string msg = stm.str();
		throw ProtocolError(msg);
	}

	const string tail = responseData.substr(6);
	IndicatorEventReporting response(tail);
	return response;
}

void SBDModem::SetIndicatorEventReporting(const IndicatorEventReporting& newSettings)
{
	Logger::Locked(logger)->Info << "SBDModem::SetIndicatorEventReporting " << newSettings.ToString() << endl;
	string responseData;
	SendMessageExpectTokenWithStatus("AT+CIER?", false, responseData, settings.GetAckTimeoutMS());

	if (!boost::starts_with(responseData, "+CIER:"))
	{
		stringstream stm;
		stm << "SBDModem::SetIndicatorEventReporting: AT+CIER? response (" << responseData << ") was expected to start with +CIER:";
		string msg = stm.str();
		throw ProtocolError(msg);
	}

	const string tail = responseData.substr(6);
	IndicatorEventReporting currentSettings(tail);

	IndicatorEventReporting newSettings2 = newSettings;
	newSettings2.haveAntennaFaultSetting &= newSettings.haveAntennaFaultSetting;
	newSettings2.reportAntennaFault &= newSettings2.haveAntennaFaultSetting;

	stringstream stm;
	stm << "AT+CIER=" << newSettings2.ToDataString();
	const string msg = stm.str();
	SendMessageWithStatusResponse(msg, settings.GetAckTimeoutMS());
}

SBDModem::NetworkRegistration SBDModem::GetNetworkRegistration()
{
	Logger::Locked(logger)->Info << "SBDModem::GetNetworkRegistration" << endl;

	string responseData;

        {
            AwaitingTagHolder("+CREG:", tag, tagProtector);
            SendMessageExpectTokenWithStatus("AT+CREG?", false, responseData, settings.GetAckTimeoutMS());
        }

	if (!boost::starts_with(responseData, "+CREG:"))
	{
		stringstream stm;
		stm << "SBDModem::GetIndicatorEventReporting: AT+CREG? response (" << responseData << ") was expected to start with +CIER:";
		string msg = stm.str();
		throw ProtocolError(msg);
	}

	const string tail = responseData.substr(6);
	NetworkRegistration response(tail);
	Logger::Locked(logger)->Info << "SBDModem::GetNetworkRegistration: " << response.ToString() << endl;
	return response;
}

void SBDModem::SetNetworkRegistration(unsigned notification)
{
	Logger::Locked(logger)->Info << "SBDModem::SetNetworkRegistration notification=" << notification << endl;
        stringstream cmdstm;
        cmdstm << "AT+CREG=" << notification;
        string cmd = cmdstm.str();
	SendMessageWithStatusResponse(cmd, settings.GetAckTimeoutMS());
}

inline bool AllDigits(const string& seq)
{
	size_t len = 0;
	bool allDigits = true;

	for (string::const_iterator ci = seq.begin(); allDigits && ci != seq.end(); ++ci)
	{
		allDigits = isdigit(*ci);
		++len;
	}

	return allDigits && !!len;
}

string SBDModem::GetSIMCardNumber()
{
    Logger::Locked(logger)->Info << "SBDModem::GetSIMCardNumber" << endl;
    string result;
    SendMessageExpectTokenWithStatus("AT+CICCID", false, result, settings.GetAckTimeoutMS());

    const bool tooShort = (result.length() < 19);
    const bool tooLong = (result.length() > 20);
    const bool allDigits = !tooShort && !tooLong && AllDigits(result);

    if (tooShort || tooLong || !allDigits)
    {
    	stringstream msg;
    	msg << "Result of GetSIMCardNumber (" << result << ") is not a valid ICCID";
    	throw ActionFailed(msg.str());
    }

    Logger::Locked(logger)->Info << "SBDModem::GetSIMCardNumber: " << result << endl;
    return result;
}

string SBDModem::GetManufacturer()
{
    Logger::Locked(logger)->Info << "SBDModem::GetManufacturer" << endl;
    string result;
    SendMessageExpectTokenWithStatus("AT+CGMI", false, result, settings.GetAckTimeoutMS());
    Logger::Locked(logger)->Info << "SBDModem::GetManufacturer: " << result << endl;
    return result;
}

string SBDModem::GetModel()
{
    Logger::Locked(logger)->Info << "SBDModem::GetModel" << endl;
    string result;
    SendMessageExpectTokenWithStatus("AT+CGMM", false, result, settings.GetAckTimeoutMS());
    Logger::Locked(logger)->Info << "SBDModem::GetModel: " << result << endl;
    return result;
}

SBDModem::StringArrayPtr SBDModem::GetRevisions()
{
    Logger::Locked(logger)->Info << "SBDModem::GetRevisions" << endl;
    StringArrayPtr response;
    response.reset(new StringArray);
    SendMessageExpectStringListTerminatedByStatus("AT+CGMR", *response, settings.GetAckTimeoutMS());
    stringstream stm;
    stm << "GetRevisions: " << response->size() << " items {";
    for (StringArray::const_iterator ci = response->begin(); ci != response->end(); ++ci)
        stm << " " << (ci-response->begin()+1) << " \"" << *ci << "\"";
    stm << " }";
    Logger::Locked(logger)->Info << stm.str() << endl;
    return response;
}

bool SBDModem::GetRingIndication()
{
    Logger::Locked(logger)->Info << "SBDModem::GetRingIndication" << endl;
    string response;
    const string query = "AT+SBDMTA?";
    SendMessageExpectTokenWithStatus(query, false, response, settings.GetAckTimeoutMS());
    const string expectedResponse = "+SBDMTA:";

    if (!boost::starts_with(response, expectedResponse))
    {
        stringstream stm;
        stm << "SBDModem::GetRingIndication: response to " << query << " was expected to start with " << expectedResponse << ", but " << response << " was received";
        string msg = stm.str();
        Logger::Locked(logger)->Error << msg << endl;
        throw ProtocolError(msg);
    }

    bool result;
    const string tail = response.substr(expectedResponse.length());

    try
    {
        result = !!boost::lexical_cast<int>(tail);
    }
    catch(...)
    {
        stringstream stm;
        stm << "SBDModem::GetRingIndication: the tail of the response to " << query << " (" << tail << ") could not be converted to integer";
        string msg = stm.str();
        Logger::Locked(logger)->Error << msg << endl;
        throw ProtocolError(msg);
    }

    Logger::Locked(logger)->Info << "SBDModem::GetRingIndication: " << result << endl;
    return result;
}

void SBDModem::SetRingIndication(bool newStatus)
{
    Logger::Locked(logger)->Info << "SBDModem::SetRingIndication newStatus=" << newStatus << endl;
    stringstream cmdstm;
    cmdstm << "AT+SBDMTA=" << (newStatus ? 1 : 0);
    string cmd = cmdstm.str();
    SendMessageWithStatusResponse(cmd, settings.GetAckTimeoutMS());
}

SBDModem::SBDStatus SBDModem::GetSBDStatus()
{
	// TODO: finish this
	throw logic_error("Not implemented");
}

string SBDModem::GetSerialNumber()
{
    Logger::Locked(logger)->Info << "SBDModem::GetSerialNumber" << endl;
    string result;
    SendMessageExpectTokenWithStatus("AT+CGSN", false, result, settings.GetAckTimeoutMS());
    return result;
}

unsigned SBDModem::GetSignalStrength(bool fast)
{
    Logger::Locked(logger)->Info << "SBDModem::GetSignalStrength fast=" << BooleanToString(fast) << endl;
    string resultData;
    string prefix;

    if (fast)
    {
        SendMessageExpectTokenWithStatus("AT+CSQF", false, resultData, settings.GetAckTimeoutMS());
        prefix = "+CSQF:";
    }
    else
    {
        SendMessageExpectTokenWithStatus("AT+CSQ", false, resultData, settings.GetShortTransactionTimeoutMS());
        prefix = "+CSQ:";
    }

    if (!boost::starts_with(resultData, prefix))
    {
        stringstream stm;
        stm << "SBDModem::GetSignalStrength: response string (" << resultData << ") was expected to start with " << prefix;
        const string msg = stm.str();
        Logger::Locked(logger)->Error << msg << endl;
        throw ProtocolError(msg);
    }

    const string tail = resultData.substr(prefix.length());
    unsigned result;

    try
    {
        result = boost::lexical_cast<unsigned>(tail);
    }
    catch(...)
    {
        stringstream stm;
        stm << "SBDModem::GetSignalStrength: number of bars (" << resultData << ") could not be converted to integer";
        const string msg = stm.str();
        Logger::Locked(logger)->Error << msg << endl;
        throw ProtocolError(msg);
    }

    return result;
}

void SBDModem::Ping()
{
    Logger::Locked(logger)->Info << "SBDModem::Ping" << endl;
    SendMessageWithStatusResponse("AT", settings.GetAckTimeoutMS());
}

string SBDModem::ReadMTBuffer()
{
    Logger::Locked(logger)->Info << "SBDModem::ReadMTBuffer" << endl;
    string result;
    SendMessageExpectTokenWithStatus("AT+SBDRB", true, result, settings.GetAckTimeoutMS());
    return result;
}

unsigned SBDModem::ReadSignalQuality(bool wait)
{
    Logger::Locked(logger)->Info << "SBDModem::ReadSignalQuality wait=" << wait << endl;
    unsigned result = 0;
    string response;

    if (wait)
    	SendMessageExpectTokenWithStatus("AT+CSQ", false, response, settings.GetLongTransactionTimeoutMS());
    else
    	SendMessageExpectTokenWithStatus("AT+CSQF", false, response, settings.GetAckTimeoutMS());

    const size_t colonIdx = response.find_first_of(':');

    if (colonIdx == string::npos)
    {
    	stringstream ss;
    	ss << "SBDModem::ReadSignalQuality: missing colon in response (" << response << ")";
    	const string s = ss.str();
    	Logger::Locked(logger)->Error << s << endl;
    	throw ProtocolError(s);
    }

    result = boost::lexical_cast<unsigned>(response.substr(colonIdx));
    Logger::Locked(logger)->Info << "SBDModem::ReadSignalQuality: returning " << result << endl;
    return result;
}

template<typename T>
T ConvertHexString(const string& input)
{
	stringstream ss;
	ss << std::hex << input;
	T t;
	ss >> t;
	return t;
}

template<typename T>
bool ValidHexString(const string& input)
{
	const size_t inputLen = input.length();
	bool result = inputLen && (inputLen <= 2*sizeof(T));

	for (string::const_iterator ci = input.begin(); result && (ci != input.end()); ++ci)
	{
		const char c = *ci;
		result = isxdigit(c);
	}

	return result;
}

uint32_t SBDModem::RequestSystemTime()
{
	using namespace boost::posix_time;

    Logger::Locked(logger)->Info << "SBDModem::RequestSystemTime" << endl;
    string response;

   	SendMessageExpectTokenWithStatus("AT-MSSTM", false, response, settings.GetAckTimeoutMS());

   	const string expectedResponsePrefix = "-MSSTM:";

   	if (!boost::starts_with(response, expectedResponsePrefix))
   	{
   		stringstream ss;
   		ss << "SBDModem::RequestSystemTime: response (" << response << ") expected to start with " << expectedResponsePrefix;
   		const string s = ss.str();
    	Logger::Locked(logger)->Error << s << endl;
    	throw ProtocolError(s);
   	}

   	string tail = response.substr(expectedResponsePrefix.length());
   	boost::trim(tail);

   	if (!ValidHexString<uint32_t>(tail))
   	{
   		stringstream ss;
   		ss << "SBDModem::RequestSystemTime: response tail (" << tail << ") is not a valid hexadecimal string";
   		const string s = ss.str();
    	Logger::Locked(logger)->Error << s << endl;
   		throw ActionFailed(s);
   	}

   	uint32_t result;

   	try
   	{
   		result = ConvertHexString<uint32_t>(tail);
   	}
   	catch(...)
   	{
   		stringstream ss;
   		ss << "SBDModem::RequestSystemTime: response tail (" << tail<< ") could not be converted to integer (from hexadecimal string)";
   		const string s = ss.str();
   		Logger::Locked(logger)->Error << s << endl;
   		throw ActionFailed(s);
   	}

    Logger::Locked(logger)->Info << "SBDModem::RequestSystemTime: returning " << result << " (" << to_simple_string(SystemTimeToBoostTime(result)) << ")" << endl;
    return result;
}


void SBDModem::RingIndicatorAutoRegister(bool enable, bool automatic)
{
    Logger::Locked(logger)->Info << "SBDModem::RingIndicatorAutoRegister enable=" << enable << " automatic=" << automatic << endl;
	const unsigned mode = enable ? (automatic ? 1 : 2) : 0;
	stringstream cmdstr;

	cmdstr << "AT+SBDAREG=" << mode;

	const string cmd = cmdstr.str();

	SendMessageWithStatusResponse(cmd, settings.GetAckTimeoutMS());
}

void SBDModem::RingIndicatorDetach()
{
    Logger::Locked(logger)->Info << "SBDModem::RingIndicatorDetach" << endl;
    string response;

	SendMessageExpectTokenWithStatus("AT+SBDDET", false, response, settings.GetLongTransactionTimeoutMS());

	if (!boost::starts_with(response, "+SBDDET:"))
	{
		stringstream ss;
		ss << "SBDModem::RingIndicatorDetach: response (" << response << ") does not start with the expected tag (+SBDDET)";
		const string s = ss.str();
	    Logger::Locked(logger)->Error << s << endl;
	    throw ProtocolError(s);
	}

	using namespace boost;
	const string tail = response.substr(8);
	StringArray fields;

	split(fields, tail, is_any_of(","), token_compress_off);

	const unsigned numFields = fields.size();

	if (numFields != 2)
	{
		stringstream ss;
		ss << "SBDModem::RingIndicatorDetach response (" << response << ") was expected to have 2 fields, but " << numFields << " were found";

		const string s = ss.str();
	    Logger::Locked(logger)->Error << s << endl;
		throw ProtocolError(s);
	}

	int fieldNum = 0;
	//int statusCode; //= 0;
	int errorCode = 0;

	try
	{
		/*statusCode =*/ lexical_cast<int>(fields[fieldNum]);
		++fieldNum;
		errorCode = lexical_cast<int>(fields[fieldNum]);
	}
	catch(...)
	{
		stringstream ss;
		ss << "SBDModem::RingIndicatorDetach field " << (fieldNum+1) << " (" << fields[fieldNum] << ") could not be converted to integer";

		const string s = ss.str();
	    Logger::Locked(logger)->Error << s << endl;
		throw ProtocolError(s);
	}

    stringstream codeStream;
    codeStream << "SBDModem::RingIndicatorDetach returned error code " << errorCode;
    string codeMsg = codeStream.str();;

    switch(errorCode)
    {
        case 15:
        case 16:

            throw AccessDenied(codeMsg);

        case 17:
        case 18:
        case 19:
        case 32:

            throw NetworkError(codeMsg);

        case 35:

            throw BusyTryLater(codeMsg);

        default:

            if (errorCode > 4)
                throw ActionFailed(codeMsg);

            break;
    }
}

inline string PositionToString(double pos, unsigned degDig)
{
	const int sgn = (pos < 0) ? -1 : 1;				// sign of the orignal quantity
	const int divisor = 60000;						// full divisor 60 minutes * 1000 thousandths
	const double pos2 = pos+sgn/divisor/2.0;		// rounding in the direction of the sign
	int tmp = floor(pos2*divisor*sgn);				// absolute value of the position in integer form
	const unsigned thousandths = tmp % 1000;		// pulling out thousandths
	tmp /= 1000;									// shifting off the thousandths
	const unsigned minutes = tmp % 60;				// pulling out minutes
	tmp /= 60;										// shifting off miutes
	const unsigned degrees = tmp;					// result is degrees

	// create the string representation and return it
	stringstream ss;
	if (sgn < 0) ss << "-";
	ss << ultos(degrees,10,degDig) << ultos(minutes,10,2) << "." << ultos(thousandths,10,3);
	const string result = ss.str();
	return result;
}

void SBDModem::RingIndicatorRegister(double latitude, double longitude)
{
    Logger::Locked(logger)->Info << "SBDModem::RingIndicatorRegister latitude=" << latitude << " longitude=" << longitude << endl;

	stringstream cmdstr;
	cmdstr << "AT+SBDREG=" << PositionToString(latitude,2) << "," << PositionToString(longitude,3);
	const string cmd = cmdstr.str();

	string response;
	SendMessageExpectTokenWithStatus(cmd, false, response, settings.GetLongTransactionTimeoutMS());

	if (!boost::starts_with(response, "+SBDREG:"))
	{
		stringstream ss;
		ss << "SBDModem::RingIndicatorRegister: response (" << response << ") does not start with the expected tag (+SBDREG)";
		const string s = ss.str();
	    Logger::Locked(logger)->Error << s << endl;
	    throw ProtocolError(s);
	}

	using namespace boost;
	const string tail = response.substr(8);
	StringArray fields;

	split(fields, tail, is_any_of(","), token_compress_off);

	const unsigned numFields = fields.size();

	if (numFields != 2)
	{
		stringstream ss;
		ss << "SBDModem::RingIndicatorRegister response (" << response << ") was expected to have 2 fields, but " << numFields << " were found";

		const string s = ss.str();
	    Logger::Locked(logger)->Error << s << endl;
		throw ProtocolError(s);
	}

	int fieldNum = 0;
	//int statusCode = 0;
	int errorCode = 0;

	try
	{
		/*statusCode = */lexical_cast<int>(fields[fieldNum]);
		++fieldNum;
		errorCode = lexical_cast<int>(fields[fieldNum]);
	}
	catch(...)
	{
		stringstream ss;
		ss << "SBDModem::RingIndicatorRegister field " << (fieldNum+1) << " (" << fields[fieldNum] << ") could not be converted to integer";

		const string s = ss.str();
	    Logger::Locked(logger)->Error << s << endl;
		throw ProtocolError(s);
	}

    stringstream codeStream;
    codeStream << "SBDModem::RingIndicatorRegister returned error code " << errorCode;
    string codeMsg = codeStream.str();;

    switch(errorCode)
    {
        case 15:
        case 16:

            throw AccessDenied(codeMsg);

        case 17:
        case 18:
        case 19:
        case 32:

            throw NetworkError(codeMsg);

        case 35:
        case 36:

            throw BusyTryLater(codeMsg);

        default:

            if (errorCode > 2)
                throw ActionFailed(codeMsg);

            break;
    }
}

SBDModem::SessionStatusExtended SBDModem::SessionExecute(bool fromRingIndicator)
{
    Logger::Locked(logger)->Info << "SBDModem::SessionExecute fromRingIndicator=" << fromRingIndicator << endl;
    stringstream cmdstr;
    cmdstr << "AT+SBDIX";
    if (fromRingIndicator) cmdstr << "A";
    return DoSessionExecute(cmdstr.str());
}

SBDModem::SessionStatusExtended SBDModem::SessionExecute(bool fromRingIndicator, double latitude, double longitude)
{
    Logger::Locked(logger)->Info << "SBDModem::SessionExecute fromRingIndicator=" << fromRingIndicator << " latitude=" << latitude << " longitude=" << longitude << endl;
    stringstream cmdstr;
    cmdstr << "AT+SBDIX";
    if (fromRingIndicator) cmdstr << "A";
	cmdstr << "=" << PositionToString(latitude,2) << "," << PositionToString(longitude,3);
    return DoSessionExecute(cmdstr.str());
}

SBDModem::SessionStatusExtended SBDModem::DoSessionExecute(const string& cmd)
{
	string response;
	SendMessageExpectTokenWithStatus(cmd, false, response, settings.GetLongTransactionTimeoutMS());

	bool parsing = true;

	try
	{
		SessionStatusExtended result(response);
		parsing = false;
		Logger::Locked(logger)->Info << "SBDModem::DoSessionExecute for '" << cmd << "' returning " << result << endl;
		return result;
	}
	catch(exception& ex)
	{
		if (parsing)
			Logger::Locked(logger)->Info << "SBDModem::DoSessionExecute for '" << cmd << "' caught " << typeid(ex).name() << " '" << ex.what() << "' (rethrown)" << endl;

		throw;
	}
}

void SBDModem::WriteMOBuffer(const string& message)
{
	unsigned short sum = 0;
	for (string::const_iterator ci = message.begin(); ci != message.end(); sum += (unsigned char) *ci++);

    Logger::Locked(logger)->Info << "SBDModem::WriteMOBuffer message=" << message.length() << " bytes (sum=0x" << ultos(sum,16,4) << ")" << endl;

	string temp(message.begin(), message.end());
	temp.push_back(sum >> 8);
	temp.push_back(sum & 0xFF);

	stringstream initmsg;
	initmsg << "AT+SBDWB=" << message.length();

	const string init = initmsg.str();
	const unsigned baudRateNum =  ISerialPort::BaudRate2Num(serialPort->GetBaudRate());
	const unsigned additionalTime = baudRateNum ? temp.length()*9600/baudRateNum : temp.length();

	SendMessageExpectString2TerminatedByStatus(init, "READY", MessageFormatter(temp,true), settings.GetAckTimeoutMS()+additionalTime, settings.GetShortTransactionTimeoutMS());
}

LockableConceptMutex& SBDModem::GetEventLock()
{
	return eventMutex;
}

SBDModem::ScopedConnectionPtr SBDModem::SubscribeIndicators(IndicatorsSignalType::slot_type slot)
{
	SBDModem::ScopedConnectionPtr result;
	result.reset(new ScopedConnection(indicatorsSignal.connect(slot)));
	return result;
}

SBDModem::ScopedConnectionPtr SBDModem::SubscribeRegStatus(RegStatusSignalType::slot_type slot)
{
	SBDModem::ScopedConnectionPtr result;
	result.reset(new ScopedConnection(regStatusSignal.connect(slot)));
	return result;
}

SBDModem::ScopedConnectionPtr SBDModem::SubscribeRing(RingSignalType::slot_type slot)
{
	SBDModem::ScopedConnectionPtr result;
	result.reset(new ScopedConnection(ringSignal.connect(slot)));
	return result;
}

void SBDModem::OnSerialReceive(SerialListener& sender, const ByteArray& receivedData)
{
	if (receivedData.size())
	{
		if (settings.GetDebugShowSerialReceive())
			Logger::Locked(logger)->Debug << "OnSerialReceive: " << receivedData.ToHexString() << endl;

		unsigned offset = 0;
		unsigned startpt = 0;

		while(offset < receivedData.size())
		{
			TokenPtr token;
			unsigned offset0 = offset;

			if (!(token = ParseBinary(receivedData, offset))
			 && !(token = ParseASCIILine(receivedData, offset)))

				++offset;

			if (token && (offset0 != startpt))
			{
				ByteArray ignored(receivedData.begin()+startpt, receivedData.begin()+offset0);

				Logger::Locked(logger)->Warning << "Ignoring some received data before token: " << ignored.ToHexString() << endl;
			}

			if (token)
			{
				startpt = offset;

				if (token->binary || token->data.length())
				{
					startpt = offset;

					if (!token->binary && (token->lineTerminator == '\r'))
						Logger::Locked(logger)->Debug << "Ignoring echoed data: " << token->data << endl;

					else
					{
						TokenPtrQueue::Ptr rr = readResults;
						string handled;
                                                bool returned = false;

                                                if (!token->binary)
                                                {
                                                    ILockableConcept::Lock lock(tagProtector);

                                                    if (token->data.length() && boost::starts_with(token->data, tag))
                                                        returned = true;
                                                }

						if (DefaultMessageHandler(token))
                                                {
                                                    if (returned)
                                                        handled = " >> HANDLED/RETURNED";
                                                    else
							handled = " >> HANDLED";
                                                }

						if (token->binary)
							Logger::Locked(logger)->Debug << "Received binary packet of " << token->data.length() << " bytes" << (rr ? "" : " (no listener)") << handled << endl;
						else
							Logger::Locked(logger)->Debug << "Received ASCII packet of " << token->data.length() << " bytes " << token->data << (rr ? "" : " (no listener)") << handled << endl;

						try
						{
							if (rr && (returned || handled.empty()))
								rr->WriteExpand(token);
						}
						catch(exception& ex)
						{
							Logger::Locked(logger)->Error << "SBDModem::OnSerialReceive caught " << typeid(ex).name() << " '" << ex.what() << "' while writing token to queue" << endl;
						}
					}
				}
			}
		}
	}
}

inline unsigned short GetBigEndianShort(const ByteArray& data, unsigned offset)
{
	unsigned short result = 0;

	if ((offset+2) <= data.size())
	{
		result = data[offset];
		result <<= 8;
		result |= data[offset+1];
	}

	return result;
}

SBDModem::TokenPtr SBDModem::ParseASCIILine(const ByteArray& data, unsigned& offset)
{
	TokenPtr result;
	unsigned idx = offset;
	char c;
	const unsigned dataSize = data.size();

	while((idx < dataSize) && (c = data[idx], isprint(c) || isblank(c)))
		++idx;

	if (idx < dataSize)
	{
		const bool isLF = (c == '\n');
		const bool isCRLF = !isLF && (c == '\r') && ((idx+1) < dataSize) && (data[idx+1] == '\n');
		const bool isCR = !isCRLF && (c == '\r');

		if (isLF || isCRLF || isCR)
		{
			const string dataPart((const char*) &data.at(offset), (const char*) &data.at(idx));

			result.reset(new Token(dataPart, false, isCR ? '\r' : '\n'));
			offset = idx+(isCRLF ? 2 : 1);
		}
	}

	return result;
}

bool SBDModem::DefaultMessageHandler(TokenPtr token)
{
	bool handled = false;

	if (token && !token->binary)
	{
		handled = true;

		if (!token->data.length())
		{
			// ignoring zero-length packets
		}
		else if (boost::starts_with(token->data, "+CIEV:"))
		{
			// signal level change event
			Logger::Locked(logger)->Notice << "Firing signal level change event" << endl;
			boost::shared_ptr<IndicatorEventReport> eventReport;

			try
			{
				string tail = token->data.substr(6);
				eventReport.reset(new IndicatorEventReport(tail));
			}
			catch(exception& ex)
			{
				Logger::Locked(logger)->Error << "No event will be fired: " << ex.what() << endl;
			}

			if (eventReport)
			{
				ILockableConcept::Lock fireLock(eventMutex);
				indicatorsSignal(*this, *eventReport);
			}
		}
		else if (boost::starts_with(token->data, "+CREG:"))
		{
			// registration status change event
			//Logger::Locked(logger)->Notice << "Firing network registration event" << endl;
			//ILockableConcept::Lock fireLock(eventMutex);
			// TODO: fire event
		}
		else if (boost::equals(token->data, "SBDRING"))
		{
			// SBD ring indicator event
			Logger::Locked(logger)->Notice << "Firing SBD ring indicator event" << endl;
			ILockableConcept::Lock fireLock(eventMutex);
			ringSignal(*this);
		}
		else
			handled = false;
	}

	return handled;
}


SBDModem::TokenPtr SBDModem::ParseBinary(const ByteArray& data, unsigned& offset)
{
	TokenPtr result;
	unsigned idx = offset;

	if ((idx+2) <= data.size())
	{
		const unsigned short len = GetBigEndianShort(data, offset);

		if ((idx+2+len+2) <= data.size())
		{
			const unsigned endpt = idx+2+len;
			unsigned short sum = 0;

			for (unsigned i = idx+2; i < endpt; ++i) sum += data[i];

			const unsigned short sumFromPacket = GetBigEndianShort(data, endpt);

			if (sum == sumFromPacket)
			{
				string dataPart((const char*) &data.at(idx+2), (const char*) &data.at(endpt));

				result.reset(new Token(dataPart, true));
				offset = endpt+2;
			}
			else
				Logger::Locked(logger)->Debug << "Possible binary packet of " << len << " at index " << offset
					<< " calculated sum=0x" << ultos(sum,16,4)
					<< " in-packet=0x" << ultos(sumFromPacket,16,4) << endl;
		}
	}

	return result;
}

unsigned SBDModem::BaudRateToRateID(unsigned baudRate, bool throwOnError)
{
	unsigned result;

	switch(baudRate)
	{
	case 600:		result = 1; break;
	case 1200:		result = 2; break;
	case 2400:		result = 3; break;
	case 4800:		result = 4; break;
	case 9600:		result = 5; break;
	case 19200:		result = 6; break;
	case 38400:		result = 7; break;
	case 57600:		result = 8; break;
	case 115200:	result = 9; break;
	default:		result = 0;	break;
	}

	if (!result)
	{
		stringstream stm;
		stm << "Baud rate parameter (" << baudRate << ") is invalid";

		const string msg = stm.str();
		throw invalid_argument(msg);
	}

	return result;
}

unsigned SBDModem::RateIDToBaudRate(unsigned rateID, bool throwOnError)
{
	unsigned result;

	switch(rateID)
	{
	case 1:		result = 600;		break;
	case 2:		result = 1200;		break;
	case 3:		result = 2400;		break;
	case 4:		result = 4800;		break;
	case 5:		result = 9600;		break;
	case 6:		result = 19200;		break;
	case 7:		result = 38400;		break;
	case 8:		result = 57600;		break;
	case 9:		result = 115200;	break;
	default:	result = 0;			break;
	}

	if (!result)
	{
		stringstream stm;
		stm << "Rate ID parameter (" << rateID << ") is invalid";

		const string msg = stm.str();
		throw invalid_argument(msg);
	}

	return result;
}

SBDModem::DTERate::DTERate(const string& iprResponse)
: rateID(0)
, rate(0)
, autoBaud(false)
{
	using namespace boost;

	if (!starts_with(iprResponse, "+IPR:"))
	{
		stringstream ss;
		ss << "SBDModem::DTERate response (" << iprResponse << ") was expected to start with +IPR:";

		const string s = ss.str();
		throw invalid_argument(s);
	}

	const string tail = iprResponse.substr(5);
	StringArray fields;

	split(fields, tail, is_any_of(","), token_compress_off);

	const unsigned numFields = fields.size();

	if ((numFields < 1) || (numFields > 2))
	{
		stringstream ss;
		ss << "SBDModem::DTERate response (" << iprResponse << ") was expected to have 1 to 2 fields, but " << numFields << " were found";

		const string s = ss.str();
		throw invalid_argument(s);
	}

	int fieldNum = 0;

	try
	{
		rateID = lexical_cast<unsigned>(fields[fieldNum]);

		if (numFields > 1)
		{
			++fieldNum;
			autoBaud = !!lexical_cast<int>(fields[fieldNum]);
		}
		else
			autoBaud = false;
	}
	catch(...)
	{
		stringstream ss;
		ss << "SBDModem::DTERate field " << (fieldNum+1) << " (" << fields[fieldNum] << ") could not be converted";

		const string s = ss.str();
		throw invalid_argument(s);
	}

	rate = RateIDToBaudRate(rateID, false);
}

SBDModem::DTERate::DTERate(unsigned _rateID, bool _autoBaud)
: rateID(_rateID)
, rate(0)
, autoBaud(_autoBaud)
{
	rate = RateIDToBaudRate(_rateID, true);
}

SBDModem::IndicatorEventReport::IndicatorEventReport(const string& tail)
{
	using namespace boost;
	StringArray fields;
	split(fields, tail, is_any_of(","), token_compress_off);

	const unsigned numFields = fields.size();

	if (numFields != 2)
	{
		stringstream ss;
		ss << "SBDModem::IndicatorEventReport tail (" << tail << ") expected to have 2 fields, but " << numFields << " were found";

		string s = ss.str();
		throw invalid_argument(s);
	}

	int fieldNum = 0;

	try
	{
		reportType = (IndicatorEventReport::EReportType) lexical_cast<int>(fields[fieldNum]);
		++fieldNum;
		param = lexical_cast<int>(fields[fieldNum]);
	}
	catch(...)
	{
		stringstream ss;
		ss << "SBDModem::IndicatorEventReport tail (" << tail << ") field #" << (fieldNum+1) << " could not be converted to integer";

		string s = ss.str();
		throw invalid_argument(s);
	}

	if ((reportType != ERP_SERVICE_AVAILABLE)
	 && (reportType != ERP_SIGNAL_STRENGTH)
	 && (reportType != ERP_ANTENNA_FAULT))
	{
		stringstream ss;
		ss << "SBDModem::IndicatorEventReport for tail (" << tail << ") the report type (" << reportType << ") is out of range";

		string s = ss.str();
		throw invalid_argument(s);
	}
}

ostream& operator<<(ostream& s, const SBDModem::DTERate& instance)
{
	s << "rateID=" << instance.rateID << " rate=" << instance.rate << " autoBaud=" << instance.autoBaud;
	return s;
}

SBDModem::IndicatorEventReport::IndicatorEventReport(EReportType _reportType, int _param)
: reportType(_reportType)
, param(_param)
{
	if ((_reportType != ERP_SERVICE_AVAILABLE)
	 && (_reportType != ERP_SIGNAL_STRENGTH)
	 && (_reportType != ERP_ANTENNA_FAULT))

		throw invalid_argument("SBDModem::IndicatorEventReport: report type is out of range");
}

inline int ConvertToBooleanInt(const string& str)
{
	using namespace boost;
	return lexical_cast<int>(str) ? 1 : 0;
}

SBDModem::IndicatorEventReporting::IndicatorEventReporting(const string& tail)
: reportQualityChanges(false)
, reportServiceChanges(false)
, haveAntennaFaultSetting(false)
, reportAntennaFault(false)
{
	using namespace boost;
	StringArray fields;
	split(fields, tail, is_any_of(","), token_compress_off);

	const unsigned numFields = fields.size();

	if ((numFields < 3) || (numFields > 4))
	{
		stringstream ss;
		ss << "SBDModem::IndicatorEventReporting tail (" << tail << ") expected to have 3 or 4 fields, but " << numFields << " were found";

		string s = ss.str();
		throw invalid_argument(s);
	}

	int fieldNum = 0;

	try
	{
		enabled = ConvertToBooleanInt(fields[fieldNum]);
		++fieldNum;
		reportQualityChanges = ConvertToBooleanInt(fields[fieldNum]);
		++fieldNum;
		reportServiceChanges = ConvertToBooleanInt(fields[fieldNum]);
		++fieldNum;
		haveAntennaFaultSetting = (numFields == 4);
		reportAntennaFault = haveAntennaFaultSetting && ConvertToBooleanInt(fields[fieldNum]);
	}
	catch(...)
	{
		stringstream ss;
		ss << "SBDModem::IndicatorEventReporting tail (" << tail << ") field #" << (fieldNum+1) << " could not be converted to integer";

		string s = ss.str();
		throw invalid_argument(s);
	}
}

SBDModem::IndicatorEventReporting::IndicatorEventReporting(bool _reportQualityChanges, bool _reportServiceChanges, bool _haveAntennaFaultSetting, bool _reportAntennaFault)
: reportQualityChanges(_reportQualityChanges)
, reportServiceChanges(_reportServiceChanges)
, haveAntennaFaultSetting(_haveAntennaFaultSetting)
, reportAntennaFault(_haveAntennaFaultSetting && _reportAntennaFault)
{
	enabled = reportServiceChanges || reportQualityChanges || reportAntennaFault;
}

string SBDModem::IndicatorEventReporting::ToString() const
{
	stringstream stm;
	stm << "enabled=" << enabled
	    << " reportQualityChanges=" << reportQualityChanges
	    << " reportServiceChanges=" << reportServiceChanges
	    << " haveAntennaFaultSetting=" << haveAntennaFaultSetting
	    << " reportAntennaFault=" << reportAntennaFault;
	string result = stm.str();
	return result;
}

string SBDModem::IndicatorEventReporting::ToDataString() const
{
	stringstream stm;
	stm << enabled << "," << reportServiceChanges << "," << reportQualityChanges;
	if (haveAntennaFaultSetting) stm << "," << reportAntennaFault;
	string result = stm.str();
	return result;
}

SBDModem::RingIndicatorRegStatus::RingIndicatorRegStatus(unsigned _eventID, unsigned _regError)
: eventID(_eventID)
, regError(_regError)
{
}

SBDModem::SessionStatusExtended::SessionStatusExtended()
: moStatus(0)
, moMSN(0)
, mtStatus(0)
, mtMSN(0)
, mtMessageSize(0)
, mtCountQueued(0)
{
}

template<typename T>
T TrimAndCast(const string& input)
{
    using namespace boost;
    const string temp = trim_copy(input);
    const T result = lexical_cast<T>(temp);
    return result;
}

SBDModem::SessionStatusExtended::SessionStatusExtended(const string& response)
: moStatus(0)
, moMSN(0)
, mtStatus(0)
, mtMSN(0)
, mtMessageSize(0)
, mtCountQueued(0)
{
	using namespace boost;

	if (!starts_with(response, "+SBDIX:"))
	{
		stringstream ss;
		ss << "SBDModem::SessionStatusExtended response (" << response << ") expected to start with +SBDIX:";
		const string s = ss.str();
		throw invalid_argument(s);
	}

	const string tail = response.substr(7);
	StringArray fields;
	split(fields, tail, is_any_of(",:"), token_compress_off);

	const unsigned numFields = fields.size();

	if (numFields != 6)
	{
		stringstream ss;
		ss << "SBDModem::SessionStatusExtended response (" << response << ") expecting to have 6 fields, but " << numFields << " were found";
		const string s = ss.str();
		throw invalid_argument(s);
	}

	int fieldNum = 0;

	try
	{
		moStatus = TrimAndCast<unsigned>(fields[fieldNum]);
		++fieldNum;
		moMSN = TrimAndCast<unsigned>(fields[fieldNum]);
		++fieldNum;
		mtStatus = TrimAndCast<unsigned>(fields[fieldNum]);
		++fieldNum;
		mtMSN = TrimAndCast<unsigned>(fields[fieldNum]);
		++fieldNum;
		mtMessageSize = TrimAndCast<unsigned>(fields[fieldNum]);
		++fieldNum;
		mtCountQueued = TrimAndCast<unsigned>(fields[fieldNum]);
	}
	catch(...)
	{
		stringstream ss;
		ss << "SBDModem::SessionStatusExtended tail (" << tail << ") field #" << (fieldNum+1) << " could not be converted to integer";

		string s = ss.str();
		throw invalid_argument(s);
	}
}

SBDModem::SessionStatusExtended::SessionStatusExtended(unsigned _moStatus, unsigned _moMSN, unsigned _mtStatus, unsigned _mtMSN, unsigned _mtMessageSize, unsigned _mtCountQueued)
: moStatus(_moStatus)
, moMSN(_moMSN)
, mtStatus(_mtStatus)
, mtMSN(_mtMSN)
, mtMessageSize(_mtMessageSize)
, mtCountQueued(_mtCountQueued)
{
}

ostream& operator<<(ostream& s, const SBDModem::SessionStatusExtended& instance)
{
	s << "moStatus=" << instance.moStatus;

	switch(instance.moStatus)
	{
	case SBDModem::SessionStatusExtended::MOSV_SUCCESS:							s << " (success)";							break;
	case SBDModem::SessionStatusExtended::MOSV_SUCCESS_MT_MESSAGE_TOO_BIG:		s << " (success, MT message too big)";		break;
	case SBDModem::SessionStatusExtended::MOSV_SUCCESS_BAD_LOCATION_UPDATE:		s << " (success, bad location update)";		break;
	case SBDModem::SessionStatusExtended::MOSV_CALL_TIMEOUT:					s << " (call timeout)";						break;
	case SBDModem::SessionStatusExtended::MOSV_QUEUE_FULL:						s << " (queue full)";						break;
	case SBDModem::SessionStatusExtended::MOSV_TOO_MANY_SEGMENTS:				s << " (too many segments)";				break;
	case SBDModem::SessionStatusExtended::MOSV_SESSION_DID_NOT_COMPLETE:		s << " (incomplete session)";				break;
	case SBDModem::SessionStatusExtended::MOSV_INVALID_SEGMENT_SIZE:			s << " (invalid segment size)";				break;
	case SBDModem::SessionStatusExtended::MOSV_ACCESS_DENIED:					s << " (access denied)";					break;
	case SBDModem::SessionStatusExtended::MOSV_ISU_LOCKED:						s << " (ISU locked)";						break;
	case SBDModem::SessionStatusExtended::MOSV_GATEWAY_TIMEOUT:					s << " (gateway timeout)";					break;
	case SBDModem::SessionStatusExtended::MOSV_CONNECTION_LOST:					s << " (connection lost)";					break;
	case SBDModem::SessionStatusExtended::MOSV_LINK_FAILURE:					s << " (link failure)";						break;
	case SBDModem::SessionStatusExtended::MOSV_NO_NETWORK:						s << " (no network)";						break;
	case SBDModem::SessionStatusExtended::MOSV_ISU_BUSY:						s << " (ISU busy)";							break;

	default:

		if (instance.moStatus < 5)
			s << " (reserved, success)";
		else
			s << " (reserved, failure)";

		break;
	}

	s << " moMSN=" << instance.moMSN
	  << " mtStatus=" << instance.mtStatus;

	switch(instance.mtStatus)
	{
	case SBDModem::SessionStatusExtended::MTSV_NO_SBD_MESSAGE:					s << " (no message)";						break;
	case SBDModem::SessionStatusExtended::MTSV_SBD_MESSAGE_RECEIVED:			s << " (message received)";					break;
	case SBDModem::SessionStatusExtended::MTSV_ERROR_OCCURRED:					s << " (error)";							break;
	default:																	s << " (?)";								break;
	}

	s << " mtMSN=" << instance.mtMSN
	  << " mtMessageSize=" << instance.mtMessageSize
	  << " mtCountQueued=" << instance.mtCountQueued;

	return s;

}

bool SBDModem::SessionStatusExtended::GoodMOStatus() const
{
	return (moStatus >= 0) && (moStatus <= 4);
}

bool SBDModem::SessionStatusExtended::HaveMTMessage() const
{
	return (mtStatus == MTSV_SBD_MESSAGE_RECEIVED);
}

bool SBDModem::SessionStatusExtended::GoodMTStatus() const
{
	return (mtStatus == MTSV_NO_SBD_MESSAGE) || (mtStatus == MTSV_SBD_MESSAGE_RECEIVED);
}

void SBDModem::SessionStatusExtended::ThrowError() const
{
    if (mtStatus != MTSV_SBD_MESSAGE_RECEIVED)
    {
        stringstream codeStream;
        codeStream << "moStatus = " << moStatus;
        string codeMsg = codeStream.str();;

        switch(moStatus)
        {
            case MOSV_SUCCESS:
            case MOSV_SUCCESS_MT_MESSAGE_TOO_BIG:
			case MOSV_SUCCESS_BAD_LOCATION_UPDATE:

                break;

			case MOSV_ACCESS_DENIED:
			case MOSV_ISU_LOCKED:

                throw AccessDenied(codeMsg);

            case MOSV_CALL_TIMEOUT:
			case MOSV_TOO_MANY_SEGMENTS:
			case MOSV_SESSION_DID_NOT_COMPLETE:
			case MOSV_INVALID_SEGMENT_SIZE:
			case MOSV_GATEWAY_TIMEOUT:
			case MOSV_CONNECTION_LOST:
			case MOSV_LINK_FAILURE:
			case MOSV_NO_NETWORK:

                throw NetworkError(codeMsg);

			case MOSV_ISU_BUSY:

                throw BusyTryLater(codeMsg);

            case MOSV_QUEUE_FULL:
            default:

                throw ActionFailed(codeMsg);
        }
    }
}

SBDModem::SBDStatus::SBDStatus(bool _haveMOMessage, bool _moMSN, bool _haveMTMessage, unsigned _mtMSN)
: haveMOMessage(_haveMOMessage)
, moMSN(_moMSN)
, haveMTMessage(_haveMTMessage)
, mtMSN(_mtMSN)
, ringAlertPending(false)
, mtCountQueued(0)
, haveRAandMTCount(false)
{
}

SBDModem::SBDStatus::SBDStatus(bool _haveMOMessage, bool _moMSN, bool _haveMTMessage, unsigned _mtMSN, bool _ringAlertPending, unsigned _mtCountQueued)
: haveMOMessage(_haveMOMessage)
, moMSN(_moMSN)
, haveMTMessage(_haveMTMessage)
, mtMSN(_mtMSN)
, ringAlertPending(_ringAlertPending)
, mtCountQueued(_mtCountQueued)
, haveRAandMTCount(true)
{
}

SBDModem::NetworkRegistration::NetworkRegistration()
: notification(~0)
, status(~0)
, notSupported(true)
{
}

SBDModem::NetworkRegistration::NetworkRegistration(const string& tail)
: notification(~0)
, status(~0)
, notSupported(false)
{
	using namespace boost;
	StringArray fields;
	split(fields, tail, is_any_of(","), token_compress_off);

	const unsigned numFields = fields.size();

	if ((numFields != 2) && (numFields != 4))
	{
		stringstream ss;
		ss << "SBDModem::NetworkRegistration tail (" << tail << ") expected to have 2 or 4 fields, but " << numFields << " were found";

		string s = ss.str();
		throw invalid_argument(s);
	}

	int fieldNum = 0;

	try
	{
            notification = boost::lexical_cast<unsigned>(fields[fieldNum]);
            ++fieldNum;
            status = boost::lexical_cast<unsigned>(fields[fieldNum]);
	}
	catch(...)
	{
		stringstream ss;
		ss << "SBDModem::NetworkRegistration tail (" << tail << ") field #" << (fieldNum+1) << " could not be converted to integer";

		string s = ss.str();
		throw invalid_argument(s);
	}

        if (numFields == 4)
        {
            lac = fields[fieldNum];
            ++fieldNum;
            ci = fields[fieldNum];
        }
}

SBDModem::NetworkRegistration::NetworkRegistration(unsigned _notification, unsigned _status)
: notification(_notification)
, status(_status)
, notSupported(false)
{
}

SBDModem::NetworkRegistration::NetworkRegistration(unsigned _notification, unsigned _status, const string& _lac, const string& _ci)
: notification(_notification)
, status(_status)
, lac(_lac)
, ci(_ci)
, notSupported(false)
{
}

string SBDModem::NetworkRegistration::ToString() const
{
    stringstream stm;
    if (notSupported)
        stm << "(not supported)";
    else
    {
        stm << "notification=" << notification;
        switch(notification)
        {
        case NREG_NOTIFY_OFF:							stm << " (off)";						break;
        case NREG_NOTIFY_ON:							stm << " (on)";							break;
        case NREG_NOTIFY_ON_WITH_LOCATIONS:				stm << " (on w/ locations)";			break;
        default:										stm << " (?)";							break;
        }

        stm << " status=" << status;

        switch(status)
        {
        case NREG_STATUS_NOT_REGISTERED_IDLE:			stm << " (not registered - IDLE)";		break;
        case NREG_STATUS_REGISTERED_AT_HOME:			stm << " (registered - home network)";	break;
        case NREG_STATUS_NOT_REGISTERED_SEARCHING:		stm << " (not registered - searching)";	break;
        case NREG_STATUS_REGISTRATION_DENIED:			stm << " (regisitration DENIED)";		break;
        case NREG_STATUS_UNKNOWN:						stm << " (not registered - starting)";	break;
        case NREG_STATUS_REGISTERED_ROAMING:			stm << " (registered - ROAMING)";		break;
        default:										stm << " (?)";							break;
        }

        stm << " lac=" << lac << " ci=" << ci;
    }

    const string result = stm.str();
    return result;
}

SBDModem::Token::Token(const string& _data, bool _binary, char _lineTerminator)
: binary(_binary)
, data(_data)
, lineTerminator(_lineTerminator)
{
}

SBDModem::TokenPtrQueueHolder::TokenPtrQueueHolder(TokenPtrQueue::Ptr& _rr)
: rr(_rr)
{
	tpq.reset(new TokenPtrQueue);
	rr = tpq;
}

SBDModem::TokenPtrQueueHolder::~TokenPtrQueueHolder()
{
	rr.reset();
}

SBDModem::AwaitingTagHolder::AwaitingTagHolder(const string newTag, string& _tag, ILockableConcept& _concept)
: tag(_tag)
, concept(_concept)
, lock(_concept)
{
    tag = newTag;
    lock.unlock();
}

SBDModem::AwaitingTagHolder::~AwaitingTagHolder()
{
    lock.lock();
    tag.clear();
    lock.unlock();
}

string SBDModem::MessageFormatter::ToString() const
{
    stringstream stm;

    if (isBinary)
    {
        const size_t len = data.length();

        stm << "(binary " << len << " bytes)";

        if (len)
        {
            const ByteArray::byte* pStart = (const ByteArray::byte*) data.data();
            ByteArray temp(pStart, pStart+len);

            stm << " " << temp.ToHexString();
        }
    }
    else
        stm << "(ASCII) '" << data << "'";

    string result = stm.str();
    return result;
}

inline boost::posix_time::time_duration OffsetTime90(uint32_t systemTime)
{
	using namespace boost::posix_time;

    const unsigned _9sec = systemTime/100;
	const unsigned _9secMod = systemTime%100;

	time_duration result = seconds(9*_9sec)+milliseconds(_9secMod*90);
	return result;
}

boost::posix_time::ptime SBDModem::SystemTimeToBoostTime(unsigned systemTime, unsigned epochOffset)
{
	using namespace boost::posix_time;
	using namespace boost::gregorian;

	//ptime result(date(1996,Jun,1), time_duration(0,0,13));  -- this epoch time is in some older manuals but has been superceded
	ptime result(date(2007,Mar,8), time_duration(3,50,35));

	result += OffsetTime90(systemTime);

	if (epochOffset)
	{
		const time_duration epochCycle = time_duration(107374,10,56)+milliseconds(64);
		result += epochCycle*epochOffset;
	}

	return result;
}

}
}
