/***********************************************************************/
/*                                                                     */
/*                      ADOBE CONFIDENTIAL                             */
/*                   _ _ _ _ _ _ _ _ _ _ _ _ _                         */
/*                                                                     */
/*  Copyright 2016 Adobe Systems Incorporated                          */
/*  All Rights Reserved.                                               */
/*                                                                     */
/* NOTICE:  All information contained herein is, and remains           */
/* the property of Adobe Systems Incorporated and its suppliers,       */
/* if any.  The intellectual and technical concepts contained          */
/* herein are proprietary to Adobe Systems Incorporated and its        */
/* suppliers and are protected by all applicable intellectual property */
/* laws, including trade secret and copyright laws.                    */
/* Dissemination of this information or reproduction of this material  */
/* is strictly forbidden unless prior written permission is obtained   */
/* from Adobe Systems Incorporated.                                    */
/*                                                                     */
/***********************************************************************/

function WorkflowProcessorState(/*[Number]*/ inMaxSteps)
{
	this.step		= NaN;
	this.stepRetry	= NaN;
	this.isSuccess	= false;
	this.isFailure	= false;
	
	var maxStep = inMaxSteps;
	
	//////////////////////////////////////////////////////////////////////////////
	//
	// Reset
	//
	this.reset = function(/*[Boolean]*/ inInit)
	{
		if (inInit)
		{
			this.step		= NaN;
			this.stepRetry	= NaN;
			this.isSuccess	= false;
			this.isFailure	= false;
		}
		else
		{
			this.step		= 0;
			this.stepRetry	= 0;
			this.isSuccess	= false;
			this.isFailure	= false;
		}
	}
	
	//////////////////////////////////////////////////////////////////////////////
	//
	// Increase step counter, call callback when exceeding max count
	//
	this.incStep = function(/*[Function]*/ inCallback)
	{
		if (isNaN(this.step))
		{
			this.step = 0;
		}
		else
		{
			this.step++;
		}
		
		var ret = (this.step < maxStep);
		
		if (ret)
		{
			this.stepRetry = 0;
		}
		else if (isValidProperty(inCallback))
		{
			inCallback();
		}
		
		return ret;
	}
	
	//////////////////////////////////////////////////////////////////////////////
	//
	// Decrease step counter, call callback when fall below 0
	//
	this.decStep = function(/*[Function]*/ inCallback)
	{
		if (isNaN(this.step))
		{
			this.step = 0;
		}
		else
		{
			this.step--;
		}
		
		var ret = (this.step >= 0);
		
		if (ret)
		{
			this.stepRetry = 0;
		}
		else if (isValidProperty(inCallback))
		{
			inCallback();
		}
		
		return ret;
	}

	//////////////////////////////////////////////////////////////////////////////
	//
	// Set step counter, call callback when exceeding max count
	//
	this.jumpStep = function(/*Number*/ inStepNo, /*[Function]*/ inCallback)
	{
		var ret = false;

		if (inStepNo >= 0 && inStepNo < maxStep)
		{
			this.step = inStepNo;
			this.stepRetry = 0;
			ret = true;
		}
		else if (isValidProperty(inCallback))
		{
			inCallback();
		}

		return ret;
	}

	//////////////////////////////////////////////////////////////////////////////
	//
	// Increase retry count
	//
	this.incRetry = function()
	{
		if (isNaN(this.stepRetry))
		{
			this.stepRetry = 0;
		}
		else
		{
			this.stepRetry++;
		}
		
		this.isFailure = false;
	}

	//////////////////////////////////////////////////////////////////////////////
	//
	// Set success flag
	//
	this.setSuccess = function()
	{
		this.isSuccess = true;
	}
	
	//////////////////////////////////////////////////////////////////////////////
	//
	// Set failure flag
	//
	this.setFailure = function()
	{
		this.isFailure = true;
	}
	
	//////////////////////////////////////////////////////////////////////////////
	//
	// Clear failure flag
	//
	this.clearFailure = function()
	{
		this.isFailure = false;
	}
}

