/*************************************/
/* Written by David Yates            */
/* Copyright Siemens IT Systems 2007 */
/*************************************/
#pragma warning (disable :4786)
#include "stdafx.h"
#include "ccInfodriver.h"
#include "cc.h"
#include "ccConfig.h"
#include "bncs.h"
#include "bbc_mxc.h"
#include "bncs32.h"
#include "bncswm32.h"
#include "bncsif32.h"
#include "decodebncsstring.h"

#ifndef HWND_MESSAGE
	#define HWND_MESSAGE     ((HWND)-3)
#endif

/** 
\class ccInfodriver
\brief A Colledia Control Infodriver class

This is a class that provides full Infodriver functionality for Colledia Control drivers.
\par Usage Example
\code
class myDriver: public IccInfodriverCallback
{
public:
	myDriver( int device )
	{
		m_info = new ccInfodriver( this );
		m_info->connect( 101 );
	};
	virtual ~myDriver() {	delete m_info; };

protected:
	// reimplement those functions we need to (declared in IccInfodriverCallback)
	void cciSlotRequest( int device, int index, const string & slot )
	{
		debug( "slot request. Device %d, index %d contents %s", device, index, slot.c_str());
	};
private:
	ccInfodriver *m_info;
}
\endcode

Redudndancy.
Multiple infodrivers of the same identity can run on a Colledia Control network. There is a simple mechanism
to enable automatic changeover in case of failure which takes a previously passive device on the network and 
enable it to take over.

Modes
There are a couple of distinct modes that redundancy can run in. 
Most of the time either of two (or more) drivers can run with any one of them able to provide the service to the network. This is cc::multipleControllerCapable mode.
Occasionally however there are devices, such as serially controlled devices, where there is only a single control port that cannot be 
shared. this is cc::singleControllerCapable mode. In this case only one instance of the driver can be active on the network at one time - importantly in this mode 
if a driver detects it has active comms it will aggresively try and force itself onto the network since no other driver can have communications with the controlled device.

*/


/** Construct a Colledia Control infodriver class and tell it where to send notifications too (i.e. pass it an interface to the class to callback). 
If the callback is not specified this class will be able to get slots but be unable to receive asynchronous notifications of change of state etc.
\param[in] callback pointer to the class that implements the IccInfodriverCallback class
\param[in] debugStringHeader The member functions of this class generate debug messages for out of range values to which this string is prepended. A message such as "Error: Device=0" message in itself is not useful without some context!
*/
ccInfodriver::ccInfodriver( IccInfodriverCallback * callback, const string & debugStringHeader ) : ccCheck( debugStringHeader )
{
	m_redundancyMode = cc::multipleControllerCapable;
	m_driverState = cc::ok;
	m_redundancyState = cc::unknown;

	// init a few variables
	m_callback = callback;
	m_hInfo = 0;
	m_hWndMessage = 0;
	m_ipc = cc::none;
	m_ipcOptions = 0;
	m_workstation=ccConfig::workstation();

	// register a messaging window
	WNDCLASS wc;

	memset( &wc, 0, sizeof( wc ));

	wc.lpszClassName="ccInfodriverMessageWindow";
	wc.cbWndExtra = sizeof( ccInfodriver * );
	wc.lpfnWndProc=WndProc;

	RegisterClass( &wc );

	// create the messaging window
	m_hWndMessage = CreateWindow( wc.lpszClassName, "", 0, 0,0,0, 0, HWND_MESSAGE, 0,0,this);
	if( !m_hWndMessage )
		::MessageBox( 0, "Cannot create ccInfodriverMessageWindow", "Very Serious Error!", 0 );
}

ccInfodriver::~ccInfodriver()
{
	if( m_hWndMessage )
		DestroyWindow( m_hWndMessage );
}

/** Simple function to ensure that we can tell what version this class is programatically
\returns HIWORD( version()) returns major version number, LOWORD(version()) the minor revision number
*/
long ccInfodriver::version( void )
{
	return MAKELONG( 1, 0 );
}

