//-----------------------------------------------------------------------------------
// <copyright file="SSQCommunicator.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>
// Declarations for the SSQCommunicator class, which oversees the communication with
// the SBD modem using the SBDModem class.  After construction and successful
// initialization, the background thread continually attempts to establish
// communication with the modem:
//
// - The client invokes RequestSession to indicate when there are messages waiting
//   to be sent
// - The client is signaled whenever a packet is successfully received
// - The client is signaled whenever a session is about to be executed in order to
//   provide data to be sent; if the supplied packet is empty, no session is
//   executed.  The client can request a non-default mailbox check using this method.
// - The client is signaled when a packet is successfully transmitted.
// - The client is signaled with statistics whenever statistics change.
// </summary>
//
// <owner>Mike Cookson</owner>
//-----------------------------------------------------------------------------------

#include "SSQCommunicator.h"
#include "SBDModem.h"
#include "SSQCommunicatorSettings.h"
#include "LqrSMACLockableConcept.h"
#include "LqrSMACLogger.h"
#include <algorithm>
#include <boost/algorithm/string.hpp>

using namespace std;
using namespace LiquidRobotics::SMACFramework;
using namespace LiquidRobotics::PPPHostLib;

namespace LiquidRobotics {
namespace SimpleSBDQueue {

IMPLEMENT_INNER_EXCEPTION(SSQCommunicator, ShutdownRequested, std::runtime_error);

SSQCommunicator::SSQCommunicator(const SSQCommunicatorSettings& _settings, Logger& _logger)
: settings(_settings)
, logger(_logger)
, terminateSem(0)
, wakeupSem(0)
, isPowered(false)
, messageCookie(0)
, overrideMailboxCheckTime(0)
, havePosition(false)
, savedLat(0.0)
, savedLon(0.0)
{
}

SSQCommunicator::~SSQCommunicator()
{
    Shutdown();
}

void SSQCommunicator::Initialize()
{
    const string method = "SSQCommunicator::Initialize";
    Logger::Locked(logger)->Info << method << endl;

    ILockableConcept::Lock cmdLock(commandMutex);

    if (backgroundThread)
        Logger::Locked(logger)->Warning << method << ": Communicator was already initialized" << endl;
    else
    {
        Logger::Locked(logger)->Debug << method << ": Starting new thread for background" << endl;
        terminateSem.SetCount(0);
        backgroundThread.reset(new Thread(boost::bind(&SSQCommunicator::Background, this)));
    }
}

void SSQCommunicator::Shutdown()
{
    const string method = "SSQCommunicator::Shutdown";
    Logger::Locked(logger)->Info << method << endl;

    ILockableConcept::Lock cmdLock(commandMutex);

    if (backgroundThread)
    {
        ThreadPtr temp = backgroundThread;
        backgroundThread.reset();
        terminateSem.Post();
        if (!wakeupSem.GetCount())
            wakeupSem.Post();
        temp->join();
    }
    else
        Logger::Locked(logger)->Warning << method << ": Communicator was not initialized" << endl;
}

void SSQCommunicator::SetPosition(double lat, double lon)
{

    ILockableConcept::Lock lock(varsMutex);
    havePosition = true;
    savedLat = lat;
    savedLon = lon;
}

bool SSQCommunicator::GetSavedPosition(double& lat, double& lon)
{
    ILockableConcept::Lock lock(varsMutex);
    bool result;

    if (havePosition)
    {
        lat = savedLat;
        lon = savedLon;
        result = true;
    }
    else
        result = false;

    return result;
}

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

void SSQCommunicator::RequestSession()
{
    ILockableConcept::Lock cmdLock(commandMutex);

    if (!backgroundThread)
        throw logic_error("Not initialized");

    if (!wakeupSem.GetCount())
        wakeupSem.Post();
}

// Ensure the modem is off (wait for off time after setting power switch)
//
// while not terminating
//
//      If this is the first time running
//      or there was a network error and the network error time has passed
//      or there was a communication error and the communication error time has passed
//      or there was no problem then
//
//          something to be received = false
//
//          while time for a mailbox check
//          or no attempts made yet
//          or ((there is something to be sent or something to be received) and (no power up time limit or the power up time not exceeded))
//
//              If there is no modem interface then
//                  Power up the modem and wait for 1 second
//                  Create the modem interface and initialize
//                  Configure the interface
//                  Read the model number
//                  Update communication status
//                  Wait for up to the limit for the network registration to be good (fail if timeout)
//                  Update network status
//              end If
//
//              If there's something to send and the buffer is not full then
//                  Write the message to be sent to the buffer
//              end
//
//              Perform the session
//
//              Update session counts
//
//              If successful transmission then
//                  Fire successful transmission event
//              endif
//
//              If successful receive then
//                  Read the message from the buffer
//                  Fire successful receive event
//              endif
//
//          end while
//
//          If duty cycling modem or comm error then
//              Discard the modem instance
//              Power modem off
//              Wait for power down time
//          end if
//
//      end if
//
//  end while



void SSQCommunicator::Background()
{
    using namespace boost::posix_time;
    Semaphore::Lock terminateLock(terminateSem, boost::defer_lock_t());
    bool moreToRead = false;

    modemUpCounter.Reset();
    modemOffCounter.Reset();
    lastSessionCounter.Reset();
    errorCounter.Reset();
    networkUpCounter.Reset();

    isPowered = false;
    messageCookie = 0;
    messageData.clear();
    modemPtr.reset();
    overrideMailboxCheckTime = 0;

    // power the modem off
    PowerModem(false);

    // start the power off counter
    modemOffCounter.Reset(0, true);

    // fire the initial statistics event
    stats = mystats;
    Fire_Statistics(mystats, false);

    bool shutdownException = false;
    bool networkError = false;
    bool commError = false;
    TimeCounter failedCommandThrottleCounter(0, false);

    // each time through the loop, looking for exit request
    while(!shutdownException && !terminateLock.try_lock())
    {
        try
        {
            // if the maximum modem up time is exceeded in duty cycle mode and the power is on, then deactivate the modem;
            {
                const unsigned maxon = settings.GetMaxOnTimeMS();
                Logger::Locked(logger)->Debug << "ModemUpCounter running=" << modemUpCounter.IsRunning() << " maxon=" << maxon << " remaining=" << modemUpCounter.Remaining(maxon) << " duty=" << settings.GetDutyCycleModem() << endl;

                if (modemUpCounter.IsRunning() && maxon && !modemUpCounter.Remaining(maxon) && settings.GetDutyCycleModem() && isPowered && modemPtr)
                {
                    Logger::Locked(logger)->Debug << "Max modem up time exceeded: shutting the modem off" << endl;
                    DeallocateModem();
                }
            }

            // if this isn't the first time and the modem is powered off and there are errors to wait for, wait for an error timers to pass
            if (lastSessionCounter.IsRunning() && !isPowered && errorCounter.IsRunning())
            {
                if (commError)
                    DoWait(terminateLock, errorCounter, settings.GetCommFailureDelayMS(), "Comm Fail Delay");
                else if (networkError)
                    DoWait(terminateLock, errorCounter, settings.GetNetworkFailDelayMS(), "Network Fail Delay");

                errorCounter.Reset();
            }

            // wait for a bit if throttling commands after a failure
            if (failedCommandThrottleCounter.IsRunning())
                DoWait(terminateLock, failedCommandThrottleCounter, 5000, "Failed Command Throttle");

            // if we don't know the type of modem yet, we have to turn the modem on and ask
            if (mystats.model.empty())
                InitializeModem(terminateLock);

            // pull up data for the next message if nothing stored yet
            if (!messageData.size())
            {
                overrideMailboxCheckTime = 0;
                Fire_RequestDataToSend(TransmitBufferSizeFromModel(mystats.model), messageData, messageCookie, overrideMailboxCheckTime);

                if (terminateLock.try_lock())
                    throw ShutdownRequested("Shutdown requested during check for messages");
            }

            // if it's not the first time thru and nothing to send and no indication that there's more to read ...
            if (lastSessionCounter.IsRunning() && !messageData.size() && !moreToRead)
            {
                // determine how long to wait for the next mailbox check
                const unsigned mbchk0 = settings.GetMailboxCheckIntervalMS();
                const unsigned mbchk = mbchk0 ? max((unsigned) 30000, mbchk0) : 0;
                const unsigned controllingTimer = overrideMailboxCheckTime ? min(mbchk,overrideMailboxCheckTime) : mbchk;

                if (controllingTimer)
                {
                    const unsigned timeToNextCheck = lastSessionCounter.Remaining(controllingTimer);

                    // shut off the modem if in duty cycling mode
                    if ((timeToNextCheck > 10000) && settings.GetDutyCycleModem())
                        DeallocateModem();

                    // wait to be informed of a message or timeout
                    if (timeToNextCheck)
                    {
                        Logger::Locked(logger)->Debug << "Waiting " << timeToNextCheck << " ms for message available" << endl;
                        wakeupSem.TimedWait(timeToNextCheck, false);
                    }
                }
                else
                {
                    Logger::Locked(logger)->Debug << "Waiting indefinitely for message available" << endl;
                    wakeupSem.Wait();
                }
            }

            // determine if there's anything to send
            if (!messageData.size() && !mystats.model.empty())
            {
                overrideMailboxCheckTime = 0;
                Fire_RequestDataToSend(TransmitBufferSizeFromModel(mystats.model), messageData, messageCookie, overrideMailboxCheckTime);

                if (terminateLock.try_lock())
                    throw ShutdownRequested("Shutdown requested while waiting for new message");
            }

            // only do a session if there's a message or we're doing mailbox checks or we think there's more to read
            if (messageData.size() || settings.GetMailboxCheckIntervalMS() || moreToRead)
            {
                InitializeModem(terminateLock);

                SBDModem::SessionStatusExtended extendedStatus;
                ByteArray receivedPacketData;

                if (messageData.size())
                {
                    const char* pStart = (const char*) &messageData.at(0);
                    const char* pEnd = pStart+messageData.size();
                    const string temp(pStart, pEnd);

                    modemPtr->WriteMOBuffer(temp);
                }
                else
                    modemPtr->ClearSBDBuffers(true, false);

                mystats.communicating = true;
                Fire_Statistics(mystats);

                try
                {
                    const unsigned signalStrength = modemPtr->GetSignalStrength(true);
                    Logger::Locked(logger)->Info << "Signal strength = " << signalStrength << endl;
                }
                catch(...)
                {
                    Logger::Locked(logger)->Info << "Failed to read signal strength" << endl;
                }

                ++mystats.numSessions;
                double lat0, lon0;

                if (GetSavedPosition(lat0, lon0))
                    extendedStatus = modemPtr->SessionExecute(false, lat0, lon0);
                else
                    extendedStatus = modemPtr->SessionExecute(false);

                extendedStatus.ThrowError();
                mystats.accessOK = true;
                mystats.networkOK = true;
		mystats.networkTimeout = false;
		mystats.networkTimeoutStart = 0;

                const bool goodSession = extendedStatus.GoodMOStatus() || extendedStatus.GoodMTStatus();
                const bool goodReceive = (extendedStatus.mtStatus == SBDModem::SessionStatusExtended::MTSV_SBD_MESSAGE_RECEIVED);
                const bool goodTransmit = extendedStatus.GoodMOStatus() && messageData.size();

                if (goodSession)
                    ++mystats.numSessionsSuccessful;

                if (goodTransmit)
                    ++mystats.numSent;

                if (goodReceive)
                    ++mystats.numReceived;

                Fire_Statistics(mystats);

                moreToRead = !!extendedStatus.mtCountQueued;
                lastSessionCounter.Reset(0, true);

                if (goodTransmit)
                {
                    Fire_SentData(messageCookie, extendedStatus.moMSN);
                    messageData.clear();
                    messageCookie = 0;
                }

                if (goodReceive)
                {
                    const string temp = modemPtr->ReadMTBuffer();

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

                        receivedPacketData.assign(pStart, pEnd);

                        Fire_ReceivedData(receivedPacketData, extendedStatus.mtMSN);
                    }
                }
            }

            const unsigned maxon = settings.GetMaxOnTimeMS();

            if (settings.GetDutyCycleModem() && modemUpCounter.IsRunning() && maxon && !modemUpCounter.Remaining(maxon) && modemPtr)
            {
                Logger::Locked(logger)->Warning << "Max modem on time exceeded, cycling power to modem" << endl;
                DeallocateModem();
            }
        }
        catch(SBDModem::AccessDenied& accessDeniedEx)
        {
            Logger::Locked(logger)->Warning << "Caught access denied exception '" << accessDeniedEx.what() << "'" << endl;
            mystats.accessOK = false;
            Fire_Statistics(mystats);
            DeallocateModem();
            failedCommandThrottleCounter.Reset(0, true);
        }
        catch(SBDModem::ActionFailed& accessFailedEx)
        {
            Logger::Locked(logger)->Warning << "Caught action failed exception '" << accessFailedEx.what() << "'" << endl;
            failedCommandThrottleCounter.Reset(0, true);
        }
        catch(SBDModem::BusyTryLater& busyEx)
        {
            Logger::Locked(logger)->Warning << "Caught busy exception '" << busyEx.what() << "'" << endl;
            failedCommandThrottleCounter.Reset(0, true);
        }
        catch(SBDModem::NetworkError& networkEx)
        {
            Logger::Locked(logger)->Warning << "Caught network error exception '" << networkEx.what() << "'" << endl;

	    if (mystats.networkOK)
		    mystats.networkTimeoutStart = GetCurrentTime();

	    mystats.networkOK = false;
	    mystats.networkTimeout = false;

            Fire_Statistics(mystats);

            if (networkUpCounter.IsRunning() && !networkUpCounter.Remaining(settings.GetNetworkAvailableTimeoutMS()))
            {
                networkError = true;
                errorCounter.Reset(0, true);
                DeallocateModem();
            }
            else
            {
                TimeCounter commandThrottleCounter;
                DoWait(terminateLock, commandThrottleCounter, 5000, "Command Throttle Delay");
            }
        }
        catch(SBDModem::ProtocolError& protocolEx)
        {
            Logger::Locked(logger)->Warning << "Caught protocol error exception '" << protocolEx.what() << "'" << endl;
            mystats.communicating = false;
            Fire_Statistics(mystats);
            commError = true;
            errorCounter.Reset(0, true);
            DeallocateModem();
        }
        catch(SBDModem::ProtocolTimeout& protocolEx)
        {
            Logger::Locked(logger)->Warning << "Caught protocol timeout exception '" << protocolEx.what() << "'" << endl;
            mystats.communicating = false;
            Fire_Statistics(mystats);
            commError = true;
            errorCounter.Reset(0, true);
            DeallocateModem();
        }
        catch(ShutdownRequested& shutEx)
        {
            shutdownException = true;
            Logger::Locked(logger)->Warning << "Caught shutdown exception: " << shutEx.what() << endl;
        }
        catch(exception& ex)
        {
            Logger::Locked(logger)->Error << "Caught exception " << typeid(ex).name() << " '" << ex.what() << "'" << endl;
            mystats.communicating = false;
            Fire_Statistics(mystats);
            commError = true;
            errorCounter.Reset(0, true);
            DeallocateModem();
        }
    }

