/*
PROGRAM NAME	Joysticks_C.dll

USED BY			BBC Academy Studio C SD installation

Version	Date		Programmer				Comments
======= ==========	==========				========
1.0		04-07-2017	Andy Woodhouse			Initial Release

TARGET SCREEN SIZE	1024 x 768 or larger
PANEL SIZE			800 x 600	(Automatic panel)

Overview
========
This automatic monitors the OCP overpress contacts in Wood Norton Studio C and switches the relevant
camera or the default sustain signal to the primary engineering monitor. When multiple joysticks are 
pressed simultaneously the leftmost pressed joystick source is selected (lower camera number precedence).

The contact closures on the joysticks connect to inputs on a FXtion GPI crate in the Studio C bays. 

Various entries in Instances.XML are used by this module:

Name							Information
======================			================================
"StC_Vision_Check"				BNCS driver ID for SDI router and destination switched by this module
"Studio C Camera 1 Joystick"	BNCS driver ID for GPI interface, and input GPI ID for leftmost joystick
"Studio C Camera 2 Joystick"	Input GPI ID for second camera
"Studio C Camera 3 Joystick"	Input GPI ID for third camera
"Studio C Camera 4 Joystick"	Input GPI ID for fourth camera
"Studio C Camera 5 Joystick"	Input GPI ID for fifth camera
"Studio C Camera 6 Joystick"	Input GPI ID for sixth camera
"StC Camera 1 In"				CCU 1/Camera 1 SDI input to Studio C Router
"StC Camera 2 In"				CCU 2/Camera 2 SDI input to Studio C Router
"StC Camera 3 In"				CCU 3/Camera 3 SDI input to Studio C Router
"StC Camera 4 In"				CCU 4/Camera 4 SDI input to Studio C Router
"StC Camera 5 In"				CCU 5/Camera 5 SDI input to Studio C Router
"StC Camera 6 In"				CCU 6/Camera 6 SDI input to Studio C Router

Although the allocations of cameras and joystick inputs are sequential, this task
supports a random allocation in case future expansions require more cameras and
joysticks.

*/

#include <windows.h>
#include <stdio.h>
#include <bncs_string.h>
#include <bncs_config.h>
#include "Joysticks_C.h"

// Define the parameters needed to show the User Interface
#define MAIN_PNL	1
#define	PANEL_NAME	"joysticks.bncs_ui"

// Define the properties for timers used
#define TIMER_GPI	1			// There is a short "settling" time after a GPI change in case more are in process queue
#define TIMER_GPI_TIMEOUT	20
#define TIMER_SDI	2			// At startup allow a short delay after polling GPI before poll the SDI destination
#define TIMER_SDI_DELAY	1000

#define BNCS_DRV_MIN	1		// Minimum supported BNCS Driver ID
#define BNCS_DRV_MAX	999		// Maximum supported BNCS Driver ID

#define JOY_OFF	"WN_GPI_Green"	// Statesheet name for mimic buttons when joystick is not pressed
#define JOY_ON	"WN_GPI_Red"	// Statesheet name for mimic buttons when joystick is pressed

///////////////////////////////////////////////
// Make our class visible to the outside world.
EXPORT_BNCS_SCRIPT(Joysticks_C)