LRESULT CALLBACK ccInfodriver::WndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
	switch( uMsg )
	{
		case WM_COPYDATA:
			BNCS32ReceiveMessage( hWnd, (HWND) LOWORD( wParam ), (COPYDATASTRUCT*) lParam);
			return true;

		case BBC_COPYDATA:
			return BNCS32RxMessage( hWnd, wParam, lParam );
			
		case WM_CREATE:
			SetWindowLong( hWnd, 0, (long) ((CREATESTRUCT *)lParam)->lpCreateParams );
			break;
	}

	ccInfodriver * c= (ccInfodriver*) GetWindowLong( hWnd, 0 );
	if( c )
	{
		switch( uMsg )
		{
			case BBC_INTERNALPOST:
			{
				while( c->postedQueue.size())
				{
					ccInfodriverPost p = c->postedQueue.front();
					c->postedQueue.pop();

					if( c->m_callback )
					{
						if( p.m_type == BBC_IDSLOTMSGPOSTED )
						{
							c->m_callback->cciSlotRequest( c->m_device, p.m_slot, p.m_contents );
						}
						else if( p.m_type == BBC_DEVCOMMANDMSGPOSTED )
						{
							decodeBNCSString dec( p.m_contents.c_str()  );

							// message is of format:
							//	NS <dev> <toWs> <msg> <fromWs> <handle> <reference>
							// e.g.
							//	NS 123 909 'Hello!' 100 12345 12345
							if( dec.subs < 7 )
								break;

							// extract and check device number
							int dev = atoi( dec.sub[ 1 ] );
							if( c->checkDevice( dev ))
								break;
							if( c->m_device != dev )
								break;


							// extract and check for our workstation number
							int toWs = atoi( dec.sub[ 2 ] );
							if( c->checkWorkstation( toWs ))
								break;
							if( c->m_workstation != toWs )
								break;

							// extract and check the message
							string msg = dec.sub[ 3 ];
							if( c->checkString( msg ))
								break;
							
							// extract and check originating workstation number
							int sendingWs =  atoi( dec.sub[ 4 ] );
							if( c->checkWorkstation( sendingWs ))
								break;

							// extract and check the client handle
							string client = dec.sub[ 5 ];
							if( c->checkString( client ))
								break;
							
							// extract and check the free form reference string
							string ref    = dec.sub[ 6 ];
							if( c->checkString( ref ))
								break;

							// after all that bounds checking and destination checking 
							//  we can finally pass things on to our client
							c->m_callback->cciClientMessage( sendingWs, dev, msg, client, ref );
						}
					}
				}
				break;
			}

			case BBC_DEVCOMMANDMSG:
			{
				// QUEUE MESSAGE FOR PROCESSING LATER
				c->postedQueue.push( ccInfodriverPost( BBC_DEVCOMMANDMSGPOSTED, 0, string( (LPSTR) lParam )));

				PostMessage( hWnd, BBC_INTERNALPOST, 0, 0 );
			}
			return true;

			case BBC_IDSLOTMSG:
				// QUEUE MESSAGE FOR PROCESSING LATER
				c->postedQueue.push( ccInfodriverPost( BBC_IDSLOTMSGPOSTED, wParam, string( (LPSTR) lParam )));

				PostMessage( hWnd, BBC_INTERNALPOST, 0, 0 );
				return true;

			case BBC_CSICLOSING:
				if( c->m_callback )
					c->m_callback->cciDisconnected();
				break;

			case BBC_IDHANDLE:
				c->m_hInfo = (HWND) wParam;
				break;
				
			case WM_DESTROY:
				c->disconnect();
				break;

			case BBC_REQDEVGORXONLY:
				if(c->m_driverState == cc::ok )
					return false;
				return true;

			case BBC_REQDEVGOTXRX:
				// only go TXRX if we have active comms
				if( c->m_driverState == cc::ok )
				{
					c->m_redundancyState = cc::tx;
					c->m_callback->cciRedundancyState( cc::tx );
					return true;		// ok go txrx
				}
				return false;		// refuse to go txrx

			case BBC_INQDEVGOTXRX:
				// don't ask to go TXRX if we can't offer a reserve
				if( c->m_driverState == cc::ok )
					return true;
				return false;

			case BBC_DEVICEIDINUSE:
				// this device is in use
				c->m_redundancyState = cc::rx;
				if( c->m_callback )
					c->m_callback->cciRedundancyState( cc::rx );
				return 0;

			default:
				break;
		}
	}
	return DefWindowProc( hWnd, uMsg, wParam, lParam );
}

LRESULT ccInfodriver::BNCSSendMessage( UINT uMsg, WPARAM wParam, LPARAM lParam, int dataLen, int expectedReturnDataLen, int iDataType )
{
	if( m_ipc == cc::wm )
		return BNCS32SendMessage( m_hWndMessage, m_hInfo, uMsg, wParam, lParam, expectedReturnDataLen, iDataType );
	else if( m_ipc == cc::bbc )
		return BNCS32TxMessage( m_hInfo, uMsg, wParam, lParam, iDataType, max( dataLen, expectedReturnDataLen ) );
	return 0;
}