//////////////////////////////////////////////////////////////////////////////
//
function WorkflowProcessor(/*[Workflow]*/ inWorkflow, /*[PanelSurface]*/ inSurface)
{
	throwInvalid(inWorkflow);
	throwInvalid(inSurface);

	var thisObj = this;
	var workflow = inWorkflow;
	var display = new WorkflowDisplayAdapter(inSurface, workflow);
	var maxSteps = inWorkflow.getStepLength();
	var state = new WorkflowProcessorState(maxSteps);
	var timerID = NaN;
	var timeMeasure = {};
	var subProcessor = null;
	
	this.triggerHandler = null;

	// unique workflow identifier for this processing
	workflow.session = generateUUID();
	
	//////////////////////////////////////////////////////////////////////////////
	//
	// Initialize Processor
	//
	this.initialize = function()
	{
		state.reset(true);
		display.showInitial();

		if (!isNaN(timerID))
		{
			clearTimeout(timerID);
		}

		dbglogInfo('WorkflowProcessor INIT - Workflow: "' + workflow.name + '" [' + workflow.id + ']');
	}
	
	//////////////////////////////////////////////////////////////////////////////
	//
	// Return is started
	//
	this.isStarted = function()
	{
		return !isNaN(state.step);
	}
	
	//////////////////////////////////////////////////////////////////////////////
	//
	// Return is in failure
	//
	this.isFailure = function()
	{
		return state.isFailure;
	}

	//////////////////////////////////////////////////////////////////////////////
	//
	// Return true if interactive step succeeded
	//
	this.isSuccess = function()
	{
		return state.isSuccess;
	}

	//////////////////////////////////////////////////////////////////////////////
	//
	// Return current step number
	//
	this.getCurrentStep = function()
	{
		return state.step;
	}
	
	//////////////////////////////////////////////////////////////////////////////
	//
	// Return measured time
	//
	this.getMeasurements = function()
	{
		return timeMeasure;
	}
	
	//////////////////////////////////////////////////////////////////////////////
	//
	// Return how often the current step was retried
	//
	this.getRetry = function()
	{
		return state.stepRetry;
	}
	
	//////////////////////////////////////////////////////////////////////////////
	//
	// Start workflow processing
	//
	this.start = function()
	{
		this.uninstallTrigger(state.step);
		state.reset(false);

		if (!isNaN(timerID))
		{
			clearTimeout(timerID);
		}

		var step = workflow.getStep(state.step);

		step.validatePreConditions(function()
		{
			// validation of pre conditions OK
			//
			runStep(state.step);
			gBroadcaster.postMessage(WorkflowProcessor.STARTED);

			startMeasure('workflow');
			startMeasure('step_total' + state.step);
			dbglogInfo('WorkflowProcessor START - Workflow: "' + workflow.name + '" [' + workflow.id + ']');
		},
		function()
		{
			// validation of pre conditions FAILED
			//
			dbglogInfo('WorkflowProcessor preCondition SKIP STEP - Workflow: "' + workflow.name + '" [' + workflow.id + ']');
			gBroadcaster.sendMessage(CLICKEVENT_WORKFLOW_STEP_SKIP);
		});
	}

	//////////////////////////////////////////////////////////////////////////////
	//
	// Switch to next step 
	// (if there's no more steps then finish processing)
	//
	this.nextStep = function()
	{
		endMeasure('step_total' + state.step);
		this.uninstallTrigger(state.step);

		if (!isNaN(timerID))
		{
			clearTimeout(timerID);
		}

		if (!isNaN(state.step))
		{
			// switch from one step to the next one
			gBroadcaster.postMessage(WorkflowProcessor.SWITCH_STEP_NEXT);
		}

		if (state.incStep(function()
			{
				finish(WorkflowProcessor.FINISHED_SUCCESS);
			}))
		{
			var processor = this;
			var step = workflow.getStep(state.step);

			step.validatePreConditions(function()
			{
				// validation of pre conditions OK
				//
				runStep(state.step);

				startMeasure('step_total' + state.step);
				dbglogInfo('WorkflowProcessor NEXT - Workflow: "' + workflow.name + '" [' + workflow.id + ']');
			},
			function()
			{
				// validation of pre conditions FAILED

				dbglogInfo('WorkflowProcessor preCondition SKIP STEP - Workflow: "' + workflow.name + '" [' + workflow.id + ']');
				gBroadcaster.sendMessage(CLICKEVENT_WORKFLOW_STEP_SKIP);
			});
		}
	}
	
	//////////////////////////////////////////////////////////////////////////////
	//
	// Switch to previous step
	// (if there's no previous step then stay with the current)
	//
	this.prevStep = function()
	{
		endMeasure('step_total' + this.step);
		this.uninstallTrigger(this.step);

		if (!isNaN(timerID))
		{
			clearTimeout(timerID);
		}

		if (!isNaN(state.step))
		{
			// switch from one step to the previous one
			gBroadcaster.postMessage(WorkflowProcessor.SWITCH_STEP_PREV);
		}

		if (state.decStep(function() { gBroadcaster.sendMessage(WorkflowProcessor.RESTART); }))
		{
			runStep(state.step);
			startMeasure('step_total' + state.step);
			dbglogInfo('WorkflowProcessor PREV - Workflow: "' + workflow.name + '" [' + workflow.id + ']');
		}
	}
	
	//////////////////////////////////////////////////////////////////////////////
	//
	// Refresh current step
	//
	this.refreshStep = function()
	{
		this.uninstallTrigger(state.step);
		
		state.incRetry();

		if (!isNaN(timerID))
		{
			clearTimeout(timerID);
		}

		runStep(state.step);
		dbglogInfo('WorkflowProcessor REFRESH - Workflow: "' + workflow.name + '" [' + workflow.id + ']');
	}

	//////////////////////////////////////////////////////////////////////////////
	//
	// Switch to given step
	// (if there's no more steps then finish processing)
	//
	this.jumpStep = function(/*Number*/ inStepNo)
	{
		if (isNaN(inStepNo))
		{
			// cancel workflow
			//
			this.cancel();
		}
		else
		{
			endMeasure('step_total' + state.step);
			this.uninstallTrigger(state.step);

			if (!isNaN(timerID))
			{
				clearTimeout(timerID);
			}

			if (!this.isStarted())
			{
				state.reset(false);
			}

			if (!isNaN(state.step))
			{
				// post switch from one step to another one
				gBroadcaster.postMessage(WorkflowProcessor.SWITCH_STEP_JUMP);
			}

			if (inStepNo < 0)
			{
				// jum to workflow start page
				//
				gBroadcaster.sendMessage(WorkflowProcessor.RESTART);
			}
			else
			{
				// jump to workflow step
				//
				if (state.jumpStep(inStepNo, function() { finish(WorkflowProcessor.FINISHED_SUCCESS); }))
				{
					runStep(state.step);
					startMeasure('step_total' + state.step);
					dbglogInfo('WorkflowProcessor JUMP - Workflow: "' + workflow.name + '" [' + workflow.id + ']');
				}
			}
		}
	}

	//////////////////////////////////////////////////////////////////////////////
	//
	// User did something unexpected for the current step
	//
	this.failure = function(/*[Any]*/ inData)
	{
		endMeasure('step_total' + state.step);
		dbglogInfo('WorkflowProcessor FAILURE - Workflow: "' + workflow.name + '" [' + workflow.id + ']');
		state.setFailure();
		finish(WorkflowProcessor.FINISHED_FAILURE, inData)
	}
	
	//////////////////////////////////////////////////////////////////////////////
	//
	// User canceled the workflow processing
	//
	this.cancel = function()
	{
		endMeasure('step_total' + state.step);
		this.uninstallTrigger(state.step);

		try
		{
			display.showCancel(this.step);
		}
		catch(exc)
		{
			exclog(exc);
		}
		
		dbglogInfo('WorkflowProcessor CANCEL - Workflow: "' + workflow.name + '" [' + workflow.id + ']');
		finish(WorkflowProcessor.FINISHED_CANCELED);
	}
	
	//////////////////////////////////////////////////////////////////////////////
	//
	// Install trigger for current step
	//
	this.installTrigger = function(/*[Number]*/ inStepNumber)
	{
		if (!isNaN(inStepNumber))
		{
			var wfStep = workflow.getStep(inStepNumber);
			
			if (isValidProperty(wfStep))
			{
				var triggers = wfStep.getTriggers();

				if (isValidProperty(triggers) && triggers.length > 0)
				{
					try
					{
						var processor = this;
						this.triggerHandler = new WorkflowTriggerHandler(triggers, function(/*[String]*/inReason, /*WorkflowTriggerValidationStatus*/ inStatus)
						{
							processor.onTrigger(inReason, inStatus);
						});
						this.triggerHandler.installHandler();
					}
					catch(exc)
					{
						exclog(exc);
						this.triggerHandler = null;
					}
				}
			}
		}
	}
	
	//////////////////////////////////////////////////////////////////////////////
	//
	// Uninstall trigger for current step
	//
	this.uninstallTrigger = function(/*[Number]*/ inStepNumber)
	{
		// clear sub-processor
		finalizeSubProcessor();

		// clear event objects
		var job = new ScriptJob('clearEvents();');
		job.wait(null, function(/*[String]*/ inError)
		{
			dbglogError(inError);
		});

		if (isValidProperty(this.triggerHandler))
		{
			this.triggerHandler.uninstallHandler();
			this.triggerHandler = null;
		}
	}

	// private ///////////////////////////////////////////////////////////////////

	//////////////////////////////////////////////////////////////////////////////
	//
	// Start workflow step processing
	//
	function runStep(/*Number*/ inStep)
	{
		var step = workflow.getStep(state.step);
		if (step.hasSubSteps())
		{
			startSubProcessor(inStep);
		}
		else
		{
			try
			{
				display.showStep(inStep);
			}
			catch (exc)
			{
				exclog(exc);
			}

			thisObj.installTrigger(inStep);
		}
	}

	//////////////////////////////////////////////////////////////////////////////
	//
	// Start SubProcessor for given step
	//
	function startSubProcessor(/*Number*/ inStep)
	{
		gBroadcaster.registerHandler(SubProcessor.SKIP_SUBSTEP, thisObj.onMessage, thisObj);
		gBroadcaster.registerHandler(SubProcessor.FINISHED, thisObj.onMessage, thisObj);
		gBroadcaster.registerHandler(SubProcessor.FINISHED_SUBSTEP_SUCCESS, thisObj.onMessage, thisObj);

		try
		{
			// display main content of this step
			display.showStep(inStep);

			// create and start sub-processor
			subProcessor = SubProcessor.create(SubProcessor.TYPE_SEQUENCE, workflow, inStep, display);
			throwInvalid(subProcessor);

			subProcessor.start();
		}
		catch (exc)
		{
			finalizeSubProcessor();
			exclog(exc);
		}
	}

	//////////////////////////////////////////////////////////////////////////////
	//
	// Delete current SubProcessor
	//
	function finalizeSubProcessor()
	{
		gBroadcaster.unregisterHandler(SubProcessor.SKIP_SUBSTEP, thisObj.onMessage, thisObj);
		gBroadcaster.unregisterHandler(SubProcessor.FINISHED, thisObj.onMessage, thisObj);
		gBroadcaster.unregisterHandler(SubProcessor.FINISHED_SUBSTEP_SUCCESS, thisObj.onMessage, thisObj);

		if (isValidProperty(subProcessor))
		{
			subProcessor.cancel();
			subProcessor = null;
		}
	}

	//////////////////////////////////////////////////////////////////////////////
	//
	// Finalize step processing
	//
	function finishStep()
	{
		endMeasure('step_total' + state.step);
		thisObj.uninstallTrigger(state.step);
		state.clearFailure();
		state.setSuccess();

		gBroadcaster.sendMessage(WorkflowProcessor.FINISHED_STEP_SUCCESS);

		var step = workflow.getStep(state.step);

		switch(step.stepswitch)
		{
			case Workflow.STEPSWITCH_AUTOMATIC:
			{
				// start next step with a short delay
				timerID = setTimeout(function()
				{
					timerID = NaN;
					gBroadcaster.sendMessage(WorkflowProcessor.FINISHED_NEXT_STEP);
				}, TIMEOUT_SWITCH_NEXT_STEP);
			}
				break;

			case Workflow.STEPSWITCH_STEP_AUTOMATIC:
			{
				var timeout = step.switchtimeout;

				if (isNaN(timeout) || timeout <= 0)
				{
					gBroadcaster.sendMessage(WorkflowProcessor.FINISHED_NEXT_STEP);
				}
				else
				{
					timerID = setTimeout(function()
					{
						timerID = NaN;
						gBroadcaster.sendMessage(WorkflowProcessor.FINISHED_NEXT_STEP);
					}, timeout);
				}
			}
				break;
		}
	}

	//////////////////////////////////////////////////////////////////////////////
	//
	// Finalize workflow processing
	//
	function finish(/*[String]*/ inWhy, /*[Any]*/ inData)
	{
		dbglogInfo('WorkflowProcessor FINISH (Reason:"' + inWhy + '") - Workflow: "' + workflow.name + '" [' + workflow.id + ']');
		
		if (!isNaN(timerID))
		{
			clearTimeout(timerID);
		}

		gBroadcaster.sendMessage(inWhy, inData);
	}

	//////////////////////////////////////////////////////////////////////////////
	//
	// Handle broadcast messages
	//
	this.onMessage = function(/*[String]*/ inCommand)
	{
		switch(inCommand)
		{
			// Sub-step finished or skipped
			//
			case SubProcessor.FINISHED_SUBSTEP_SUCCESS:
			case SubProcessor.SKIP_SUBSTEP:
			{
				throwInvalid(subProcessor);
				if (!subProcessor.isStarted())
				{
					throwlog("SubProcessor not started, but SubProcessor.FINISHED_SUBSTEP_SUCCESS signaled");
				}

				subProcessor.nextSubStep();
			}
			break;

			case SubProcessor.FINISHED:
			{
				throwInvalid(subProcessor);
				if (!subProcessor.isStarted())
				{
					throwlog("SubProcessor not started, but SubProcessor.FINISHED signaled");
				}

				finalizeSubProcessor();
				finishStep();
			}
			break;
		}
	}

	//////////////////////////////////////////////////////////////////////////////
	//
	// Trigger handler callback function
	//
	this.onTrigger = function(/*[String]*/ inReason, /*WorkflowTriggerValidationStatus*/ inStatus)
	{
		dbglogInfo('WorkflowProcessor TRIGGER (Reason:"' + inReason + '") - Workflow: "' + workflow.name + '" [' + workflow.id + ']');
		
		switch (inReason)
		{
			case WorkflowProcessor.FINISHED_STEP_SUCCESS:
			{
				finishStep();
			}
			break;
			
			case WorkflowProcessor.FINISHED_FAILURE:
			{
				this.uninstallTrigger(state.step);
				
				var data = null;
				
				if (isValidProperty(inStatus) &&
				    isValidProperty(inStatus.trigger) &&
				    isValidProperty(inStatus.trigger.content))
				{
					data = inStatus.trigger.content;
				}
				
				var processor = this;
				processor.failure(data);
			}
			break;
		}
	}

	//////////////////////////////////////////////////////////////////////////////
	//
	function startMeasure(/*[String]*/ inName)
	{
		timeMeasure[inName] = new TimeMeasure();
	}

	//////////////////////////////////////////////////////////////////////////////
	//
	function endMeasure(/*[String]*/ inName)
	{
		if (isValidProperty(timeMeasure[inName]))
		{
			timeMeasure[inName].end();
		}
	}
}

WorkflowProcessor.STARTED 				= 'started';
WorkflowProcessor.RESTART				= 'restart';
WorkflowProcessor.FINISHED_SUCCESS 		= 'success';
WorkflowProcessor.FINISHED_FAILURE 		= 'failure';
WorkflowProcessor.FINISHED_CANCELED 	= 'canceled';
WorkflowProcessor.FINISHED_STEP_SUCCESS = 'step_success';
WorkflowProcessor.FINISHED_NEXT_STEP 	= 'next_step';
WorkflowProcessor.SWITCH_STEP_NEXT 		= 'switch_next';
WorkflowProcessor.SWITCH_STEP_PREV 		= 'switch_prev';
WorkflowProcessor.SWITCH_STEP_JUMP		= "switch_jump";