//////////////////////////////////////////////////
// Constructor - equivalent to ApplCore STARTUP //
//////////////////////////////////////////////////
Joysticks_C::Joysticks_C(bncs_client_callback * parent, const char * path) : bncs_script_helper(parent, path)
{ 
	int dummy;		// Used when we must provide a variable address,
					// but we have zero interest in the returned value
	bool result;
	bool failedInit = false;
	bncs_string cam_id;

	// Initialise application wide variables.
	gpiDriver = 0;			// BNCS Driver ID of gpi interface to which joysticks connect
	sdiDriver = 0;			// BNCS Driver ID for SDI matrix
	sdiDest = 0;			// Destination that is switched by joystick presses
	nonPressed = 0;			// Source ID to select when no joystick is pressed.
	currentSource = 0;		// Keeps record of current sdi source id routed to sdiDest
	maxSDIdest = 0;			// Holds the maximum destination number for the SDI router
	minGPI = 0;				// Lowest numbered GPI contact for the leftmost joystick
	maxGPI = 0;				// Highest numbered GPI contact for rightmost joystick
	sdiRevertRx = false;	// Set true when a revertive has been seen from the SDI router
	joyRevertRx = false;	// Set true when all joysticks have returned a status revertive
	debounceTimer = false;	// Set true when the joystick debounce timer is running.
	for (int idx = 0; idx < NUM_JOYSTICKS; idx++)	stickPressed[idx] = 0;	// Flag per joystick indicating status (index range 0 ..5)
	for (int idx = 0; idx < NUM_JOYSTICKS; idx++)	stickID[idx] = 0;		// Holds the the GPI input ID for each joystick
	for (int idx = 0; idx < NUM_JOYSTICKS; idx++)	videoSource[idx] = 0;	// Video source to select when joystick idx is pressed and active
	for (int idx = 0; idx < NUM_JOYSTICKS; idx++)	jsRevertRx[idx] = 0;	// Clear array of revertive RX flags for joysticks

	js_down = JOY_ON;		// Set state sheet names for joystick mimics to working variables
	js_up = JOY_OFF;

	// Show our mimic panel from file joysticks.bncs_ui
	// We need this active so we can show error reports in the listbox.
	panelShow(MAIN_PNL, PANEL_NAME);

	result = getDev("StC_Vision_Check", &sdiDriver, &sdiDest);
	if ((!result) || (sdiDriver < BNCS_DRV_MIN) || (sdiDriver > BNCS_DRV_MAX))
	{
		ReportError(bncs_string("Failed to get sdi driver ID or SDI destination"));
		failedInit = true;
	}
	else
	{
		// Have a valid SDI driver ID, so need to check for valid destination.
		// Valid is between 1 and the maximum available in the router whose id we now have.
		maxSDIdest = getRouterSize(sdiDriver, 1);
		if (maxSDIdest == 0)
		{
			ReportError(bncs_string("Failed to get sdi router size.\n"));
			failedInit = true;
		}
		else
		{
			if ((sdiDest > 0) || (sdiDest <= maxSDIdest))
			{
				// Assume destination ok. Report values on the UI form
				textPut("text", bncs_string(sdiDriver), MAIN_PNL, "MtxID");
				textPut("text", bncs_string(sdiDest), MAIN_PNL, "MtxDest");
			}
		}

		// Build the array of source IDs switched by the joysticks
		for (int idx = 0; idx < NUM_JOYSTICKS; idx++)
		{
			cam_id = bncs_string("StC Camera %1 In").arg(idx + 1);	// Form name of camera number (1 to 6)
			result = getDev(cam_id, &dummy, &videoSource[idx]);		// Get the offset for that camera
			if (!result)
			{
				ReportError(bncs_string("Failed to get camera source sdi id for camera %d").arg(idx + 1));
				failedInit = true;
			}
		}
	}

	// Now process the joystick selectors. First extract the driver ID
	bncs_string jsID;

	jsID = bncs_string("Studio C Camera %1 Joystick").arg(1);

	result = getDev(jsID, &gpiDriver, &dummy);			// Read joystick driver ID
	if ((!result) || (gpiDriver < BNCS_DRV_MIN) || (gpiDriver > BNCS_DRV_MAX))
	{
		ReportError(bncs_string("Failed to get gpi driver ID joysticks"));
		failedInit = true;
	}
	else
	{
		// Report the GPI ID to the panel
		textPut("text", bncs_string(gpiDriver), MAIN_PNL, "GPIDriverID");

		// Read the joystick data
		for (int idx = 0; idx < NUM_JOYSTICKS; idx++)
		{
			jsID = bncs_string("Studio C Camera %1 Joystick").arg(idx+1);	// Need +1 offset between internal and external numbering
			result = getDev(jsID, &dummy, &stickID[idx]);					// Read joystick idx+1 GPI In contact number
			if (!result)
			{
				ReportError(bncs_string("Failed to get gpi contact id for joystick %d").arg(idx+1));
				failedInit = true;
			}
		}	// End for loop

		// Form the GPI contact data into a string and display on the User Interface panel
		bncs_stringlist gpiList;
		for (int idx = 0; idx < NUM_JOYSTICKS; idx++)
		{
			gpiList << stickID[idx];
		}
		bncs_string gpi_ids = gpiList.toString(',');		// Convert list of values to comma delimited string.
		textPut("text", gpi_ids, MAIN_PNL, "GPIslots");		// Show list on UI panel

	}	// End else clause of if ((!result) || (gpiDriver < BNCS_DRV_MIN) || (gpiDriver > BNCS_DRV_MAX))

	if (!failedInit)
	{
		// Compute the maximum and minimum GPI contacts ready for registration
		minGPI = stickID[0];
		maxGPI = stickID[0];
		for (int i = 0; i < NUM_JOYSTICKS; i++)
		{
			if (stickID[i] < minGPI) minGPI = stickID[i];
			if (stickID[i] > maxGPI) maxGPI = stickID[i];
		}
		gpiRegister(gpiDriver, minGPI, maxGPI);		// Register for GPI revertives.
		gpiPoll(gpiDriver, minGPI, maxGPI);			// Get current state of jooysticks

		// Register for SDI revertives.
		routerRegister(sdiDriver, sdiDest, sdiDest);	
		// Poll of sdi dstination will happen after GPI polls have had time to respond.
		// Use timer to initiate the SDI poll.
		timerStart(TIMER_SDI, TIMER_SDI_DELAY);		
	}
	else
	{
		textPut("add", "Initialisation errors found. Joysticks_C will not function correctly", MAIN_PNL, "ErrorList");
	}
}