/** Connect to the specified infodriver using the specified IPC mechanism
\param[in] ipc IPC type as defined in the cc::ipcType enumeration
\param[in] ipcOptions values relevant to the ipc type chosen (none are defined at the time of writing)
\returns true on error, false on success
*/
bool ccInfodriver::connect( int device, enum cc::ipcType ipc, long ipcOptions )
{
	if( isConnected())
		disconnect();

	m_device = device;
	m_ipc = ipc;
	m_ipcOptions = ipcOptions;

	if( m_ipc == cc::wm )
		SendMessage( HWND_BROADCAST, BBC_IDCONNECT, MAKEWPARAM( m_hWndMessage, 0 ), (LPARAM) m_device );
	else if( m_ipc == cc::bbc )
		SendMessage( HWND_BROADCAST, BBC_IDCONNECT32, MAKEWPARAM( m_hWndMessage, 0 ), (LPARAM) m_device );
	
	if( m_hInfo )
	{
		if( m_callback )
			m_callback->cciConnected();

		switch( SendMessage( m_hInfo, BBC_IDGETTXRXMODE, 0, 0 ))
		{
			case IFMODE_FORCETXRX:	
			case IFMODE_TXRX:	
				m_redundancyState = cc::tx;	
				m_callback->cciRedundancyState( cc::tx );
				break;
			
			case IFMODE_RXONLY:	
				m_redundancyState = cc::rx;	
				m_callback->cciRedundancyState( cc::rx );
				break;

			default:
			case IFMODE_NONE:	
				m_redundancyState = cc::unknown;	
				m_callback->cciRedundancyState( cc::unknown );
				break;
		}
		return false;
	}
	return true;
}

/** Disconnect from infodriver. This calls our cciDisconnected event if we successfully disconnected 
\returns false on successful disconnection or false on there being no connection to disconnect.
*/
bool ccInfodriver::disconnect( void )
{
	m_redundancyState = cc::unknown;

	if( isConnected())
	{
		SendMessage( m_hInfo, BBC_IDDISCONNECT, 0, (LPARAM) m_device );
		m_hInfo = 0;
		m_ipc = cc::none;
		if( m_callback )
			m_callback->cciDisconnected();
		return false;
	}
	return true;
}

/** Are we connected to an Infodriver? This is also checks whether the handle we have for it is valid 
\returns true on connected, false on not connected
*/
bool ccInfodriver::isConnected( void )
{
	if( m_hInfo )
		if( IsWindow( m_hInfo ))
			return true;
	return false;
}

/** Get value of an infodriver slot 
\param[in] index The slot number
\param[out] value The variable to stick return value
\returns Returns true on error
*/
bool ccInfodriver::slot( int index, string & value )
{
	if( checkIndex( index ))
		return true;

	char buf[ 300 ];
	memset( buf, 0, sizeof( buf ));

	BNCSSendMessage( BBC_IDGETSLOT, index , (LPARAM) buf, 0, sizeof( buf ), CBDATA_TYPE_POINTER );

	value = buf;
	return false;
}

/** Set the value of an infodriver slot
\param[in] index The slot number
\param[in] value The new contents of the slot
\param[in] send Whether to send a change of state message to the network or just update the slot contents silently
\returns true on error
*/
bool ccInfodriver::setSlot( int index, const string & value, bool send )
{
	if( checkIndex( index ))
		return true;
	if( checkString( value ))
		return true;

	char buf[ 300 ];
	strcpy( buf, value.c_str() );

	UINT msg;
	if( send )
		msg=BBC_IDSETSLOT;
	else
		msg=BBC_IDSETSLOTONLY;

	BNCSSendMessage( msg, index , (LPARAM) buf, sizeof( buf ), 0, CBDATA_TYPE_POINTER );
	return false;
}


/** Set the value of an infodriver slot to an integer value
\param[in] index The slot number
\param[in] value The new contents of the slot
\param[in] format Format specifier e.g. for zero padded string to 6 characters format would be "%06d". Format may be decimal (d) or hex (x or X)
\param[in] send Whether to send a change of state message to the network or just update the slot contents silently
\returns true on error
\note The format parameter is the argument to "sprintf" and so must not contain more than one placeholder. If a percentage symbol is required then this should be entered as "%%" e.g. "%d%%".
*/
bool ccInfodriver::setSlot( int index, int value, const string & format, bool send )
{
	if( checkString( format, 16 ))
		return true;

	char buf[ 300 ];

	sprintf( buf, format.c_str(), value );

	return setSlot( index, buf, send );
}

