//---------------------------------------------------------------------------------
// <copyright file="PPPHost.h" 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 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>
//---------------------------------------------------------------------------------

#ifndef __PPPHOST_H__
#define __PPPHOST_H__

#include "PPPHostSettings.h"
#include "LqrSMACDeclareException.h"
#include "LqrSMACLineMachine.h"
#include "LqrSMACLogger.h"
#include "LqrSMACSerialListener.h"
#include "LqrSMACSerialPortSettings.h"
#include "LqrSMACSharedLib.h"
#include "LqrSMACWaitableThreadSafeQueue.h"
#include "LqrSMACAsynchronousProcessExecutor.h"
#include <ostream>
#include <stdexcept>
#include <string>
#include <list>
#include <vector>
#include <typeinfo>
#include <boost/shared_ptr.hpp>
#include <boost/signals2.hpp>
#include <boost/thread.hpp>
#include <boost/thread/locks.hpp>
#include <boost/thread/recursive_mutex.hpp>

namespace LiquidRobotics {
namespace PPPHostLib {

// PPPHost is a boundary class that establishes and monitors a PPP connection via RUDICS
class SHARED_LIB_PUBLIC PPPHost
: boost::noncopyable
{
public:

	// === Exception classes thrown by PPPHost ===

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

	// Status values reported for the connection
	enum ConnectionStatus
	{
		DISCONNECTED,				// modem is disconnected and idle
		INITIALIZING_MODEM,			// initializing modem, connecting to modem media
		DIALING,					// dialog number
		CONNECTING,					// PPP connecting
		CONNECTED,					// PPP has connected
		DISCONNECTING,				// disconnecting, not ready for connection request yet
	};

	typedef LiquidRobotics::SMACFramework::AsynchronousProcessExecutor AsynchronousProcessExecutor;
	typedef boost::unique_lock<PPPHost> Lock;
	typedef LiquidRobotics::SMACFramework::Logger Logger;
	typedef boost::recursive_mutex Mutex;
	typedef boost::unique_lock<Mutex> MutexLock;
	typedef boost::shared_ptr<PPPHost> Ptr;
	typedef LiquidRobotics::SMACFramework::Semaphore Semaphore;
	typedef LiquidRobotics::SMACFramework::SerialPortSettings SerialPortSettings;
	typedef SerialPortSettings::SerialPortPtr SerialPortPtr;
	typedef std::string string;
	typedef boost::signals2::signal<void (PPPHost& sender, ConnectionStatus connectionStatus)> ConnectionSignalType;
	typedef boost::signals2::scoped_connection ScopedConnection;
	typedef boost::shared_ptr<ScopedConnection> ScopedConnectionPtr;

	PPPHost(const PPPHostSettings& _pppSettings, Logger::Ptr _logger = Logger::Ptr());

	virtual ~PPPHost();

	virtual void Connect();
	virtual void Disconnect();
	virtual ConnectionStatus GetConnectionStatus();
	//virtual string GetIpAddress();
	virtual string GetModemSerialNumber() const;
	virtual string GetModemSimCardNumber() const;
	virtual bool IsConnected();
	virtual void Shutdown();
	virtual ScopedConnectionPtr Subscribe(ConnectionSignalType::slot_type slot);

	// Lockable concept used internally and usable by external actors
	virtual void lock();
	virtual bool try_lock();
	virtual void unlock();

	// QueueEntry is an entity class that contains a message type and parameters that
	// are enqueued in the queueToThread or in the local queues established by Connect()
	// and Disconnect()
	struct QueueEntry
	{
		enum Type
		{
			UNSPECIFIED = -1,
			START_CONNECT,				// command to thread to start connection
			START_DISCONNECT,			// command to thread to start disconnection
			SHUT_DOWN,					// command to shut down immediately
			LINE_RECEIVED,				// information from the LineMachine that a line was parsed from the modem output stream
			PPP_CONNECTING,				// event notification that the dialer connected and the PPP protocol is starting to connect
			PPP_CONNECTED,				// event notification that the PPP protocol daemon connected
			PPP_DISCONNECTED,			// event notification that the PPP protocol daemon disconnected
			ACTION_SUCCEEDED,			// event notification that the Connect or Disconnect action succeeded
			ACTION_FAILED				// event notification that the Connect or Disconnect action failed
		};

		QueueEntry();
		QueueEntry(Type _type);
		QueueEntry(Type _type, const string& rxLine);
		QueueEntry(Type _type, ConnectionStatus _newStatus);
		QueueEntry(Type _type, const std::type_info& _typeInfo, const string& _exwhat);

		void Rethrow() const;

		Type type;										// type of message
		boost::shared_ptr<string> lineReceived;			// the line that was received
		ConnectionStatus newStatus;						// the new connection status
		const std::type_info* pTypeInfo;				// the type of exception that was thrown (action failed)
		string exwhat;									// the text within the exception that failed (action failed)
	};

private:

	typedef LiquidRobotics::SMACFramework::WaitableThreadSafeQueue<QueueEntry> QueueType;
	typedef boost::shared_ptr<QueueType> QueuePtrType;
	typedef std::list<QueuePtrType> QueueListType;
	typedef std::vector<string> StringArray;

	// QueuePtrLifespace allocates a QueuePtrType object and inserts a copy of it into
	// a list (QueueListType) on construction; on destruction, the copy is removed from
	// the list and the queue is destroyed.  A method can instantiate QueuePtrLifespace
	// and use it as safe way to clean up, no matter how the method exits (i.e. even if
	// there is an exception thrown)
	class QueuePtrLifespan : boost::noncopyable
	{
	public:

		QueuePtrLifespan(Lock& _lock, QueueListType& _listRef, size_t queueSize);

		~QueuePtrLifespan();

		QueueType* operator->() const;
		QueuePtrType get() const;

	private:

		Lock& lock;
		QueueListType& listRef;
		QueuePtrType q;
	};

	// SerialLineMachine instantiates a SerialListener and a LineMachine and configures
	// event handlers to funnel the output of the listener to the line machine; any resulting
	// parsed lines are stored as entries in a queue.
	class SerialLineMachine : boost::noncopyable
	{
	public:

		typedef LiquidRobotics::SMACFramework::ByteArray ByteArray;
		typedef LiquidRobotics::SMACFramework::LineMachine LineMachine;
		typedef LiquidRobotics::SMACFramework::SerialListener SerialListener;
		typedef boost::shared_ptr<SerialLineMachine> Ptr;

		// I had some errors when I tried to use the SerialLineMachine instance, itself, as the
		// event sink since it could not be copied, so I created Callback as a simple copyable
		// sink object
		struct Callback
		{
			Callback(QueueType& _queue, LineMachine& _lineMachine, Logger::Ptr _logger);

			void operator()(LineMachine& sender, const string& lineReceived);
			void operator()(SerialListener& sender, const ByteArray& receivedData);

			QueueType& queue;
			LineMachine& lineMachine;
			Logger::Ptr logger;
		};

		SerialLineMachine(QueueType& _queue, SerialPortPtr _serialPort, Logger::Ptr _logger);

		~SerialLineMachine();

		void Initialize();
		void Shutdown();

	private:

		typedef boost::signals2::scoped_connection ScopedConnection;
		typedef boost::shared_ptr<ScopedConnection> ScopedConnectionPtr;

		bool initialized;
		QueueType& queue;
		Logger::Ptr logger;
		SerialPortPtr serialPort;
		SerialListener listener;
		LineMachine lineMachine;
		Callback callback;
		ScopedConnectionPtr lineMachineToListener;
		ScopedConnectionPtr serialMachineToLineMachine;
	};

	class APEClient
	{
	public:

		typedef LiquidRobotics::SMACFramework::ByteArray ByteArray;
		typedef LiquidRobotics::SMACFramework::LineMachine LineMachine;
		typedef boost::shared_ptr<APEClient> Ptr;
		typedef boost::signals2::scoped_connection ScopedConnection;
		typedef boost::shared_ptr<ScopedConnection> ScopedConnectionPtr;
		typedef LiquidRobotics::SMACFramework::Semaphore Semaphore;
		typedef std::string string;

		struct Callback
		{
			Callback(LineMachine& _lineMachine, Logger::Ptr _logger, QueueType& _threadQueue, Semaphore& _exitSem);

			void operator()(AsynchronousProcessExecutor& sender, int resultCode);									// handles process exit callback
			void operator()(AsynchronousProcessExecutor& sender, const ByteArray& dataReceived, bool fromStdErr);	// handles reporting callback
			void operator()(LineMachine& sender, const string& receivedLine);

			void Reset();

			LineMachine& lineMachine;
			Logger::Ptr logger;
			QueueType& threadQueue;
			bool connecting;
			bool connected;
			string localIP;
			string remoteIP;
			Semaphore& exitSem;
		};

		APEClient(Logger::Ptr _logger, QueueType& _threadQueue, const string& commandLine);
		~APEClient();

	private:

		Logger::Ptr logger;
		AsynchronousProcessExecutor ape;
		LineMachine lineMachine;
		Semaphore exitSem;
		Callback callback;
		ScopedConnectionPtr callbackLineMachineConnection;
		ScopedConnectionPtr callbackReportConnection;
		ScopedConnectionPtr callbackExitConnection;
	};

	friend class boost::detail::thread_data<boost::reference_wrapper<PPPHost> >;

	void operator()();			// background thread

	void AwaitConnectionActionComplete(QueuePtrType qptr);
	void AwaitDisconnectionActionComplete(QueuePtrType qptr);
	void AwaitModemRegistration(SerialPortPtr& serialPort);
	void ConfigureModem(SerialPortPtr serialPort);
	void Delay(float seconds, bool throwOnShutdown = true);
	void DialGateway(SerialPortPtr& serialPort);
	ConnectionStatus DoConnect();
	ConnectionStatus DoDisconnect();
	static string GetErrorMessage(int err);
	void ExpectSingle(SerialPortPtr& serialPort, string& responseBuffer, float timeoutSeconds = -1);
	void ExpectSingle(SerialPortPtr& serialPort, string& responseBuffer, const string& startsWith, float timeoutSeconds = -1);
	void NewConnectionStatus(ConnectionStatus newStatus, bool fireOnChange = true);
	//static bool PPPConnectionEstablished();
	//bool ProcessExited(pid_t childPid);
	static int RegistrationStatus(const string& line);
	static void RunExec(const string& commandLine);
	void Send(SerialPortPtr& serialPort, const string& command, bool flushInput = true);
	void SendExpect(SerialPortPtr& serialPort, const string& command, const string& successResponse, const string& failureResponse);
	void SendExpectSingle(SerialPortPtr& serialPort, const string& command, string& responseBuffer, const string& startsWith, const string& successResponse, const string& failureResponse);
	void SetModemPower(bool on);
	void StartConnectionAction(QueuePtrType qptr);
	void StartDisconnectionAction(QueuePtrType qptr);
	void StartProtocolDaemon();
	void WriteToQueueList(const QueueEntry& qe);

	//PPPHostSettings noSettings;
	const PPPHostSettings pppSettings;
	Logger::Ptr logger;
	ConnectionStatus connectionStatus;
	bool shutDown;
	ConnectionSignalType connectionStateChanged;
	boost::thread* pThread;
	FILE* toThreadReadPtr;
	Mutex mutex;
	Mutex connectionMutex;
	Mutex firingMutex;
	string actionExceptionType;
	string actionExceptionText;
	string modemSerialNumber;
	string modemSimCardNumber;
	QueueType queueToThread;
	QueueListType queueList;
	APEClient::Ptr apeClient;
};

SHARED_LIB_PUBLIC std::ostream& operator<<(std::ostream& out, const PPPHost::QueueEntry::Type& qetype);
SHARED_LIB_PUBLIC std::ostream& operator<<(std::ostream& out, const PPPHost::ConnectionStatus& connectionStatus);

}
}

#endif /* PPPHOST_H_ */
