//---------------------------------------------------------------------------------
// <copyright file="PPPHost.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 PPPHost class, which provides tools to connect to the
//   internet via PPP and a modem and fires events for status changes.
//
//	 pppd is expected to output:
//
//		"Connect: ppp" when starting the ppp protocol handshaking (CONNECTING state)
//
//		"local" followed by "IP address" followed by the local IP address when it is
//		estalbished
//
//		"remote" followed by "IP address" followed by the remote IP address when it is
//		estalbished
//
//		In CONNECTING state, establishing both local and remote IP addresses represents
//		good connection (CONNECTED state)
// </summary>
//
// <owner>Mike Cookson</owner>
//---------------------------------------------------------------------------------

#include "PPPHost.h"
#include "LqrSMACGetErrorDescription.h"
#include "LqrSMACUptime.h"
#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>
#include <boost/foreach.hpp>
#include <boost/lexical_cast.hpp>

using namespace std;
using namespace LiquidRobotics::SMACFramework;

namespace LiquidRobotics {
namespace PPPHostLib {

IMPLEMENT_INNER_EXCEPTION(PPPHost, ModemPowerControlFailure, std::runtime_error);
IMPLEMENT_INNER_EXCEPTION(PPPHost, ModemResponseTimeout, std::runtime_error);
IMPLEMENT_INNER_EXCEPTION(PPPHost, ModemRegistrationRefused, std::runtime_error);
IMPLEMENT_INNER_EXCEPTION(PPPHost, ModemRegistrationTimeout, std::runtime_error);
IMPLEMENT_INNER_EXCEPTION(PPPHost, NotDisconnected, std::logic_error);
IMPLEMENT_INNER_EXCEPTION(PPPHost, PPPConnectionFailed, std::runtime_error);
IMPLEMENT_INNER_EXCEPTION(PPPHost, ReceivedFailingResponse, std::runtime_error);
IMPLEMENT_INNER_EXCEPTION(PPPHost, ResponseTimeout, std::runtime_error);
IMPLEMENT_INNER_EXCEPTION(PPPHost, ServerDialupFailed, std::runtime_error);
IMPLEMENT_INNER_EXCEPTION(PPPHost, ShutDown, std::runtime_error);
IMPLEMENT_INNER_EXCEPTION(PPPHost, SystemError, std::runtime_error);

/*static void ThrowSystemError(int err, string msg)
{
	char temp[256];
	stringstream stm;

	strerror_r(err, temp, sizeof(temp));
	stm << msg << temp;
	throw PPPHost::SystemError(stm.str());
}*/

PPPHost::PPPHost(const PPPHostSettings& _pppSettings, Logger::Ptr _logger)
: pppSettings(_pppSettings)
, logger(_logger)
, connectionStatus(DISCONNECTED)
, shutDown(false)
, pThread(NULL)
, toThreadReadPtr(NULL)
, queueToThread(_pppSettings.GetQueueCapacity())
{
}

PPPHost::~PPPHost()
{
#if 0
	Logger::Ptr log2 = logger;
	if (log2)
	{
		Logger::Lock logLock(*log2);
		log2->Debug << "~PPPHost" << endl;
	}
#else
	Shutdown();
#endif
}

void PPPHost::Connect()
{
	MutexLock connectionLock(connectionMutex);
	Lock lock(*this);

	if (shutDown) throw ShutDown("PPPHost cannot connect: already shut down");

	QueuePtrLifespan myQueue(lock, queueList, pppSettings.GetQueueCapacity());

	switch(connectionStatus)
	{
	case CONNECTED:

		break;

	case DISCONNECTING:

		lock.unlock();
		AwaitDisconnectionActionComplete(myQueue.get());
		lock.lock();

		if (connectionStatus == CONNECTED)
			break;

		// falling through

	case DISCONNECTED:

		if (connectionStatus == DISCONNECTED)
		{
			StartConnectionAction(myQueue.get());

			if (connectionStatus == CONNECTED)
				break;
		}

		// falling through

	case INITIALIZING_MODEM:
	case DIALING:
	case CONNECTING:

		if (lock) lock.unlock();
		AwaitConnectionActionComplete(myQueue.get());
		break;

	default:

		if (logger)
		{
			Logger::Lock loglock(*logger);
			logger->Warning << "PPPHost unexpected initial value for connectionStatus (" << connectionStatus << ") in connection attempt" << endl;
		}

		break;
	}
}

void PPPHost::Disconnect()
{
	MutexLock connectionLock(connectionMutex);
	Lock lock(*this);

	if (shutDown) throw ShutDown("PPPHost::Disconnect: already shut down");

	if (logger)
	{
		Logger::Lock logLock(*logger);
		logger->Debug << "PPPHost::Disconnect initial state = " << connectionStatus << endl;
	}

	QueuePtrLifespan myQueue(lock, queueList, pppSettings.GetQueueCapacity());

	switch(connectionStatus)
	{
	case DISCONNECTED:

		break;

	case INITIALIZING_MODEM:
	case DIALING:
	case CONNECTING:
	case CONNECTED:

		StartDisconnectionAction(myQueue.get());

		// falling through

	case DISCONNECTING:

		lock.unlock();
		AwaitDisconnectionActionComplete(myQueue.get());
		break;

	default:

		if (logger)
		{
			Logger::Lock loglock(*logger);
			logger->Warning << "PPPHost unexpected initial value for connectionStatus (" << connectionStatus << ") in disconnection attempt" << endl;
		}

		break;
	}
}

PPPHost::ConnectionStatus PPPHost::GetConnectionStatus()
{
	Lock lock(*this);
	return connectionStatus;
}

#if 0
string PPPHost::GetIpAddress()
{
	return "none";
}
#endif

string PPPHost::GetModemSerialNumber() const
{
	Lock lock(const_cast<PPPHost&>(*this));
	return modemSerialNumber;
}

string PPPHost::GetModemSimCardNumber() const
{
	Lock lock(const_cast<PPPHost&>(*this));
	return modemSimCardNumber;
}

bool PPPHost::IsConnected()
{
	Lock lock(*this);
	return connectionStatus == CONNECTED;
}

void PPPHost::Shutdown()
{
	Lock lock(*this);

	if (!shutDown)
	{
		if (pThread)
		{
			QueueEntry qe(QueueEntry::SHUT_DOWN);
			queueToThread.Write(qe);
			lock.unlock();
			pThread->join();
		}
		else
			shutDown = true;
	}

	SetModemPower(0);
}

PPPHost::ScopedConnectionPtr PPPHost::Subscribe(ConnectionSignalType::slot_type slot)
{
	ScopedConnectionPtr result;

	result.reset(new ScopedConnection(connectionStateChanged.connect(slot)));
	return result;
}

void PPPHost::lock()
{
	mutex.lock();
}

bool PPPHost::try_lock()
{
	return mutex.try_lock();
}

void PPPHost::unlock()
{
	mutex.unlock();
}

void PPPHost::operator()()
{
	bool looping = true;
	ConnectionStatus nextStatus = DISCONNECTED;

	NewConnectionStatus(DISCONNECTED, false);

	while(looping)
	{
		QueueEntry qe = queueToThread.Read();

		switch(qe.type)
		{
		case QueueEntry::START_CONNECT:

			nextStatus = CONNECTING;
			break;

		case QueueEntry::START_DISCONNECT:

			nextStatus = DISCONNECTING;;
			break;

		case QueueEntry::SHUT_DOWN:

			looping = false;
			break;

		case QueueEntry::PPP_DISCONNECTED:

			if (connectionStatus == CONNECTED)
				nextStatus = DISCONNECTING;

			break;

		default:

			break;
		}

		if (looping)
		{
			try
			{
				if ((nextStatus == CONNECTING) && (connectionStatus == DISCONNECTED))
					DoConnect();
				else if ((nextStatus == DISCONNECTING) && (connectionStatus == CONNECTED))
					DoDisconnect();
			}
			catch(ShutDown& shutdownEx)
			{
				looping = false;

				if (logger)
				{
					Logger::Lock loglock(*logger);
					logger->Error << "PPPHost background thread caught ShutDown and will now exit" << endl;
				}
			}
			catch(exception& ex)
			{
				if (logger)
				{
					Logger::Lock lock(*logger);
					logger->Error << "PPPHost background thread caught exception of type " << typeid(ex).name()
								 << ": '" << ex.what() << "' and will now exit" << endl;
				}

				looping = false;
			}
		}
	}

	shutDown = true;
	NewConnectionStatus(DISCONNECTED);
}

void PPPHost::StartConnectionAction(QueuePtrType qptr)
{
	Lock lock(*this);

	if (NULL == pThread)
		pThread = new boost::thread(boost::ref(*this));

	QueueEntry qe(QueueEntry::START_CONNECT);
	queueToThread.Write(qe);
}

void PPPHost::AwaitConnectionActionComplete(QueuePtrType qptr)
{
	bool looping = true;

	while(looping)
	{
		QueueEntry qe = qptr->Read();

		switch(qe.type)
		{
		case QueueEntry::ACTION_SUCCEEDED:
			looping = false;
			break;

		case QueueEntry::ACTION_FAILED:
			qe.Rethrow();
			break;

		case QueueEntry::SHUT_DOWN:
			throw ShutDown("PPPHost detected object shut down while awaiting connection complete");

		default:

			if (logger)
			{
				Logger::Lock loggerlock(*logger);
				logger->Warning << "PPPHost got unexpected message type " << qe.type << " while awaiting connection complete";
			}

			break;
		}
	}
}

void PPPHost::StartDisconnectionAction(QueuePtrType qptr)
{
	QueueEntry qe(QueueEntry::START_DISCONNECT);
	queueToThread.Write(qe);
}

void PPPHost::AwaitDisconnectionActionComplete(QueuePtrType qptr)
{
	bool looping = true;

	while(looping)
	{
		QueueEntry qe = qptr->Read();

		switch(qe.type)
		{
		case QueueEntry::ACTION_SUCCEEDED:
			looping = false;
			break;

		case QueueEntry::ACTION_FAILED:
			qe.Rethrow();
			break;

		case QueueEntry::SHUT_DOWN:
			throw ShutDown("PPPHost detected object shut down while awaiting connection complete");

		default:

			if (logger)
			{
				Logger::Lock loggerlock(*logger);
				logger->Warning << "PPPHost got unexpected message type " << qe.type << " while awaiting disconnection complete";
			}

			break;
		}
	}
}

void PPPHost::NewConnectionStatus(ConnectionStatus newStatus, bool fireOnChange)
{
	MutexLock lock(firingMutex);
	const ConnectionStatus oldStatus = connectionStatus;
	const bool changed = connectionStatus != newStatus;
	const bool firing = changed || !fireOnChange;

	connectionStatus = newStatus;

	if (logger)
	{
		Logger::Lock loglock(*logger);
		logger->Notice << "PPPHost new connection status is " << newStatus << " was " << oldStatus;

		if (!firing)
			logger->Notice << " (not firing event)";

		logger->Notice << endl;
	}

	if (!fireOnChange || changed)
		connectionStateChanged(*this, newStatus);
}

PPPHost::ConnectionStatus PPPHost::DoConnect()
{
	try
	{
		NewConnectionStatus(INITIALIZING_MODEM);
		SetModemPower(true);

		{
			SerialPortPtr serialPort = pppSettings.GetSerialPortSettings().CreatePort();// logger);

			SerialLineMachine::Ptr lineMachine;
			lineMachine.reset(new SerialLineMachine(queueToThread, serialPort, logger));
			lineMachine->Initialize();

			ConfigureModem(serialPort);

			NewConnectionStatus(DIALING);

			if (pppSettings.GetPhoneNumber().empty())
			{
				if (logger)
				{
					Logger::Lock loglock(*logger);
					logger->Info << "Skipping dialing phase since no phone number was specified" << endl;
				}
			}
			else
				DialGateway(serialPort);

			// line machine and serial port deleted at the end of this scope
		}

		//NewConnectionStatus(CONNECTING);
		StartProtocolDaemon();

		QueueEntry qe(QueueEntry::ACTION_SUCCEEDED);
		WriteToQueueList(qe);
		return CONNECTED;
	}
	catch(exception& ex)
	{
		if (logger)
		{
			Logger::Lock loglock(*logger);
			logger->Error << "DoConnect caught " << typeid(ex).name() << ": " << ex.what() << endl;
		}

		NewConnectionStatus(DISCONNECTING);
		apeClient.reset();
		SetModemPower(0);
		Delay(pppSettings.GetPowerOffDelaySeconds());
		NewConnectionStatus(DISCONNECTED);

		QueueEntry qe(QueueEntry::ACTION_FAILED, typeid(ex), ex.what());
		WriteToQueueList(qe);
		return DISCONNECTED;
	}
}

PPPHost::ConnectionStatus PPPHost::DoDisconnect()
{
	NewConnectionStatus(DISCONNECTING);
	apeClient.reset();
	SetModemPower(0);
	Delay(pppSettings.GetPowerOffDelaySeconds());
	NewConnectionStatus(DISCONNECTED);

	QueueEntry qe(QueueEntry::ACTION_SUCCEEDED);
	WriteToQueueList(qe);
	return DISCONNECTED;
}

void PPPHost::SetModemPower(bool on)
{
	string command = on ? pppSettings.GetPowerOnVerb() : pppSettings.GetPowerOffVerb();

	if (!command.empty())
	{
		int status = system(command.c_str());

		if (status)
		{
			stringstream msg;
			msg << "PPPHost setting modem power " << (on ? "ON" : "OFF") << " verb='" << command << "' failed with exit code " << status;
			throw ModemPowerControlFailure(msg.str());
		}

		if (on && pppSettings.GetPowerOnDelaySeconds())
			Delay(pppSettings.GetPowerOnDelaySeconds());
	}
}

void PPPHost::Delay(float seconds, bool throwOnShutdown)
{
	float startTime = GetSystemUpTimeSeconds();
	bool timeout = false;

	while(!shutDown && !timeout)
	{
		const float elapsed = GetSystemUpTimeSeconds()-startTime;
		const unsigned millisecRemaining = (elapsed >= seconds) ? 0 : (unsigned) ceil(1000*(seconds-elapsed));
		QueueType::Lock qlock(queueToThread, boost::defer_lock);

		if (queueToThread.AwaitReadable(millisecRemaining, &qlock, false))
		{
			if (queueToThread.Read().type == QueueEntry::SHUT_DOWN)
			{
				// (shutDown should now be set)
				if (throwOnShutdown)
					throw ShutDown("PPPHost detected object shutdown while delaying");
			}
		}
		else
			timeout = true;
	}
}

void PPPHost::ConfigureModem(SerialPortPtr serialPort)
{
	serialPort->SetRts(true);
	serialPort->SetDtr(true);

	const string OK = "OK";
	const string ERROR = "ERROR";

	SendExpect(serialPort, "ATV1", OK, ERROR);				// turn on verbose mode so we get OK and ERROR, etc.
	SendExpect(serialPort, "ATE0", OK, ERROR);				// turn echo off
	SendExpect(serialPort, "AT&K3", OK, ERROR);				// set to RTS/CTS flow control
	SendExpect(serialPort, "AT&D2", OK, ERROR);				// DTR transaction from ON to OFF during call brings modem on on-hook command mode
	SendExpect(serialPort, "ATS0=0", OK, ERROR);			// do not auto-answer
	SendExpect(serialPort, "AT+SBDMTA=0", OK, ERROR);		// disable SBD ring indication
	SendExpect(serialPort, "AT+CIER=0,0,0", OK, ERROR);		// turn off signal strength reports (automated reports interfere with single line responses)
	SendExpect(serialPort, "AT+CREG=0", OK, ERROR);			// turn off registration reports (automated reports interfere with single line responses)

	string line;
	SendExpectSingle(serialPort, "AT+CGMI", line, "", OK, ERROR);	 // get manufacturing ID (Iridium typically) for log

	if (logger)
	{
		Logger::Lock loglock(*logger);
		logger->Info << "Modem Manufacturer = " << line << endl;
	}

	SendExpectSingle(serialPort, "AT+CGMM", line, "", OK, ERROR);	// get manufacturing model (9522B typically) for log

	if (logger)
	{
		Logger::Lock loglock(*logger);
		logger->Info << "Modem Model = " << line << endl;
	}

	// wait for the modem to register with the Iridium network
	AwaitModemRegistration(serialPort);

	SendExpectSingle(serialPort, "AT+CGSN", line, "", OK, ERROR);		// get the IMEI for the log

	if (logger)
	{
		Logger::Lock loglock(*logger);
		logger->Info << "IMEI = " << line << endl;
	}

	{
		Lock lock(*this);
		modemSerialNumber = line;
	}

	SendExpectSingle(serialPort, "AT+CICCID", line, "", OK, ERROR);

	if (logger)
	{
		Logger::Lock loglock(*logger);
		logger->Info << "SIM card ID = " << line << endl;
	}

	{
		Lock lock(*this);
		modemSimCardNumber = line;
	}

	SendExpectSingle(serialPort, "AT+CPIN?", line, "+CPIN:", OK, ERROR);

	if (line.compare("+CPIN:SIM PIN2"))
	{
		stringstream msg;
		msg << "The SIM pin is not unlocked or is not ready (" << line << ")";

		throw ModemRegistrationTimeout(msg.str());
	}

	SendExpect(serialPort, "AT+CBST=71,0,1", OK, ERROR);
}

void PPPHost::AwaitModemRegistration(SerialPortPtr& serialPort)
{
	const string OK = "OK";
	const string ERROR = "ERROR";

	SendExpect(serialPort, "AT+CIER=1,1,1", OK, ERROR);		// re-enable signal strength indications for log
	SendExpect(serialPort, "AT+CREG=2", OK, ERROR);			// enable Iridium network registration reports

	string line;
	bool registered = false;

	SendExpectSingle(serialPort, "AT+CREG?", line, "+CREG", OK, ERROR); 	// get the current registration status

	const float regStart = GetSystemUpTimeSeconds();

	do
	{
		const int regStatus = RegistrationStatus(line);

		if (logger)
		{
			Logger::Lock loglock(*logger);
			logger->Info << "Iridium network registration status = " << (long) regStatus << endl;
		}

		switch(regStatus)
		{
		case 1:				// registered, home network
		case 5:				// registered, roaming

			registered = true;
			break;

		case 3:				// registration denied

			throw ModemRegistrationRefused("Modem returned registration status 3 (registration denied)");

		case 0:				// not registered, ISU is not currently search a new operator to register to

			throw ModemRegistrationTimeout("Modem returned registration status 0 (not registered, not searching for a new operator)");

		case 2:				// not registered, but ISU is currently search a new operator to register to
		case 4:				// unknown (initial state)

			break;

		default:
			{
				stringstream msg;
				msg << "Received unexpected registration code " << regStatus << endl;

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

		if (!registered)
		{
			const float elapsed = GetSystemUpTimeSeconds() - regStart;
			const float remaining = pppSettings.GetModemRegistrationTimeoutSeconds() - elapsed;
			bool timeout = (remaining <= 0);

			if (!timeout)
				try
				{
					ExpectSingle(serialPort, line, "+CREG", remaining);
				}
				catch(ModemResponseTimeout& timeoutEx)
				{
					timeout = true;
				}

			if (timeout)
				throw ModemRegistrationTimeout("Timeout during modem registration");
		}

	} while(!registered);

	SendExpect(serialPort, "AT+CIER=0,0,0", OK, ERROR);					// turn off signal strength reports
	SendExpect(serialPort, "AT+CREG=0", OK, ERROR);						// turn off registration reports
}

void PPPHost::DialGateway(SerialPortPtr& serialPort)
{
	stringstream dialCmd;
	dialCmd << "ATD" << pppSettings.GetPhoneNumber();

	Send(serialPort, dialCmd.str());

	float startTime = GetSystemUpTimeSeconds();
	bool looping = true;

	while(looping)
	{
		string line;
		float elapsed = GetSystemUpTimeSeconds()-startTime;
		float remaining = pppSettings.GetDialupTimeoutSeconds()-elapsed;
		bool timeout = (remaining <= 0);

		if (!timeout)
		{
			try
			{
				ExpectSingle(serialPort, line, remaining);
			}
			catch(ModemResponseTimeout& responseTimeoutEx)
			{
				timeout = true;
			}
		}

		if (timeout)
			throw ServerDialupFailed("Timeout while waiting for response to dialup command");

		if (boost::starts_with(line, "CONNECT"))
			looping = false;

		else if (!line.compare("NO CARRIER")
			  || !line.compare("NO ANSWER")
			  || !line.compare("ERROR")
			  || !line.compare("NO DIALTONE")
			  || !line.compare("BUSY"))
		{
			stringstream msg;
			msg << "Dial-in failed with '" << line << "' message";
			throw ServerDialupFailed(msg.str());
		}

		else
		{
			stringstream msg;
			msg << "Dial-in failed with unexpected message '" << line << "'";
			throw ServerDialupFailed(msg.str());
		}
	}
}

int PPPHost::RegistrationStatus(const string& line)
{
	const string creg = "+CREG:";

	if (!boost::starts_with(line, creg))
	{
		stringstream msg;
		msg << "Registration status line (" << line << ") is exepected to start with " << creg;
		throw invalid_argument(msg.str());
	}

	StringArray tokens;
	string tail = line.substr(creg.length());

	boost::split(tokens, tail, boost::is_any_of(","));

	if (tokens.size() < 2)
	{
		stringstream msg;
		msg << "Registration status line (" << line << ") token count expected to be at least 2";
		throw invalid_argument(msg.str());
	}

	return boost::lexical_cast<int>(tokens[1]);
}

void PPPHost::StartProtocolDaemon()
{
	// splits this process into two: child runs pppd and parent monitors the progress

	if (logger)
	{
		Logger::Lock loglock(*logger);
		logger->Info << "Executing protocol daemon verb '" << pppSettings.GetProtocolDaemonVerb() << "'" << endl;
	}

	try
	{
		apeClient.reset(new APEClient(logger, queueToThread, pppSettings.GetProtocolDaemonVerb()));
	}
	catch(exception& ex)
	{
		stringstream msg;
		msg << "Caught " << typeid(ex).name() << " '" << ex.what() << "' while calling external protocol verb";
		throw PPPConnectionFailed(msg.str());
	}

	bool looping = true;

	while(looping)
	{
		QueueEntry qe = queueToThread.Read();

		switch(qe.type)
		{
		case QueueEntry::PPP_CONNECTING:
			NewConnectionStatus(CONNECTING);
			break;

		case QueueEntry::PPP_CONNECTED:

			if (logger)
			{
				Logger::Lock logLock(*logger);
				logger->Info << "Waiting for 1 second after PPP connected" << endl;
			}

			Delay(1);
			NewConnectionStatus(CONNECTED);
			looping = false;
			break;

		case QueueEntry::PPP_DISCONNECTED:
			throw PPPConnectionFailed("PPPHost external process exited before indicating good connection");

		case QueueEntry::SHUT_DOWN:
			throw ShutDown("PPPHost received shutdown request while waiting PPP server to connect");

		default:

			if (logger)
			{
				Logger::Lock logLock(*logger);
				logger->Debug << "PPPHost got unexpected message type " << qe.type << " (ignored) while waiting for for external process to connect" << endl;
			}
		}
	}
}

/*bool PPPHost::PPPConnectionEstablished()
{
	using namespace boost::filesystem;
	const string netInterfaceDir = "/sys/class/net";

	if (!exists(netInterfaceDir))
	{
		stringstream msg;
		msg << "PPPHost did not find network interface directory (" << netInterfaceDir << ")";
		throw runtime_error(msg.str());
	}

	directory_iterator endIter;
	bool established = false;

	for (directory_iterator iter(netInterfaceDir); !established && (iter != endIter); ++iter)
		if (!is_directory(iter->status()))
			if (boost::starts_with(iter->leaf(), "ppp"))
				established = true;

	return established;
}*/

/*bool PPPHost::ProcessExited(pid_t childPid)
{
	stringstream msg;
	msg << "Checking for pid " << childPid;

	int status;
	pid_t waitPIDResult;
	int errSave;
	int times = 3;

	do
	{
		msg << " ...";
		waitPIDResult = waitpid(childPid, &status, WNOHANG);
		errSave = errno;
		msg << " " << waitPIDResult << "/" << status;
		if (waitPIDResult == -1)
			msg << " " << errSave << " '" << GetErrorDescription(errSave) << "'";
	} while((waitPIDResult == -1) && (errSave == EINTR) && --times);

	const bool result = (childPid == waitPIDResult);

	msg << ": " << result;

	if (logger)
	{
		Logger::Lock loglock(*logger);
		logger->Debug << msg.str() << endl;
	}

	return result;
}*/

string PPPHost::GetErrorMessage(int err)
{
	char temp[256];

	strerror_r(err, temp, sizeof(temp));
	return string(temp);
}

void PPPHost::RunExec(const string& commandLine)
{
#if 0
	string fileName;
	string tail;
	string trimmed = commandLine;

	boost::trim(trimmed);

	size_t foundPos = trimmed.find_first_of(" \t");

	if (foundPos == trimmed.npos)
		fileName = trimmed;

	else
	{
		fileName = trimmed.substr(0, foundPos);
		tail = trimmed.substr(foundPos);
		boost::trim(tail);
	}

	if (fileName.empty())
		throw PPPConnectionFailed("Protocol daemon verb does not name an executable");

	else if (tail.empty())
		execlp("sh", "sh", "-c", fileName.c_str(), NULL);

	else
		execlp("sh", "sh", "-c", fileName.c_str(), tail.c_str(), NULL);
#else
	string trimmed = commandLine;

	boost::trim(trimmed);
	//cerr << "Command line is " << trimmed << endl;

	if (trimmed.empty())
		throw PPPConnectionFailed("Protocol daemon verb does not name an executable");

	execlp("sh", "sh", "-c", trimmed.c_str(), NULL);
#endif
}

void PPPHost::ExpectSingle(SerialPortPtr& serialPort, string& responseBuffer, float timeoutSeconds)
{
	float startTime = GetSystemUpTimeSeconds();
	float elapsed;
	unsigned millisecRemaining;
	bool gotResponse = false;

	if (timeoutSeconds < 0)
		timeoutSeconds = pppSettings.GetModemResponseTimeoutSeconds();

	if (logger)
	{
		Logger::Lock loglock(*logger);
		logger->Info << "PPPHost expecting a single line within " << timeoutSeconds << " seconds" << endl;
	}

	do
	{
		elapsed = GetSystemUpTimeSeconds()-startTime;

		if (elapsed >= timeoutSeconds)
			millisecRemaining = 0;
		else
			millisecRemaining = (unsigned) ceil(1000*(timeoutSeconds-elapsed));

		if (millisecRemaining)
		{
			try
			{
				QueueEntry qe = queueToThread.Read(millisecRemaining);

				switch(qe.type)
				{
				case QueueEntry::LINE_RECEIVED:

					if (logger)
					{
						Logger::Lock loglock(*logger);
						logger->Info << "PPPHost received '" << *qe.lineReceived << "' from modem" << endl;
					}

					responseBuffer = *qe.lineReceived;
					gotResponse = true;
					break;

				case QueueEntry::SHUT_DOWN:

					throw ShutDown("PPPHost received shutdown request while waiting for input");

				default:

					break;
				}
			}
			catch(QueueType::ReadTimeout& readEx)
			{
				millisecRemaining = 0;
			}
		}

	} while(!gotResponse && millisecRemaining);

	if (!gotResponse && !millisecRemaining)
		throw ResponseTimeout("PPPHost response timeout from modem");

	if (logger)
	{
		Logger::Lock loglock(*logger);
		logger->Info << "PPPHost returning response '" << responseBuffer << "'" << endl;
	}
}

void PPPHost::ExpectSingle(SerialPortPtr& serialPort, string& responseBuffer, const string& startsWith, float timeoutSeconds)
{
	float startTime = GetSystemUpTimeSeconds();
	float elapsed;
	unsigned millisecRemaining;
	bool gotResponse = false;

	if (timeoutSeconds < 0)
		timeoutSeconds = pppSettings.GetModemResponseTimeoutSeconds();

	if (logger)
	{
		Logger::Lock loglock(*logger);
		logger->Info << "PPPHost expecting a single line starting with '" << startsWith << "' within " << timeoutSeconds << " seconds" << endl;
	}

	do
	{
		elapsed = GetSystemUpTimeSeconds()-startTime;

		if (elapsed >= timeoutSeconds)
			millisecRemaining = 0;
		else
			millisecRemaining = (unsigned) ceil(1000*(timeoutSeconds-elapsed));

		if (millisecRemaining)
		{
			try
			{
				QueueEntry qe = queueToThread.Read(millisecRemaining);

				switch(qe.type)
				{
				case QueueEntry::LINE_RECEIVED:

					if (logger)
					{
						Logger::Lock loglock(*logger);
						logger->Info << "PPPHost received '" << *qe.lineReceived << "' from modem" << endl;
					}

					if (!startsWith.length() || boost::starts_with(*qe.lineReceived, startsWith))
					{
						responseBuffer = *qe.lineReceived;
						gotResponse = true;
					}

					break;

				case QueueEntry::SHUT_DOWN:

					throw ShutDown("PPPHost received shutdown request while waiting for input");

				default:

					break;
				}
			}
			catch(QueueType::ReadTimeout& readEx)
			{
				millisecRemaining = 0;
			}
		}

	} while(!gotResponse && millisecRemaining);

	if (!gotResponse && !millisecRemaining)
		throw ResponseTimeout("PPPHost response timeout from modem");
}

void PPPHost::Send(SerialPortPtr& serialPort, const string& command, bool flushInput)
{
	if (logger)
	{
		Logger::Lock loglock(*logger);
		logger->Info << "PPPHost sending '" << command << "\\r' to modem" << endl;
	}

	string cmdCopy = command;

	cmdCopy.push_back('\r');
	serialPort->Write(cmdCopy);
}

void PPPHost::SendExpect(SerialPortPtr& serialPort, const string& command, const string& successResponse, const string& failureResponse)
{
	if (logger)
	{
		Logger::Lock loglock(*logger);
		logger->Info << "PPPHost to send command '" << command << "' expecting '" << successResponse
					<< "' if successful or '" << failureResponse << "' for errors" << endl;
	}

	unsigned tryNum = 0;
	string response;
	bool timeout;
	bool success;
	bool gotError;
	const float responseTimeout = pppSettings.GetModemResponseTimeoutSeconds();
	const unsigned maxTries = pppSettings.GetSendTries();

	do
	{
		++tryNum;
		Send(serialPort, command);

		timeout = false;
		success = false;

		float startTime = GetSystemUpTimeSeconds();
		float remaining;

		do
		{
			gotError = false;

			try
			{
				remaining = responseTimeout-(GetSystemUpTimeSeconds()-startTime);
				if (remaining < 0) remaining = 0;
				ExpectSingle(serialPort, response, remaining);
				success = !successResponse.compare(response);
				gotError = !failureResponse.compare(response);
			}
			catch(ResponseTimeout& rto)
			{
				if (logger)
				{
					Logger::Lock loglock(*logger);
					logger->Error << "PPPHost caught " << typeid(rto).name() << endl;
				}

				timeout = true;
				if (tryNum >= maxTries)
					throw;
			}

		} while(!timeout && !success && !gotError);

	} while(((timeout || gotError) && (tryNum < maxTries)) && !success);

	if (!success)
		throw ReceivedFailingResponse("PPPHost received failing response");
}

void PPPHost::SendExpectSingle(SerialPortPtr& serialPort, const string& command, string& responseBuffer, const string& startsWith,
		const string& successResponse, const string& failureResponse)
{
	if (logger)
	{
		Logger::Lock loglock(*logger);
		logger->Info << "PPPHost to send command '" << command << "' expecting '" << successResponse
					<< "' if successful or '" << failureResponse << "' for errors and recording a response lines starting with '"
					<< startsWith << "'" << endl;
	}

	unsigned tryNum = 0;
	string response;
	bool timeout;
	bool success;
	bool gotError;
	const float responseTimeout = pppSettings.GetModemResponseTimeoutSeconds();
	const unsigned maxTries = pppSettings.GetSendTries();

	do
	{
		++tryNum;
		Send(serialPort, command);

		timeout = false;
		success = false;

		float startTime = GetSystemUpTimeSeconds();
		float remaining;

		do
		{
			gotError = false;

			try
			{
				remaining = responseTimeout-(GetSystemUpTimeSeconds()-startTime);
				if (remaining < 0) remaining = 0;

				ExpectSingle(serialPort, responseBuffer, startsWith, remaining);

				remaining = responseTimeout-(GetSystemUpTimeSeconds()-startTime);
				if (remaining < 0) remaining = 0;

				ExpectSingle(serialPort, response, remaining);

				success = !successResponse.compare(response);
				gotError = !failureResponse.compare(response);
			}
			catch(ResponseTimeout& rto)
			{
				if (logger)
				{
					Logger::Lock loglock(*logger);
					logger->Error << "PPPHost caught " << typeid(rto).name() << endl;
				}

				timeout = true;
				if (tryNum >= maxTries)
					throw;
			}

		} while(!timeout && !success && !gotError);

	} while(((timeout || gotError) && (tryNum < maxTries)) && !success);

	if (!success)
		throw ReceivedFailingResponse("PPPHost received failing response");
}

void PPPHost::WriteToQueueList(const QueueEntry& qe)
{
	Lock lock(*this);

	BOOST_FOREACH(QueuePtrType qptr, queueList)
		qptr->Write(qe);
}

ostream& operator<<(ostream& out, const PPPHost::ConnectionStatus& connectionStatus)
{
	switch(connectionStatus)
	{
	case PPPHost::CONNECTED:			out << "CONNECTED";						break;
	case PPPHost::CONNECTING:			out << "CONNECTING";					break;
	case PPPHost::DIALING:				out << "DIALING";						break;
	case PPPHost::DISCONNECTED:			out << "DISCONNECTED";					break;
	case PPPHost::DISCONNECTING:		out << "DISCONNECTING";					break;
	case PPPHost::INITIALIZING_MODEM:	out << "INITIALIZING_MODEM";			break;
	default:							out << (long) connectionStatus;			break;
	}

	return out;
}

ostream& operator<<(ostream& out, const PPPHost::QueueEntry::Type& qetype)
{
	switch(qetype)
	{
	case PPPHost::QueueEntry::UNSPECIFIED:		out << "UNSPECIFIED";					break;
	case PPPHost::QueueEntry::START_CONNECT:	out << "START_CONNECT";					break;
	case PPPHost::QueueEntry::START_DISCONNECT:	out << "START_DISCONNECT";				break;
	case PPPHost::QueueEntry::SHUT_DOWN:		out << "SHUT_DOWN";						break;
	case PPPHost::QueueEntry::LINE_RECEIVED:	out << "LINE_RECEIVED";					break;
	case PPPHost::QueueEntry::PPP_CONNECTED:	out << "PPP_CONNECTED";					break;
	case PPPHost::QueueEntry::PPP_DISCONNECTED:	out << "PPP_DISCONNECTED";				break;
	case PPPHost::QueueEntry::ACTION_SUCCEEDED:	out << "ACTION_SUCCEEDED";				break;
	case PPPHost::QueueEntry::ACTION_FAILED:	out << "ACTION_FAILED";					break;
	default:									out << (long) qetype << "?";			break;
	}

	return out;
}

PPPHost::QueueEntry::QueueEntry()
: type(UNSPECIFIED)
, pTypeInfo(NULL)
{
}

PPPHost::QueueEntry::QueueEntry(Type _type)
: type(_type)
, pTypeInfo(NULL)
{
}

PPPHost::QueueEntry::QueueEntry(Type _type, const string& rxLine)
: type(_type)
, pTypeInfo(NULL)
{
	lineReceived.reset(new string(rxLine));
}

PPPHost::QueueEntry::QueueEntry(Type _type, ConnectionStatus _newStatus)
: type(_type)
, newStatus(_newStatus)
, pTypeInfo(NULL)
{
}

PPPHost::QueueEntry::QueueEntry(Type _type, const std::type_info& _typeInfo, const string& _exwhat)
: type(_type)
, pTypeInfo(&_typeInfo)
, exwhat(_exwhat)
{
}

void PPPHost::QueueEntry::Rethrow() const
{
	if (pTypeInfo == &typeid(ModemPowerControlFailure))
		throw ModemPowerControlFailure(exwhat);
	else if (pTypeInfo == &typeid(ModemResponseTimeout))
		throw ModemResponseTimeout(exwhat);
	else if (pTypeInfo == &typeid(ModemRegistrationRefused))
		throw ModemRegistrationRefused(exwhat);
	else if (pTypeInfo == &typeid(ModemRegistrationTimeout))
		throw ModemRegistrationTimeout(exwhat);
	else if (pTypeInfo == &typeid(NotDisconnected))
		throw NotDisconnected(exwhat);
	else if (pTypeInfo == &typeid(PPPConnectionFailed))
		throw PPPConnectionFailed(exwhat);
	else if (pTypeInfo == &typeid(ServerDialupFailed))
		throw ServerDialupFailed(exwhat);
	else if (pTypeInfo == &typeid(ShutDown))
		throw ShutDown(exwhat);
	else if (pTypeInfo == &typeid(SystemError))
		throw SystemError(exwhat);
	else
		throw std::runtime_error(exwhat);
}

PPPHost::QueuePtrLifespan::QueuePtrLifespan(Lock& _lock, QueueListType& _listRef, size_t queueSize)
: lock(_lock)
, listRef(_listRef)
{
	if (!lock)
		lock.lock();

	q.reset(new QueueType(queueSize));
	listRef.push_back(q);
}

PPPHost::QueuePtrLifespan::~QueuePtrLifespan()
{
	if (!lock)
		lock.lock();

	for (QueueListType::iterator iter = listRef.begin(); iter != listRef.end(); )
		if (iter->get() == q.get())
		{
			QueueListType::iterator toDelete = iter++;
			listRef.erase(toDelete);
		}
		else
			++iter;
}

PPPHost::QueueType* PPPHost::QueuePtrLifespan::operator->() const
{
	if (q)
		return q.get();
	else
		throw std::runtime_error("QueuePtrLifespan pointer is null");
}

PPPHost::QueuePtrType PPPHost::QueuePtrLifespan::get() const
{
	return q;
}

PPPHost::SerialLineMachine::Callback::Callback(QueueType& _queue, LineMachine& _lineMachine, Logger::Ptr _logger)
: queue(_queue)
, lineMachine(_lineMachine)
, logger(_logger)
{
}

void PPPHost::SerialLineMachine::Callback::operator()(LineMachine& sender, const string& lineReceived)
{
#ifdef EXTREMELY_VERBOSE
	if (logger)
	{
		Logger::Lock loglock(*logger);

		if (lineReceived.empty())
			logger->Debug << "PPPHost ignoring blank line" << endl;
		else
			logger->Debug << "PPPHost inserting received line '" << lineReceived << "' into buffer" << endl;
	}
#endif

	if (!lineReceived.empty())
	{
		QueueEntry qe(QueueEntry::LINE_RECEIVED, lineReceived);
		queue.Write(qe);
	}
}

void PPPHost::SerialLineMachine::Callback::operator()(SerialListener& sender, const ByteArray& receivedData)
{
#ifdef EXTREMELY_VERBOSE
	if (logger)
	{
		Logger::Lock loglock(*logger);
		logger->Debug << "PPPHost inserting " << receivedData.size() << " bytes into line processor" << endl;
	}
#endif

	lineMachine.Feed(receivedData);
}

PPPHost::SerialLineMachine::SerialLineMachine(QueueType& _queue, SerialPortPtr _serialPort, Logger::Ptr _logger)
: initialized(false)
, queue(_queue)
, logger(_logger)
, serialPort(_serialPort)
, listener(*serialPort, 500, boost::posix_time::milliseconds(100))
, callback(queue, lineMachine, _logger)
{
	serialMachineToLineMachine.reset(new ScopedConnection(lineMachine.hereIsLine_Signal.connect(callback)));
	lineMachineToListener.reset(new ScopedConnection(listener.hereIsSerialData_Signal.connect(callback)));
}

PPPHost::SerialLineMachine::~SerialLineMachine()
{
	if (initialized)
		Shutdown();
}

void PPPHost::SerialLineMachine::Initialize()
{
	listener.Initialize();
	initialized = true;
}

void PPPHost::SerialLineMachine::Shutdown()
{
	serialMachineToLineMachine.reset();
	lineMachineToListener.reset();

	if (initialized)
	{
		initialized = false;
		listener.Shutdown();
	}
}

PPPHost::APEClient::Callback::Callback(LineMachine& _lineMachine, Logger::Ptr _logger, QueueType& _threadQueue, Semaphore& _exitSem)
: lineMachine(_lineMachine)
, logger(_logger)
, threadQueue(_threadQueue)
, connecting(false)
, connected(false)
, exitSem(_exitSem)
{
}

void PPPHost::APEClient::Callback::operator()(AsynchronousProcessExecutor& sender, int resultCode)									// handles process exit callback
{
	if (logger)
	{
		Logger::Lock logLock(*logger);
		logger->Info << "Process exited with result code " << resultCode << endl;
	}

	Reset();
	QueueEntry qe(QueueEntry::PPP_DISCONNECTED);
	threadQueue.Write(qe);
	exitSem.Post();
}

void PPPHost::APEClient::Callback::operator()(AsynchronousProcessExecutor& sender, const ByteArray& dataReceived, bool fromStdErr)	// handles reporting callback
{
	lineMachine.Feed(dataReceived);
}

inline void GetIPAddressFromString(const string& received, string& result, const string& startTag)
{
	const string IPaddress = "IP address";

	if (result.empty() && boost::starts_with(received, startTag))
	{
		size_t found = received.find(IPaddress, startTag.length());

		if (found != string::npos)
		{
			result = received.substr(found);
			boost::trim(result);
		}
	}
}

void PPPHost::APEClient::Callback::operator()(LineMachine& sender, const string& receivedLine)
{
	if (logger)
	{
		Logger::Lock logLock(*logger);
		logger->Info << "Daemon> " << receivedLine << endl;
	}

	if (!connecting && (receivedLine.find("Connect: ppp") != string::npos))
	{
		connecting = true;
		QueueEntry qe(QueueEntry::PPP_CONNECTING);
		threadQueue.Write(qe);
	}

	else if (connecting && !connected)
	{
		const string local = "local";
		const string remote = "remote";
		const string IPaddress = "IP address";

		GetIPAddressFromString(receivedLine, localIP, "local");
		GetIPAddressFromString(receivedLine, remoteIP, "remote");

		if (!localIP.empty() && !remoteIP.empty())
		{
			connected = true;
			QueueEntry qe(QueueEntry::PPP_CONNECTED);
			threadQueue.Write(qe);
		}
	}
}

void PPPHost::APEClient::Callback::Reset()
{
	connected = connecting = false;
	localIP.clear();
	remoteIP.clear();
}

PPPHost::APEClient::APEClient(Logger::Ptr _logger, QueueType& _threadQueue, const string& commandLine)
: logger(_logger)
, ape(logger, SIGTERM, true, SIGKILL, 500, true, 1024)
, callback(lineMachine, _logger, _threadQueue, exitSem)
{
	callbackLineMachineConnection.reset(new ScopedConnection(lineMachine.hereIsLine_Signal.connect(callback)));
	callbackReportConnection.reset(new ScopedConnection(ape.ReportSignal.connect(callback)));
	callbackExitConnection.reset(new ScopedConnection(ape.ExitSignal.connect(callback)));

	while(exitSem.TryWait());

	try
	{
		ape.RunAsynch(commandLine, true, true);
	}
	catch(...)
	{
		exitSem.Post();
		throw;
	}
}

PPPHost::APEClient::~APEClient()
{
	try { ape.Kill(); } catch(...) { }
	Semaphore::Lock semLock(exitSem);
}

}
}