/** Set the value of an infodriver slot to a double value
\param[in] index The slot number
\param[in] value The new contents of the slot
\param[in] format Format specifier e.g. for 3 decimal places the format string would be "%1.3f", to add the units "MHz" the format string would be "%1.3fMHz"
\param[in] send Whether to send a change of state message to the network or just update the slot contents silently
\returns true on error
\note The format parameter is the argument to "sprintf" and so must not contain more than one placeholder. If a percentage symbol is required then this should be entered as "%%" e.g. "%1.1f%%"
*/
bool ccInfodriver::setSlot( int index, double value, const string & format, bool send)
{
	if( checkString( format, 16 ))
		return true;

	char buf[ 300 ];

	sprintf( buf, format.c_str(), value );

	return setSlot( index, buf, send );
}

/** Update slot contents. 
This function is equivalent to setSlot() but only sends any message to the network if old and new contents are different
\param[in] index The slot number
\param[in] value The slot contents
\param[out] updated Return value which indicates whether the slot had changed (true) or was the same (false)
\sa setSlot();
*/
bool ccInfodriver::updateSlot( int index, const string & value, bool & updated )
{
	if( checkIndex( index ))
		return true;

	string ret;

	slot( index, ret );

	if( value != ret )
	{
		updated = true;
		return setSlot( index, value );
	}
	updated = false;
	return false;
}

/** Send a response to a cciClientMessage
\param[in] workstation The client that requested this message
\param[in] device The CC device number
\param[in] message The message to be sent
\param[in] client The client identifier (simply passed on from the client notification)
\param[in] reference The client reference (simply passed on from the client notification)

 Typical usage follows.....
\code
	virtual void myApp::cciClientMessage( int ws, int dev, const string & msg, const string & client, const string & ref )
	{
		string myMsg;

		// do something interesting here to answer the message sent to us in "message"
		//  and send the reply back to the client that sent it (i.e. keep the parameters we're passed intact)
		clientMessage( ws, dev, myMsg, client, ref);
	}
\endcode
\note Implementation of this messaging mechanism is patchy so beware....
*/
void ccInfodriver::clientMessage( int workstation, int device, const string & message, const string & client, const string & reference )
{
	if( checkWorkstation( workstation ))
		return;
	if( checkDevice( device ))
		return;
	if( checkString( message ))
		return;

	char buf[ 512 ];
	sprintf( buf, "NS %d %d '%s' %d %s %s", device, ccConfig::workstation(), message.c_str(), workstation, client.c_str(), reference.c_str() );

	BNCSSendMessage( BBC_DEVSTATUSMSG, 0, (LPARAM) buf, strlen( buf ) + 1, 0, CBDATA_TYPE_POINTER );
	
}

void ccInfodriver::setRedundancyMode( enum cc::redundancyMode mode )
{
	m_redundancyMode = mode;
}

/** Force the TXRX status of this driver
\param[in] mode Either tx or rx

 Typical usage follows.....
\code
	setRedundancyState( cc:rx );		// I'm broken - allow to go RX only 
	...
	setRedundancyState( cc:tx );		// I'm working - force myself TX/RX
\endcode
*/
void ccInfodriver::setRedundancyState( enum cc::redundancyState state )
{
	if( state == cc::tx )
	{
		SendMessage( m_hInfo, BBC_IDSETTXRXMODE, IFMODE_FORCETXRX, 0 );

		// remember our mode
		m_redundancyState = cc::tx;

		// we don't get separate indication of going to TXRX so generate one
		if( m_callback )
			m_callback->cciRedundancyState( cc::tx );

	}
	else if( state == cc::rx )
	{
		SendMessage( m_hInfo, BBC_IDSETTXRXMODE, IFMODE_RXONLY, 0 );

		// remember our mode
		m_redundancyState = cc::rx;

		if( m_callback )
			m_callback->cciRedundancyState( cc::rx );
	}
}

void ccInfodriver::setDriverState( enum cc::driverState state, bool fNow )
{
	m_driverState = state;

	if( fNow )
	{
		if( m_driverState == cc::ok )
			setRedundancyState( cc::tx );
		else if( m_driverState == cc::fail )
			setRedundancyState( cc::rx );
	}
}


enum cc::redundancyState ccInfodriver::redundancyState( void )
{
	return m_redundancyState;
}