    try
    {
        DeallocateModem();
    }
    catch(exception& ex)
    {
        Logger::Locked(logger)->Warning << "Caught " << typeid(ex).name() << " '" << ex.what() << "' while deallocating modem on exit from background thread" << endl;
    }

    Logger::Locked(logger)->Warning << "Exiting SSQCommunicator background thread" << endl;
}

void SSQCommunicator::DeallocateModem()
{
    if (modemPtr)
    {
        modemPtr->Shutdown();
        modemPtr.reset();
    }

    modemUpCounter.Reset();
    networkUpCounter.Reset();

    if (isPowered)
    {
        PowerModem(false);
        modemOffCounter.Reset(0, true);
    }
}

void SSQCommunicator::InitializeModem(Semaphore::Lock& lock)
{
    if (modemOffCounter.IsRunning())
    {
        DoWait(lock, modemOffCounter, settings.GetMinOffTimeMS(), "Modem Off Time");
        modemOffCounter.Reset();
    }

    try
    {
        // if not powered up yet, turn on the power and wait for on delay
        if (!isPowered)
        {
            PowerModem(true);
            networkUpCounter.Reset(0, true);

            if (settings.GetModemPowerOnDelayMS())
            {
				TimeCounter modemOnCounter;
				DoWait(lock, modemOnCounter, settings.GetModemPowerOnDelayMS(), "Modem On Delay");
				modemUpCounter.Reset(0, true);
            }
        }

        // if there's no modem instance ...
        bool newInstance = !modemPtr;
        if (!modemPtr)
        {
            // instantiate the modem
            sbdModemSettings = settings.GetSBDModemSettings();
            modemPtr.reset(new SBDModem(logger, sbdModemSettings));

            ringIndSubscription = modemPtr->SubscribeRing(boost::bind(&SSQCommunicator::OnRingIndicator, this, _1));

            // initialize the instance, clean up if it doesn't initialize
            try
            {
            	modemPtr->Intialize();
            }
            catch(...)
            {
            	{ ILockableConceptLock lock(modemPtr->GetEventLock()); ringIndSubscription.reset(); }
            	modemPtr.reset();
            	throw;
            }

        }

        // if not communicating yet, attempt communication
        if (!lock.owns_lock() && !lock.try_lock() && (!mystats.communicating || newInstance))
        {
            // attempt to configure the the interface and get the model number
            try
            {
                if (!lock.owns_lock() && !lock.try_lock())
                    modemPtr->ConfigureInterface();
                SBDModem::IndicatorEventReporting ierSettings(false, false);
                if (!lock.owns_lock() && !lock.try_lock())
                    modemPtr->SetIndicatorEventReporting(ierSettings);
                if (!lock.owns_lock() && !lock.try_lock())
                    modemPtr->SetRingIndication(false);
                if (!lock.owns_lock() && !lock.try_lock())
                    mystats.model = modemPtr->GetModel();
                if (!lock.owns_lock() && !lock.try_lock())
                    mystats.serialNum = modemPtr->GetSerialNumber();

                if (!mystats.model.empty() && boost::starts_with(mystats.model, "95"))
                {
					if (!lock.owns_lock() && !lock.try_lock())
						modemPtr->SetNetworkRegistration(SBDModem::NREG_NOTIFY_OFF);

					mystats.simNum = modemPtr->GetSIMCardNumber();
				}
                else
                	mystats.simNum.clear();

                // some models of 9602 require waiting for system time to be available, and it
                // doesn't hurt
                do
                {
                	bool waiting;
                	bool first = true;

                	do
                	{
                		if (first)
                			first = false;
                		else
                		{
    						TimeCounter pollingCounter;
    						DoWait(lock, pollingCounter, 3000, "Network Status Polling Delay");
            			}

                		try
                		{
                			modemPtr->RequestSystemTime();
                			waiting = false;
                		}
                		catch(SBDModem::ActionFailed& afex)
                		{
                			waiting = true;
                		}

                	} while(waiting && networkUpCounter.Remaining(settings.GetNetworkAvailableTimeoutMS()));

                	if (waiting)
                		throw SBDModem::NetworkError("Failed to get system time within configured network registration timeout");

                } while(false);

                if (boost::starts_with(mystats.model, "95"))
                {
					SBDModem::NetworkRegistration nreg = modemPtr->GetNetworkRegistration();
					bool stillSearching;

					while(stillSearching = (nreg.status == SBDModem::NREG_STATUS_NOT_REGISTERED_SEARCHING) || (nreg.status == SBDModem::NREG_STATUS_UNKNOWN),
						   stillSearching && networkUpCounter.Remaining(settings.GetNetworkAvailableTimeoutMS()))
					{
						TimeCounter pollingCounter;
						DoWait(lock, pollingCounter, 3000, "Network Status Polling Delay");
						nreg = modemPtr->GetNetworkRegistration();
					}

					if (stillSearching)
						throw SBDModem::NetworkError("Timed out while waiting for a good network status after power up");

					if (nreg.status == SBDModem::NREG_STATUS_NOT_REGISTERED_IDLE)
						throw SBDModem::NetworkError("Modem stopped attempting to register with network");

					if (nreg.status == SBDModem::NREG_STATUS_REGISTRATION_DENIED)
						throw SBDModem::NetworkError("Modem network registration was denied");

					if ((nreg.status != SBDModem::NREG_STATUS_REGISTERED_AT_HOME) && (nreg.status != SBDModem::NREG_STATUS_REGISTERED_ROAMING))
						throw SBDModem::NetworkError("Unexpected network registration status");

					networkUpCounter.Reset();		// network is known to be up
				}

                if (lock.owns_lock())
                    throw ShutdownRequested("Shutdown requested while initializing modem connection");
            }
            catch(...)
            {
                mystats.communicating = false;
                Fire_Statistics(mystats);
                throw;
            }

            // mark as communicating
            mystats.communicating = true;
            Fire_Statistics(mystats);
        }
    }
    catch(SBDModem::NetworkError& nex)
    {
    	throw;
    }
    catch(...)
    {
        networkUpCounter.Reset();
        throw;
    }
}

void SSQCommunicator::DoWait(Semaphore::Lock& lock, TimeCounter& tc, unsigned limit, const string& description)
{
    using namespace boost::posix_time;

    if (tc.IsRunning())
    {
        if (!limit)
            Logger::Locked(logger)->Debug << description << " = 0" << endl;
        else
        {
            const unsigned timeRemaining = tc.Remaining(limit);

            if (timeRemaining)
            {
                Logger::Locked(logger)->Debug << "Waiting " << timeRemaining << " ms for " << description << endl;

                if (lock.timed_lock(millisec(timeRemaining)))
                {
                    stringstream stm;
                    stm << "Shutdown requested during wait for " << description;

                    string msg = stm.str();
                    throw ShutdownRequested(msg);
                }
            }
        }

        tc.Reset(0, false);
    }
    else
        Logger::Locked(logger)->Debug << "Timer for " << description << " is not running" << endl;
}

void SSQCommunicator::PowerModem(bool powerOn)
{
    string verb;

    if (powerOn)
        verb = settings.GetPowerOnVerb();
    else
        verb = settings.GetPowerOffVerb();

    const string powerTarget = powerOn ? "ON" : "OFF";

    if (verb.empty())
        Logger::Locked(logger)->Warning << "No verb defined for power " << powerTarget << endl;
    else
    {
        Logger::Locked(logger)->Info << "Executing '" << verb << "' to set power " << powerTarget << endl;
        const int status = system(verb.c_str());

        if (WIFEXITED(status) && !WEXITSTATUS(status))
            Logger::Locked(logger)->Info << "Power " << powerTarget << " verb succeeded" << endl;
        else if (WIFEXITED(status))
            Logger::Locked(logger)->Warning << "Power " << powerTarget << " verb exited with status " << WEXITSTATUS(status) << endl;
        else
            Logger::Locked(logger)->Warning << "Power " << powerTarget << " verb terminated abnormally" << endl;
    }

    isPowered = powerOn;
}

void SSQCommunicator::Fire_ReceivedData(const ByteArray& packetData, unsigned MTMSN)
{
	Logger::Locked(logger)->Notice << "ReceivedData event data=" << packetData.ToHexString() << " MTMSN=" << MTMSN << endl;

    try { receivedDataSignal(*this, packetData, MTMSN); }
    catch(...) { }
}

void SSQCommunicator::Fire_RequestDataToSend(size_t maxPacketSize, ByteArray& packetData, CookieType& cookie, unsigned& newMailboxCheckTimeMS)
{
	Logger::Locked(logger)->Notice << "RequestDataToSend event maxPacketSize=" << maxPacketSize << endl;

	try { requestDataToSendSignal(*this, maxPacketSize, packetData, cookie, newMailboxCheckTimeMS); }
    catch(...) { }
}

void SSQCommunicator::Fire_SentData(CookieType cookie, unsigned MOMSN)
{
	Logger::Locked(logger)->Notice << "SentData event cookie=" << cookie << " MOSMN=" << MOMSN << endl;

    try { sentDataSignal(*this, cookie, MOMSN); }
    catch(...) { }
}

void SSQCommunicator::Fire_Statistics(Statistics& newValue, bool onlyOnChange)
{
	if (!mystats.communicating || !mystats.accessOK)
		mystats.reliable = false;
	else
	{
		if (!mystats.networkOK && ((GetCurrentTime() - mystats.networkTimeoutStart) <= settings.GetNetworkTimeoutSeconds()))
			mystats.reliable = false;
		else
			mystats.reliable = true;
	}

	if (!onlyOnChange || (newValue != stats))
	{
		stats = newValue;
		Logger::Locked(logger)->Notice << "Statistics event " << stats.ToString() << endl;
		try { statisticsSignal(*this, newValue); }
		catch(...) { }
	}
}

size_t SSQCommunicator::TransmitBufferSizeFromModel(const string& model)
{
    size_t result;

    if (boost::starts_with(model, "95"))
        result = 1960;
    else
        result = 340;

    return result;
}

SSQCommunicator::ScopedConnectionPtr SSQCommunicator::SubscribeReceivedData(ReceivedDataSignalType::slot_type slot)
{
    ScopedConnectionPtr result;
    result.reset(new ScopedConnection(receivedDataSignal.connect(slot)));
    return result;
}

SSQCommunicator::ScopedConnectionPtr SSQCommunicator::SubscribeRequestDataToSend(RequestDataToSendSignalType::slot_type slot)
{
    ScopedConnectionPtr result;
    result.reset(new ScopedConnection(requestDataToSendSignal.connect(slot)));
    return result;
}

SSQCommunicator::ScopedConnectionPtr SSQCommunicator::SubscribeSentData(SentDataSignalType::slot_type slot)
{
    ScopedConnectionPtr result;
    result.reset(new ScopedConnection(sentDataSignal.connect(slot)));
    return result;
}

SSQCommunicator::ScopedConnectionPtr SSQCommunicator::SubscribeStatistics(StatisticsSignalType::slot_type slot)
{
    ScopedConnectionPtr result;
    result.reset(new ScopedConnection(statisticsSignal.connect(slot)));
    return result;
}

SSQCommunicator::Statistics::Statistics()
: communicating(false)
, networkOK(false)
, accessOK(false)
, numSent(0)
, numReceived(0)
, numSessions(0)
, numSessionsSuccessful(0)
, reliable(false)
, networkTimeout(true)
, networkTimeoutStart(0)
{
}

bool SSQCommunicator::Statistics::operator==(const SSQCommunicator::Statistics& other) const
{
	const bool result =
           (communicating == other.communicating)
		&& (networkOK == other.networkOK)
		&& (accessOK == other.accessOK)
		&& boost::equals(model, other.model)
		&& boost::equals(serialNum, other.serialNum)
		&& boost::equals(simNum, other.simNum)
		&& (numSent == other.numSent)
		&& (numReceived == other.numReceived)
		&& (numSessions == other.numSessions)
		&& (numSessionsSuccessful == other.numSessionsSuccessful);

	return result;
}

bool SSQCommunicator::Statistics::operator!=(const SSQCommunicator::Statistics& other) const
{
	return !(*this == other);
}

string SSQCommunicator::Statistics::ToString() const
{
	stringstream stm;
	stm << "communicating=" << BooleanToString(communicating)
	    << " networkOK=" << BooleanToString(networkOK)
	    << " accessOK=" << BooleanToString(accessOK)
	    << " model='" << model << "'"
	    << " serialNum='" << serialNum << "'"
	    << " simNum='" << simNum << "'"
	    << " numSent=" << numSent
	    << " numReceived=" << numReceived
	    << " numSessions=" << numSessions
	    << " numSessionsSuccessful=" << numSessionsSuccessful
	    << " realiable=" << BooleanToString(reliable);

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

double SSQCommunicator::GetCurrentTime()
{
	timeval tv;
	double result = 0;

	if (!gettimeofday(&tv, NULL))
	{
		tm tmbuf;
		gmtime_r(&tv.tv_sec, &tmbuf);

		result = tv.tv_sec + (tv.tv_usec/1000000.0);
	}

	return result;
}

void SSQCommunicator::OnRingIndicator(SBDModem& sender)
{
    Logger::Locked(logger)->Notice << "Ring indicator says message available" << endl;
    if (!wakeupSem.GetCount())
        wakeupSem.Post();
}

}
}