///////////////////////////////////////////////////
// Destructor - equivalent to ApplCore CLOSEDOWN //
///////////////////////////////////////////////////
Joysticks_C::~Joysticks_C()
{
	// If we have a non-zero GPI driver ID unregister for revertives
	if (gpiDriver)
	{
		gpiUnregister(gpiDriver);
		gpiDriver = 0;
	}

	// If we have a non-zero SDI driver ID unregister for revertives
	if (sdiDriver)
	{
		routerUnregister(sdiDriver);
		sdiDriver = 0;
	}
}


////////////////////////////////////////////////////
// All button pushes and notifications come here. //
////////////////////////////////////////////////////
void Joysticks_C::buttonCallback(buttonNotify *b)
{
	// No buttons on the automatic panel.
}


///////////////////////////////
// All revertives come here. //
///////////////////////////////
int Joysticks_C::revertiveCallback(revertiveNotify *r)
{
	int revIndex;
	int id;
	bool jsp;		// true if JoyStick Pressed

	// We expect revertives from the vision matrix (our switched destination) 
	// and from the GPI interface that monitors the joysticks.

	if (r->device() == sdiDriver)
	{
		if (!sdiRevertRx)						// Is this first revertive from SDI router?
		{
			controlHide(MAIN_PNL, "Info_2");	// Hide the "Waiting for" status message
			sdiRevertRx = true;
		}
		// Update the current displays on the UI panel
		textPut("text", bncs_string(r->info()), MAIN_PNL, "CurrentSourceID");
		textPut("text", bncs_string(r->sInfo()), MAIN_PNL, "CurrentSourceName");
		// Keep record of current source
		currentSource = r->info();

		// If there are no joysticks pressed, and the joystick timer is not running we have a new default destination
		jsp = false;
		for (int i = 0; i < NUM_JOYSTICKS; i++)		// Scan all slots to see if any joystick is pressed.
		{
			if (stickPressed[i]) jsp = true;
		}
		if ((!jsp) && (!debounceTimer))
		{
			// Have a new default SDI source.
			nonPressed = r->info();										// Save new default source id
			textPut("text", nonPressed, MAIN_PNL, "DefaultSourceID");	// Show on mimic display
			textPut("text", r->sInfo(), MAIN_PNL, "DefaultSourceName");	// Show name of default source on mimic display
		}
	}

	else if (r->device() == gpiDriver)
	{
		revIndex = r->index();		// Need to use the index value several times - so hopefully this just speeds access

		if ((revIndex >= minGPI) && (revIndex <= maxGPI))	// Check revertive from our joystick slots
		{
			if (joyRevertRx)
			{
				// We have an update to a joystick state. Work out which joystick has changed
				id = 0;
				for (id = 0; id < NUM_JOYSTICKS; id++)
				{
					if (stickID[id] == revIndex)
						break;						// Quit the for loop as a match is found for index id
				}
				stickPressed[id] = r->info();		// Note the state of the joystick in the array
				textPut("statesheet", r->info() ? js_down : js_up, MAIN_PNL, bncs_string("JS_%1").arg(id + 1));

				// Trigger timer to allow close-timed events to be switch order independant
				timerStart(TIMER_GPI, TIMER_GPI_TIMEOUT);
				debounceTimer = true;				// Indicate timer is running
			}
			else
			{
				// Awaiting initial replies from all of the joysticks.
				// Work out which contact provided the revertive.
				id = 0;
				for (id = 0; id < NUM_JOYSTICKS; id++)
				{
					if (stickID[id] == revIndex)
						break;
				}
				// id has joystick index (zero means leftmost stick)
				stickPressed[id] = r->info();	// Note the state of the joystick
				textPut("statesheet", r->info() ? js_down : js_up, MAIN_PNL, bncs_string("JS_%1").arg(id + 1));
				jsRevertRx[id] = 1;				// Set flag to show js has returned a tally

				// Check if all joysticks have returned a revertive
				int sum = 0;
				for (int i = 0; i < NUM_JOYSTICKS; i++)
				{
					sum += jsRevertRx[i];
				}
				if (sum == NUM_JOYSTICKS)
				{
					controlHide(MAIN_PNL, "Info_1");		// Hide the status box
					joyRevertRx = true;
				}
			}
		}
	}	// End of else if (r->device() == gpiDriver)
		
	return 0;
}


///////////////////////////////////////////////
// All database name changes come back here. //
///////////////////////////////////////////////
void Joysticks_C::databaseCallback(revertiveNotify *r)
{
}


//////////////////////////////////////////////////////////////////////////
// All parent notifications come here i.e. when this script is just one //
// component of another dialog, our host might want to tell us things.  //
//////////////////////////////////////////////////////////////////////////
bncs_string Joysticks_C::parentCallback( parentNotify *p )
{
	if (p->command() == "return")
	{
		if (p->value() == "all")
		{	
			// Persisting values for bncs_vis_ed - NONE
			bncs_stringlist sl;
			sl << bncs_string("");
			return sl.toString( '\n' );
		}
	}

	// ***** CONNECTIONS EVENTS HELPER LIST *****
	else if (p->command() == "_events")
	{	
		// Helper-list of everything in this component generated by hostNotify's
		bncs_stringlist sl;
		sl << "";		
		return sl.toString( '\n' );
	}

	// ***** CONNECTIONS COMMANDS HELPER LIST *****
	else if (p->command() == "_commands")
	{	
		// Helper-list of any commands/parameters you might want to set at run-time
		bncs_stringlist sl;
		sl << "";
		return sl.toString( '\n' );
	}

	return "";
}


////////////////////////////
// Timer events come here //
////////////////////////////
void Joysticks_C::timerCallback(int id)
{
	int jsid;

	switch (id)
	{
		case TIMER_GPI:				// Debounce timeout has happened.
			timerStop(TIMER_GPI);
			debounceTimer = false;	// Set flag to show timer has stopped.
			// Look to see if any joysticks are pressed. If so route signal associated with leftmost joystick
			jsid = -1;
			for (int i = 0; i < NUM_JOYSTICKS; i++)
			{
				if (stickPressed[i])
				{
					jsid = i;	// Found a pressed joystick, note the index.
					break;		// Leave the for loop
				}
			}
			if (jsid == -1)
			{
				// No joystick pressed, so route default source if not already routed
				if (currentSource != nonPressed)
				{
					routerCrosspoint(sdiDriver, nonPressed, sdiDest);
				}
			}
			else
			{
				// Select camera source from array of sources
				if (currentSource != videoSource[jsid])
				{
					routerCrosspoint(sdiDriver, videoSource[jsid], sdiDest);
				}
			}
			break;


		case TIMER_SDI:
			timerStop(id);
			if (joyRevertRx)
			{
				routerPoll(sdiDriver, sdiDest, sdiDest);		// Poll to get current setting
			}
			else
			{
				textPut("add", "Re-poll GPI, reset SDI poll timer.", MAIN_PNL, "ErrorList");
				gpiPoll(gpiDriver, minGPI, maxGPI);				// Repoll the GPIs
				timerStart(TIMER_SDI, TIMER_SDI_DELAY);			// Re-start SDI poll timer.
			}
			break;

		default:	// Unhandled timer event
			timerStop(id);
			break;
	}
}


///////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////// Callbacks above - Methods below ///////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////


void Joysticks_C::ReportError(bncs_string err_msg)
{
	// Add error report to listbox
	textPut("add", err_msg, MAIN_PNL, "ErrorList");

	// Send error report to debug tool
	debug(bncs_string("Joysticks_C: " + err_msg + bncs_string("\n")));
}