//TODO: evaluate calling of EvaluatePossibleNavigationRequests to ensure that display state is always updated appropriately (i think we're missing a call after
//loading a sco, if we add this, make sure it's not redundant with the call in ScoUnloaded

function Controller(){
	
	//state data
	this.ProcessedUnload = false;
	this.MenuIsVisible = false;
	this.Initialized = false;
	this.ExitScormPlayerCalled = false;
	
	//overall processing
	this.Initialize = Controller_Initialize;
	this.Unload = Controller_Unload;
	
	//menu functions
	this.CreateMenuItem = Controller_CreateMenuItem;
	this.RenderMenuItem = Controller_RenderMenuItem;
	this.RedrawChildren = Controller_RedrawChildren;
	this.UpdateDisplay = Controller_UpdateDisplay;
	this.RefreshPage = Controller_RefreshPage;
	
	//activity
	this.Activities = null;
	
	//logistics
	this.ScoLoader = null;
	this.ScoUnloaded = Controller_ScoUnloaded;
	this.ExitScormPlayer = Controller_ExitScormPlayer;
	this.ExitSco = Controller_ExitSco;
	
	//dirty data management
	this.MarkPostedDataDirty = Controller_MarkPostedDataDirty;
	this.MarkPostedDataClean = Controller_MarkPostedDataClean;
	this.MarkDirtyDataPosted = Controller_MarkDirtyDataPosted;
	this.GetXmlForDirtyData = Controller_GetXmlForDirtyData;
	this.IsThereDirtyData = Controller_IsThereDirtyData;
	
	//error management
	this.DisplayError = Controller_DisplayError;
	this.GetExceptionText = Controller_GetExceptionText;
	
	this.TildaCounter = 0;
	this.QuestionCounter = 0;
	this.CheckForDebugCommand = Controller_CheckForDebugCommand;
	
	//GUI Event Handlers
	this.CloseSco = Controller_CloseSco;
	this.ReturnToLms = Controller_ReturnToLms;
	this.GetReturnToLmsNavigationRequest = Controller_GetReturnToLmsNavigationRequest;
	this.ToggleMenuVisibility = Controller_ToggleMenuVisibility;
	this.Next = Controller_Next;
	this.Previous = Controller_Previous;
	this.Abandon = Controller_Abandon;
	this.AbandonAll = Controller_AbandonAll;
	this.Suspend = Controller_Suspend;
	this.Exit = Controller_Exit;
	this.ExitAll = Controller_ExitAll;
	this.ChoiceRequest = Controller_ChoiceRequest;
	
	this.ScoHasTerminatedSoUnload = Controller_ScoHasTerminatedSoUnload;
	this.SignalTerminated = Controller_SignalTerminated;
	this.TranslateRunTimeNavRequest = Controller_TranslateRunTimeNavRequest;
	this.GetMessageText = Controller_GetMessageText;
	
	this.ClearPendingNavigationRequest = Controller_ClearPendingNavigationRequest;
	this.IsThereAPendingNavigationRequest = Controller_IsThereAPendingNavigationRequest;
	this.PendingNavigationRequest = null;
	
	//Sequencing
	this.Sequencer = null;
	this.LookAheadSequencer = null;
	this.DeliverActivity = Controller_DeliverActivity;
	this.PerformDelayedDeliveryInitialization = Controller_PerformDelayedDeliveryInitialization;
	
	this.PossibleNavigationRequests = new Array();
	this.InitializePossibleNavigationRequests = Controller_InitializePossibleNavigationRequests;
	this.EvaluatePossibleNavigationRequests= Controller_EvaluatePossibleNavigationRequests;
	this.FindPossibleChoiceRequestForActivity = Controller_FindPossibleChoiceRequestForActivity;
	
	this.IsTargetValid = Controller_IsTargetValid;
	this.ParseTargetStringIntoActivity = Controller_ParseTargetStringIntoActivity;
	this.IsChoiceRequestValid = Controller_IsChoiceRequestValid;
	this.IsContinueRequestValid = Controller_IsContinueRequestValid;
	this.IsPreviousRequestValid = Controller_IsPreviousRequestValid;
	this.ParseTargetStringFromChoiceRequest = Controller_ParseTargetStringFromChoiceRequest;
	
	this.CloneSequencer = Controller_CloneSequencer;
	this.TearDownSequencer = Controller_TearDownSequencer;
	
	//Runtime
	this.Api = null;
	
	//Debug
	this.WriteAuditLog = Controller_WriteAuditLog;
	this.WriteDetailedLog = Controller_WriteDetailedLog;
	this.WriteDetailedLogError = Controller_WriteDetailedLogError;
}


function Controller_Initialize(){	
	
	this.WriteAuditLog("Control Initialize");
	
	this.Api = apiReference;
	this.Package = RegistrationToDeliver.Package;
		
	this.Activities = new ActivityRepository();
	this.Activities.InitializeFromRegistration(RegistrationToDeliver, QuerystringAdditions);
	
	this.Sequencer = new Sequencer(false, this.Activities);
	this.Sequencer.GlobalObjectives = RegistrationToDeliver.GlobalObjectives;
	this.Sequencer.Activities.SetSequencer(this.Sequencer, false);
	
	this.InitializePossibleNavigationRequests();
	
	var suspendedActivity = this.Activities.GetActivityByDatabaseId(RegistrationToDeliver.SuspendedActivity);
	this.Sequencer.SetSuspendedActivity(suspendedActivity);

	this.Sequencer.InitialRandomizationAndSelection();
	
	this.CreateMenuItem(null, this.Activities.ActivityTree, IntegrationImplementation.GetDocumentObjectForMenu());
	
	this.RenderMenuItem(this.Activities.ActivityTree);
	
	IntegrationImplementation.SetMenuToggleVisibility(this.Package.Properties.ShowCourseStructure);
	
	if (this.Package.Properties.ShowCourseStructure === true &&
		this.Package.Properties.CourseStructureStartsOpen === true){
		this.ToggleMenuVisibility();	//intialized as false
	}
	else{
		IntegrationImplementation.HideMenu();
	}
	
	this.LookAheadSequencer = new Sequencer(true, this.Sequencer.Activities.Clone());
	
	this.Comm = new Communications();
	
	//If we're launching an AICC course, then we don't want to send any posts to the server, the content will take care of it
	//Also disable communications if the registration is in review/preview mode indicated by TrackingEnabled = false 
	if (this.Package.LearningStandard == STANDARD_AICC || RegistrationToDeliver.TrackingEnabled === false) {
		this.Comm.Disable();
	}
	
	this.Comm.StartPostDataProcess();

	// Build full URL's using the relative URLs for Intermediate and Popup pages.  These files will be found
	// relative to the deliver page itself.  Fully qualified urls are built due to an incompatability within
	// Safari using relative URLs.
	var deliveryPageUrl = window.location.toString();
	IntermediatePage.PageHref = BuildFullUrl(IntermediatePage.PageHref, deliveryPageUrl);
	PopupLauncherPage.PageHref = BuildFullUrl(PopupLauncherPage.PageHref, deliveryPageUrl);
	
	this.ScoLoader = new ScoLoader(IntermediatePage, PopupLauncherPage, PathToCourse, 
								   RegistrationToDeliver.Package.Properties.ScoLaunchType,
								   RegistrationToDeliver.Package.Properties.WrapScoWindowWithApi,
								   RegistrationToDeliver.Package.LearningStandard);
	
	this.Initialized = true;
	
	this.Sequencer.Start();
	
	//todo: make it an option to invoke the look ahead sequencer asychronously (can improve apparent load speed on very large courses)
	//window.setTimeout("Control.EvaluatePossibleNavigationRequests()", 1);
	this.EvaluatePossibleNavigationRequests();
}

function Controller_Unload(){
	
	//TODO: should we try to unload the SCO first before saving the data?
	//if there's a sco that only sets status onunload, and we have single sco config (i.e. no nav), then the
	//only way to get completion is to just close the window, in this case, we might not capture the completion
	//depending on the timing of the unload...the best thing to do in this scenario is to have the player launch
	//type be frameset and the sco launch type be new window
	
	this.WriteAuditLog("Control Unload");

	if (this.ProcessedUnload === false){
		
		this.ProcessedUnload = true;
		
		if (SHOULD_SAVE_CLIENT_DEBUG_LOGS) {
		    this.Comm.SaveDebugLog();
			// Upon exit, send back text map as well for substituted short-hand strings
		    var isTextMap = true;
		    this.Comm.SaveDebugLog(getSerializedCompressedTextMap(), isTextMap);
		}
		
		if (this.ExitScormPlayerCalled === false){
		
			// Either Unload() or ExitScormPlayer() should call SaveDataOnExit().  If Exit hasn't been called, do it here.
			this.Comm.SaveDataOnExit();
		
			if (this.Package.Properties.PlayerLaunchType != LAUNCH_TYPE_FRAMESET){
				
				try{
					if (window.opener && window.opener !== null && window.opener.closed === false){
						window.opener.location = RedirectOnExitUrl;
					}
				}catch(e){}
				
				window.close();
			}
		}
	}
}


//private
function Controller_CreateMenuItem(parentMenuItem, activity, documentObject){
	
	//this.WriteAuditLog("Control CreateMenuItem for " + activity);
	
	activity.MenuItem = new MenuItem(parentMenuItem, activity, documentObject);
	
	if (parentMenuItem !== null){
		parentMenuItem.Children[parentMenuItem.Children.length] = activity.MenuItem;
	}
	
	var availableChildren = activity.GetAvailableChildren();
	
	for (var childActivity in availableChildren){
		this.CreateMenuItem(activity.MenuItem, availableChildren[childActivity], documentObject);
	}
}

//private
function Controller_RenderMenuItem(activity){
	
	//this.WriteAuditLog("Control RenderMenuItem for " + activity);
	
	//JMH - 1/20/07 - This seems odd, but the activity is now a param to Render().  The reason being is that, while the 
	// activity has an associated MenuItem, it's now possible that different activity data can be used to render it
	// (main sequencer or lookahead).  As such, the activity object is no longer a local member of MenuItem, thus we
	// need to pass it in.
	activity.MenuItem.Render(activity);
	
	var childActivities = activity.GetAvailableChildren();
	
	for (var childActivity in childActivities){
		this.RenderMenuItem(childActivities[childActivity]);
	}	
}

//protected - available to sequencing functions only
function Controller_RedrawChildren(activity){
	activity.MenuItem.ResynchChildren(activity);
	this.RenderMenuItem(activity);
}


//private 
function Controller_UpdateDisplay(useLookAheadDataNavInfo, useLookAheadActivityStatus){
	
	var logParent = this.WriteAuditLog("Control Update Display");

	// We have two uses for lookahead data
	// 1. We want it's navigation choice result so use the looahead to say "what would happen".  We can use this to appropriately display a link
	// 2. While we're inside a sco, before exiting and after each commit we run the lookahead sequencer.  The activity status data at that
	//    moment is more valid/up-to-date than the main sequencer.  So we want to use lookahead activity data after commits.

	// Make sure input params are usable
	if (this.Package.Properties.LookaheadSequencerMode !== LOOKAHEAD_SEQUENCER_MODE_ENABLE) {
		useLookAheadDataNavInfo = false;
		useLookAheadActivityStatus = false;
	} else {
		if (useLookAheadDataNavInfo === undefined || useLookAheadDataNavInfo === null) {
			useLookAheadDataNavInfo = false;
		}
		if (useLookAheadActivityStatus === undefined || useLookAheadActivityStatus === null) {
			useLookAheadActivityStatus = false;
		}
	}

	var visibleMenuItemCount = 0;
	
	this.WriteDetailedLog("Updating display for each menu item", logParent);
	for (var activityId in this.Sequencer.Activities.ActivityList){
		
		if (useLookAheadActivityStatus) {
			var activity = this.LookAheadSequencer.Activities.ActivityList[activityId];
		} else { 
			var activity = this.Sequencer.Activities.ActivityList[activityId];
		}
		
		if (useLookAheadDataNavInfo) {
			var navigationRequestInfo = this.FindPossibleChoiceRequestForActivity(this.LookAheadSequencer.Activities.ActivityList[activityId]);
		} else { 
			var navigationRequestInfo = this.FindPossibleChoiceRequestForActivity(this.Sequencer.Activities.ActivityList[activityId]);
		}
		
		activity.SetHiddenFromChoice(navigationRequestInfo.Hidden);
		
		//There's only one real set of menu items and these are attached to the main sequencer, so always use "this.Sequencer" when
		//using MenuItems
		var menuItem = this.Sequencer.Activities.ActivityList[activityId].MenuItem;
				
		//menu items do not get created for activities that are not selected in the selection and randomization process
		if (menuItem !== null){
			menuItem.UpdateStateDisplay(activity, this.Sequencer.CurrentActivity, navigationRequestInfo, useLookAheadActivityStatus);
			if (menuItem.Visible) {
				visibleMenuItemCount++;
			}
		}
	}
	
	//NOTE: The UpdateControlState should always take the "real" non-lookahead sequencer because we need static data off the real current activity
	this.WriteDetailedLog("Calling Integration Implementation UpdateControl State", logParent);
	IntegrationImplementation.UpdateControlState(IntegrationImplementation.GetDocumentObjectForControls(), this.PossibleNavigationRequests, this.Sequencer.GetCurrentActivity());

	if (visibleMenuItemCount == 0) {
		if (this.MenuIsVisible === true){
			this.ToggleMenuVisibility();
		}
	} else {
		if (this.Package.Properties.CourseStructureStartsOpen === true && this.MenuIsVisible === false){
			this.ToggleMenuVisibility();
		}
	}

	this.WriteDetailedLog("Exiting Control Update Display", logParent);
}


//private 
function Controller_RefreshPage(){
	
	//called to update the AICC data which resides on the server
	//TODO - improvement would be to keep the page loaded and just pull down the updated state data via xmlhttp
	
	//find the top level window for the player...the window with the Control object
	var MAX_PARENTS_TO_SEARCH = 500; 
	var nParentsSearched = 0;
	var win = window;
	
	while ( (win.Control === null) && 
			(win.parent !== null) && (win.parent != win) && 
			(nParentsSearched <= MAX_PARENTS_TO_SEARCH) )
	{ 
				
		nParentsSearched++; 
		win = win.parent;
	} 
	
	if (win.Control === null){
		Debug.AssertError("Could not locate the top level window.");
	}
	else{
		win.location.replace(win.location);
	}
}

function Controller_ScoUnloaded(){

	var logParent = this.WriteAuditLog("Control ScoUnloaded");
	
	//called from the intermediate page when it loads...this signals that the last SCO has completely unloaded and thus finished its processing
	
	//make sure that things are loaded and ready...the intermediate page loads with the initial frameset and might slip in an extra call here on initial load
	if (this.Initialized === false){
		return;
	}

	//check to make sure that the SCO called Finish/Terminate. Note the value for NeedToCloseOutSession is a bit misleading, false means we do need to close out the session
	if (this.Api.Activity !== null && this.Api.NeedToCloseOutSession() === false){
		this.Api.CloseOutSession();
	} 
	
	if ((this.Api.Activity != null) && (this.Api.Activity.LearningObject.ScormType === SCORM_TYPE_ASSET) && this.Api.Activity.WasLaunchedThisSession()) {
		this.Api.AccumulateTotalTimeTracked();
		this.WriteDetailedLog("New Tracked Total Time for Asset: " + this.Api.RunTimeData.TotalTimeTracked);		
	}
	
	//use the user's navigation request if it exists, otherwise, check the runtime for a nav request by the SCO, then try to determine
	//the appropriate rudimentary sequencing request (exit action)
	if (this.PendingNavigationRequest === null){
		
		if ((this.Api !== null) && (this.Api.RunTimeData != null) &&
			(this.Api.RunTimeData.NavRequest != SCORM_RUNTIME_NAV_REQUEST_NONE)){
			
			this.WriteDetailedLog("API Runtime Nav Request Detected = " + this.Api.RunTimeData.NavRequest, logParent);
			
			this.PendingNavigationRequest = this.TranslateRunTimeNavRequest(this.Api.RunTimeData.NavRequest);
		}	
		
	}

	this.Sequencer.NavigationRequest = this.PendingNavigationRequest;

	this.ClearPendingNavigationRequest();

	this.Sequencer.OverallSequencingProcess();
	
	//don't bother with look ahead if we're exiting. it slows things down and introduces funny
	//sequencing situations that lead to errors
	if (this.ExitScormPlayerCalled === false){
		this.EvaluatePossibleNavigationRequests();
	}
}

function Controller_ExitScormPlayer(){
	
	//don't need to worry about unloading the sco here...this function shouldn't get called until that has happened
	
	//TODO: should this be an integration function?
	//TODO: can we always count on the current window being the player window?
	
	this.ExitScormPlayerCalled = true;
	
	if (SHOULD_SAVE_CLIENT_DEBUG_LOGS) {
		this.Comm.SaveDebugLog();
	}
	
	//we don't want to redirect the browser at all if it is already unloading
	if (this.ProcessedUnload === false){
	
		// Either Unload() or ExitScormPlayer() should call SaveDataOnExit().  If Unload hasn't been called, do it here.
		this.Comm.SaveDataOnExit();
	
		if (this.Package.Properties.PlayerLaunchType == LAUNCH_TYPE_FRAMESET){
			window.location = RedirectOnExitUrl;
		}
		else{
			window.opener.location = RedirectOnExitUrl;
			window.close();
		}
	}
}

function Controller_ExitSco(){
	this.ScoLoader.UnloadSco();
}

function Controller_MarkPostedDataDirty(){
	this.WriteAuditLog("Control MarkPostedDataDirty");
	
	for (var activity in this.Activities.ActivityList){
		if (this.Activities.ActivityList[activity].DataState == DATA_STATE_POSTED){
			this.Activities.ActivityList[activity].DataState = DATA_STATE_DIRTY;
		}
		
		this.Activities.ActivityList[activity].MarkPostedObjectiveDataDirty();
	}

	if (this.Sequencer.GlobalObjectives !== null && this.Sequencer.GlobalObjectives !== undefined){
		for (var globalObjective in this.Sequencer.GlobalObjectives){
			
			dataState = this.Sequencer.GlobalObjectives[globalObjective].DataState;
			
			if (dataState == DATA_STATE_POSTED){
				this.Sequencer.GlobalObjectives[globalObjective].DataState = DATA_STATE_DIRTY;
			}			
		}
	}	
}

function Controller_MarkPostedDataClean(){
	this.WriteAuditLog("Control MarkPostedDataClean");
	
	for (var activity in this.Activities.ActivityList){
		if (this.Activities.ActivityList[activity].DataState == DATA_STATE_POSTED){
			this.Activities.ActivityList[activity].DataState = DATA_STATE_CLEAN;
		}
		
		this.Activities.ActivityList[activity].MarkPostedObjectiveDataClean();
	}

	if (this.Sequencer.GlobalObjectives !== null && this.Sequencer.GlobalObjectives !== undefined){
		for (var globalObjective in this.Sequencer.GlobalObjectives){
			
			dataState = this.Sequencer.GlobalObjectives[globalObjective].DataState;
			
			if (dataState == DATA_STATE_POSTED){
				this.Sequencer.GlobalObjectives[globalObjective].DataState = DATA_STATE_CLEAN;
			}			
		}
	}	
}

function Controller_MarkDirtyDataPosted(){
	this.WriteAuditLog("Control MarkDirtyDataPosted");
	
	for (var activity in this.Activities.ActivityList){
		if (this.Activities.ActivityList[activity].IsAnythingDirty()){
			this.Activities.ActivityList[activity].DataState = DATA_STATE_POSTED;
		}
		
		this.Activities.ActivityList[activity].MarkDirtyObjectiveDataPosted();
	}
	
	if (this.Sequencer.GlobalObjectives !== null && this.Sequencer.GlobalObjectives !== undefined){
		for (var globalObjective in this.Sequencer.GlobalObjectives){
			
			dataState = this.Sequencer.GlobalObjectives[globalObjective].DataState;

			if (dataState == DATA_STATE_DIRTY){
				this.Sequencer.GlobalObjectives[globalObjective].DataState = DATA_STATE_POSTED;
			}			
		}
	}	
}

function Controller_GetXmlForDirtyData(){
	
	this.WriteAuditLog("Control GetXmlForDirtyData");
		
	var xml = new XmlElement("RunTimeData");
	
	xml.AddAttribute("RegistrationId", RegistrationToDeliver.Id);
	
	if (this.Sequencer.GetSuspendedActivity() !== null){
		xml.AddAttribute("SuspendedActivityId", this.Sequencer.GetSuspendedActivity().GetDatabaseIdentifier());
	}
	
	for (var activity in this.Activities.ActivityList){
		
		if (this.Activities.ActivityList[activity].IsAnythingDirty() || 
			this.Activities.ActivityList[activity].DataState == DATA_STATE_POSTED){
			
			xml.AddElement(this.Activities.ActivityList[activity].GetXml());
		}
	}
	
	
	if (this.Sequencer.GlobalObjectives !== null && this.Sequencer.GlobalObjectives !== undefined){
		for (var globalObjective in this.Sequencer.GlobalObjectives){
			
			dataState = this.Sequencer.GlobalObjectives[globalObjective].DataState;
			
			//for now we're posting all global objectives
			if (dataState == DATA_STATE_DIRTY || dataState == DATA_STATE_POSTED){
				xml.AddElement(this.Sequencer.GlobalObjectives[globalObjective].GetXml(RegistrationToDeliver.Id, globalObjective));
			}			
		}
	}
	
	return "<?xml version=\"1.0\"?>" + xml.toString();
}

function Controller_IsThereDirtyData(){
	this.WriteAuditLog("Control IsThereDirtyData");
	
	for (var activity in this.Activities.ActivityList){
		if (this.Activities.ActivityList[activity].IsAnythingDirty() || 
			this.Activities.ActivityList[activity].DataState == DATA_STATE_POSTED){
			return true;
		}
	}

	if (this.Sequencer.GlobalObjectives !== null && this.Sequencer.GlobalObjectives !== undefined){
		for (var globalObjective in this.Sequencer.GlobalObjectives){
			
			dataState = this.Sequencer.GlobalObjectives[globalObjective].DataState;
			
			if (dataState == DATA_STATE_DIRTY || dataState == DATA_STATE_POSTED){
				return true;
			}			
		}
	}
	
	return false;
}



function Controller_DisplayError(errorString){
	this.WriteAuditLog("Control DisplayError - " + errorString);
	
	if (Debug.DataIsAvailable()){
		if (confirm(errorString + "\n\nPress 'OK' to display debug information to send to technical support, or press 'Cancel' to exit.")){
			Debug.ShowAllAvailableData();
		}
	}
	else{
		alert(errorString);
	}
	return;
}


function Controller_GetExceptionText(){
	
	this.WriteAuditLog("Control GetExceptionText");
	
	//should return human readable error message from the sequencer.
	//this is called from the intermediate page if it is stuck there
	
	//there are certain timing situations where this method will get invoked before
	//the page has completed loading while the sequencer is still null
	
	if (this.Sequencer != null && this.Sequencer != undefined){
		return this.Sequencer.GetExceptionText();
	}
	else{
		return "";
	}
}


function Controller_CheckForDebugCommand(intKeyCode){
	
	if (intKeyCode == ASCII_SHIFT_IN){		
		Debug.RecordControlAudit = true;
		Debug.RecordControlDetailed = true;
		Debug.RecordRteAudit = true;
		Debug.RecordRteDetailed = true;
		Debug.RecordSequencingAudit = true;
		Debug.RecordSequencingDetailed = true;
		Debug.RecordLookAheadAudit = true;
		Debug.RecordLookAheadDetailed = true;
		alert("Debugger set to Level 8 [Maximum]");
		
	} else if (this.QuestionCounter == 4) {
		if (intKeyCode == ASCII_QUESTION){		
			Debug.ShowAllAvailableData();
		} else {
		
			var debugLevel = -1;
		
			if (intKeyCode == ASCII_0){		
				debugLevel = 0;
			} else if (intKeyCode == ASCII_1){		
				debugLevel = 1;
			} else if (intKeyCode == ASCII_2){		
				debugLevel = 2;
			} else if (intKeyCode == ASCII_3){	
				debugLevel = 3;
			} else if (intKeyCode == ASCII_4){		
				debugLevel = 4;
			} else if (intKeyCode == ASCII_5){	
				debugLevel = 5;	
			} else if (intKeyCode == ASCII_6){		
				debugLevel = 6;	
			} else if (intKeyCode == ASCII_7){		
				debugLevel = 7;	
			} else if (intKeyCode == ASCII_8){		
				debugLevel = 8;	
			} 
			
			Debug.RecordControlAudit = debugLevel > 0;
			Debug.RecordControlDetailed = debugLevel > 1;
			Debug.RecordRteAudit = debugLevel > 2;
			Debug.RecordRteDetailed = debugLevel > 3;
			Debug.RecordSequencingAudit = debugLevel > 4;
			Debug.RecordSequencingDetailed = debugLevel > 5;
			Debug.RecordLookAheadAudit = debugLevel > 6;
			Debug.RecordLookAheadDetailed = debugLevel > 7;
			
			alert("Debugger set to Level " + debugLevel);
		}
		
		this.QuestionCounter = 0;
	}
	else if (intKeyCode == ASCII_QUESTION){
		this.QuestionCounter++;		
	}	
	else if (intKeyCode == ASCII_TILDA){
		this.TildaCounter++;
		if (this.TildaCounter == 5){
			this.TildaCounter = 0;
			Debug.External.Invoke();
		}		
	}	
	else if (intKeyCode !== 0){		//in FireFox, the shift key comes through as a keypress with code of 0...we want to ignore this
		this.QuestionCounter = 0;
		this.TildaCounter = 0;
	}
	
}

function Controller_CloseSco(){

	this.WriteAuditLog("Control Recieved Close Sco From GUI");
	
	if (this.PendingNavigationRequest === null){
	
		if (this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_EXIT].WillSucceed === true){
			this.PendingNavigationRequest = new NavigationRequest(NAVIGATION_REQUEST_EXIT, null, "");
		}
		else if(this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_ABANDON] === true){
			this.PendingNavigationRequest = new NavigationRequest(NAVIGATION_REQUEST_ABANDON, null, "");
		}
		
	}
	
	//TODO: in scorm 2004 and 1.2, we need to provide a message to the user
	
	this.ScoLoader.UnloadSco();
}

function Controller_ReturnToLms(){

	this.WriteAuditLog("Control Recieved Return To Lms From GUI");
	
	this.Sequencer.ReturnToLmsInvoked = true;
	
	if (this.PendingNavigationRequest === null){
		this.PendingNavigationRequest = this.GetReturnToLmsNavigationRequest();
	}

	this.ScoLoader.UnloadSco();
}

function Controller_GetReturnToLmsNavigationRequest(){

	//The whole point of a suspend all is to get the user back to where he last left off. If we have a single sco,
	//then this isn't necessary. It is probably better in this case to process an Exit All because that will call End Attempt
	//and transfer the runtime data to the activity tree for rollup.
	
	var hideSuspendAll = this.Sequencer.CurrentActivity.LearningObject.SequencingData.HideSuspendAll;
	var hideAbandonAll = this.Sequencer.CurrentActivity.LearningObject.SequencingData.HideAbandonAll;
	var hideExitAll = this.Sequencer.CurrentActivity.LearningObject.SequencingData.HideExitAll;	
	
	if (this.Activities.GetNumDeliverableActivities() == 1){
		if (hideExitAll === false && this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_EXIT_ALL].WillSucceed === true){
			return new NavigationRequest(NAVIGATION_REQUEST_EXIT_ALL, null, "");
		}
		else if (hideSuspendAll === false && this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_SUSPEND_ALL].WillSucceed === true){
			return new NavigationRequest(NAVIGATION_REQUEST_SUSPEND_ALL, null, "");
		}
	}
	else{
		//TODO: Need a modified version of SUSPEND_ALL with rollup 
	
		if (hideSuspendAll === false && this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_SUSPEND_ALL].WillSucceed === true){
			return new NavigationRequest(NAVIGATION_REQUEST_SUSPEND_ALL, null, "");
		}
		else if (hideExitAll === false && this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_EXIT_ALL].WillSucceed === true){
			return new NavigationRequest(NAVIGATION_REQUEST_EXIT_ALL, null, "");
		}
	}

	//if neither suspend nor exit will succeed, try to abandon or just simply exit
	if (hideAbandonAll === false && this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_ABANDON_ALL].WillSucceed === true){
		return new NavigationRequest(NAVIGATION_REQUEST_ABANDON_ALL, null, "");
	}
	else {
		return new NavigationRequest(NAVIGATION_REQUEST_EXIT_PLAYER, null, "");
	}
}

function Controller_ToggleMenuVisibility(){
	this.WriteAuditLog("Control ToggleMenuVisibility");
	
	if (this.MenuIsVisible === true){
		IntegrationImplementation.HideMenu();
		this.MenuIsVisible = false;
	}
	else{
		IntegrationImplementation.ShowMenu(this.Package.Properties.CourseStructureWidth);
		this.MenuIsVisible = true;
	}
}

function Controller_Next(){
	this.WriteAuditLog("Control Recieved Next Request From GUI");
	if (this.PendingNavigationRequest === null){
		this.PendingNavigationRequest = new NavigationRequest(NAVIGATION_REQUEST_CONTINUE, null);
	}
	
	this.ScoLoader.UnloadSco();
}

function Controller_Previous(){
	this.WriteAuditLog("Control Recieved Previous Request From GUI");
	
	if (this.PendingNavigationRequest === null){
		this.PendingNavigationRequest = new NavigationRequest(NAVIGATION_REQUEST_PREVIOUS, null, "");
	}
	
	this.ScoLoader.UnloadSco();
}
function Controller_Abandon(){
	this.WriteAuditLog("Control Recieved Abandon Request From GUI");
	
	if (this.PendingNavigationRequest === null){
		this.PendingNavigationRequest = new NavigationRequest(NAVIGATION_REQUEST_ABANDON, null, "");
	}
	
	this.ScoLoader.UnloadSco();
}
function Controller_AbandonAll(){
	this.WriteAuditLog("Control Recieved Abandon All Request From GUI");
	
	if (this.PendingNavigationRequest === null){
		this.PendingNavigationRequest = new NavigationRequest(NAVIGATION_REQUEST_ABANDON_ALL, null, "");
	}
	
	this.ScoLoader.UnloadSco();
}
function Controller_Suspend(){
	this.WriteAuditLog("Control Recieved Suspend Request From GUI");
	
	if (this.PendingNavigationRequest === null){
		this.PendingNavigationRequest = new NavigationRequest(NAVIGATION_REQUEST_SUSPEND_ALL, null, "");
	}
	
	this.ScoLoader.UnloadSco();
}
function Controller_Exit(){
	this.WriteAuditLog("Control Recieved Exit Request From GUI");
	
	if (this.PendingNavigationRequest === null){
		this.PendingNavigationRequest = new NavigationRequest(NAVIGATION_REQUEST_EXIT, null, "");
	}
	
	//TODO: in scorm 2004 and 1.2, we need to provide a message to the user
	
	this.ScoLoader.UnloadSco();
}
function Controller_ExitAll(){
	this.WriteAuditLog("Control Recieved Exit All Request From GUI");
	if (this.PendingNavigationRequest === null){
		this.PendingNavigationRequest = new NavigationRequest(NAVIGATION_REQUEST_EXIT_ALL, null, "");
	}
	
	this.ScoLoader.UnloadSco();
}
function Controller_ChoiceRequest(activityItemIdentifier){

	this.WriteAuditLog("Control Recieved Choice Request For '" + activityItemIdentifier + "' From GUI");
	
	if (this.PendingNavigationRequest === null){
		this.PendingNavigationRequest = new NavigationRequest(NAVIGATION_REQUEST_CHOICE, activityItemIdentifier, "");
	}
	
	this.ScoLoader.UnloadSco();
}


function Controller_ScoHasTerminatedSoUnload(){
	
	var logParent = this.WriteAuditLog("In ScoHasTerminatedSoUnload");
	
	this.ScoLoader.UnloadSco();
	
}

function Controller_SignalTerminated(){

	if (this.ProcessedUnload === true && this.ExitScormPlayerCalled === false){
		
		
		//TODO: it appears that SignalTerminated is only called from Terminate. Since Terminage calls CloseOutSession, this call
		//might be redundant. Make sure before deleting it.
		
		//if the browser unload event has been fired and the window is closing because the user closed the browser window 
		//(and not as a result of a sequencing action which would be indicated by ExitScormPlayerCalled == true)
		//then we need to transfer the runtime data to the activity tree and rollup the data before saving and closing
		//this will be handled by running the OverallSequencingProcess
		
		if (this.Api.Activity !== null && this.Api.NeedToCloseOutSession() === false){
			this.Api.CloseOutSession();
		}

		this.Sequencer.NavigationRequest = this.GetReturnToLmsNavigationRequest();			
		this.Sequencer.OverallSequencingProcess();
		
		this.Comm.SaveDataOnExit();	
	}
	else{
		if (this.Package.Properties.FinishCausesImmediateCommit === true){
			this.Comm.SaveDataNow(false);
		}
	}	
}


function Controller_TranslateRunTimeNavRequest(adlNavRequest){
	//accepts an adl.nav.request from the RTE and translates it into an internal NavigationRequest object
	
	if (adlNavRequest.substring(0, 1) == "{"){
				
		var targetIdentifier = this.ParseTargetStringFromChoiceRequest(adlNavRequest);
		
		return new NavigationRequest(NAVIGATION_REQUEST_CHOICE, targetIdentifier, "");
	}
	
	switch (adlNavRequest){
		case SCORM_RUNTIME_NAV_REQUEST_CONTINUE :
			return new NavigationRequest(NAVIGATION_REQUEST_CONTINUE, null, "");
		//break;
		
		case SCORM_RUNTIME_NAV_REQUEST_PREVIOUS:
			return new NavigationRequest(NAVIGATION_REQUEST_PREVIOUS, null, "");
		//break;
		
		case SCORM_RUNTIME_NAV_REQUEST_EXIT:
			return new NavigationRequest(NAVIGATION_REQUEST_EXIT, null, "");
		//break;
		
		case SCORM_RUNTIME_NAV_REQUEST_EXITALL:
			return new NavigationRequest(NAVIGATION_REQUEST_EXIT_ALL, null, "");
		//break;
		
		case SCORM_RUNTIME_NAV_REQUEST_ABANDON:
			return new NavigationRequest(NAVIGATION_REQUEST_ABANDON, null, "");
		//break;
		
		case SCORM_RUNTIME_NAV_REQUEST_ABANDONALL:
			return new NavigationRequest(NAVIGATION_REQUEST_ABANDON_ALL, null, "");
		//break;

		case SCORM_RUNTIME_NAV_REQUEST_SUSPENDALL:
			return new NavigationRequest(NAVIGATION_REQUEST_SUSPEND_ALL, null, "");
		//break;
		
		case SCORM_RUNTIME_NAV_REQUEST_NONE:
			return null;
		//break;	
		
		default:
			Debug.AssertError("Unrecognized runtime navigation request");
		break;	
	}
}

function Controller_GetMessageText(){
	
	var msg = "";
	
	try {
		if (this.Sequencer.NavigationRequest !== null && this.Sequencer.NavigationRequest !== undefined){
			if (this.Sequencer.NavigationRequest.MessageToUser !== null && this.Sequencer.NavigationRequest.MessageToUser !== undefined){
				msg = this.Sequencer.NavigationRequest.MessageToUser;
			}
		}
	} catch(e) {
		// Return "" by default, timing errors have caused problems here despite our checks for null and undefined
	}

	return msg;
}

function Controller_ClearPendingNavigationRequest(){
	this.WriteAuditLog("Control ClearPendingNavigationRequest");
	
	this.PendingNavigationRequest = null;
}

function Controller_IsThereAPendingNavigationRequest(){
	return (this.PendingNavigationRequest !== null);
}

function Controller_DeliverActivity(activity) {

	var logParent = this.WriteAuditLog("Control DeliverActivity - " + activity);
	
	if (activity.IsDeliverable() === false){
		Debug.AssertError("ERROR - Asked to deliver a non-leaf activity - " + activity);
	}
	
	// If launch after click, the PopupLauncher will make these two calls once the course is actually viewed, 
	// otherwise make the calls here as usual
	if (Control.Package.Properties.ScoLaunchType !== LAUNCH_TYPE_POPUP_AFTER_CLICK) {
		this.Api.ResetState();
		this.Api.InitializeForDelivery(activity);
	}
	
	this.ScoLoader.LoadSco(activity);
	
	this.UpdateDisplay(false, false);
	
	this.WriteDetailedLog("Exiting Control Deliver Activity", logParent);
}

// For use with "Launch after click" -- called within popup launcher
function Controller_PerformDelayedDeliveryInitialization(activity) {

	this.Sequencer.ContentDeliveryEnvironmentActivityDataSubProcess(activity);
			
	this.Api.ResetState();
	this.Api.InitializeForDelivery(activity);
	this.EvaluatePossibleNavigationRequests();
}

function Controller_InitializePossibleNavigationRequests(){

	// If lookahead is disabled, we must automatically expose the navigation elements
	if (Control.Package.Properties.LookaheadSequencerMode === LOOKAHEAD_SEQUENCER_MODE_DISABLE) {
		var defaultNavRequestResult = true;
	} else {
		var defaultNavRequestResult = RESULT_UNKNOWN;
	}

	this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_START]		= new PossibleRequest(NAVIGATION_REQUEST_START, null, defaultNavRequestResult, "", "");
	this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_RESUME_ALL]	= new PossibleRequest(NAVIGATION_REQUEST_RESUME_ALL, null, defaultNavRequestResult, "", "");
	this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_CONTINUE]		= new PossibleRequest(NAVIGATION_REQUEST_CONTINUE, null, defaultNavRequestResult, "", "");
	this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_PREVIOUS]		= new PossibleRequest(NAVIGATION_REQUEST_PREVIOUS, null, defaultNavRequestResult, "", "");
	this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_EXIT]			= new PossibleRequest(NAVIGATION_REQUEST_EXIT, null, defaultNavRequestResult, "", "");
	this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_EXIT_ALL]		= new PossibleRequest(NAVIGATION_REQUEST_EXIT_ALL, null, defaultNavRequestResult, "", "");
	this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_SUSPEND_ALL]	= new PossibleRequest(NAVIGATION_REQUEST_SUSPEND_ALL, null, defaultNavRequestResult, "", "");
	this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_ABANDON]		= new PossibleRequest(NAVIGATION_REQUEST_ABANDON, null, defaultNavRequestResult, "", "");
	this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_ABANDON_ALL]	= new PossibleRequest(NAVIGATION_REQUEST_ABANDON_ALL, null, defaultNavRequestResult, "", "");
	
	var id;
	var arrayIndex = POSSIBLE_NAVIGATION_REQUEST_INDEX_CHOICE;
	
	for(var identifier in this.Activities.SortedActivityList){

		activity = this.Activities.SortedActivityList[identifier];
		
		itemId = activity.GetItemIdentifier();
		
		this.PossibleNavigationRequests[arrayIndex] = new PossibleRequest(NAVIGATION_REQUEST_CHOICE, itemId, defaultNavRequestResult, "", "");
		
		arrayIndex++;
	}
	
	this.Sequencer.InitializePossibleNavigationRequestAbsolutes(this.PossibleNavigationRequests, this.Activities.ActivityTree, this.Activities.SortedActivityList);
	

	
}

// This is the method that actually runs the Lookahead Sequencer
function Controller_EvaluatePossibleNavigationRequests(isActivityStillLoaded){
	//copy regular sequencer's clean activity state to the look ahead sequencer's activity state

	if (this.Package.Properties.LookaheadSequencerMode === LOOKAHEAD_SEQUENCER_MODE_ENABLE) {

		var logParent = this.WriteAuditLog("Control Evaluate Possible Navigation Requests");
		
		this.WriteDetailedLog("Tearing down sequencer", logParent);
		this.TearDownSequencer(this.LookAheadSequencer);
		
		this.WriteDetailedLog("Cloning sequencer", logParent);
		this.LookAheadSequencer = this.CloneSequencer(this.Sequencer);
		this.LookAheadSequencer.LookAhead = true;
		
		this.WriteDetailedLog("Setting sequencer pointer for cloned activities", logParent);
		this.LookAheadSequencer.Activities.SetSequencer(this.LookAheadSequencer, true);
		
		this.WriteDetailedLog("Performing sequencing look ahead evaluation", logParent);
		
		this.PossibleNavigationRequests = this.LookAheadSequencer.EvaluatePossibleNavigationRequests(this.PossibleNavigationRequests);
		
		this.WriteDetailedLog("Done Evaluating Possible Navigation Requests", logParent);
		
		if (isActivityStillLoaded !== undefined && isActivityStillLoaded !== null && isActivityStillLoaded == true) {
			this.UpdateDisplay(true, true);  // use lookahead nav info =  true, use lookahead activity data = true
		} else {
			this.UpdateDisplay(true, false);
		}
	} else {
	
		var logParent = this.WriteAuditLog("Bypassing Lookahead Sequencer processing because PackageProperties.LookaheadSequencerMode = disabled");
	}
}


function Controller_FindPossibleChoiceRequestForActivity(activity){
	
	var targetItemIdentifier = activity.GetItemIdentifier();
	
	var arrayIndex = POSSIBLE_NAVIGATION_REQUEST_INDEX_CHOICE;
	
	for(var i=arrayIndex; i < this.PossibleNavigationRequests.length; i++ ){
	
		if (this.PossibleNavigationRequests[i].TargetActivityItemIdentifier == targetItemIdentifier){
			return this.PossibleNavigationRequests[i];
		}
	}
	
	Debug.AssertError("Could not locate possible choice request for activity-" + activity);
	
	return null;	
}


function Controller_IsTargetValid(targetString){
		
	var targetActivity = this.ParseTargetStringIntoActivity(targetString);
	
	if (targetActivity === null){
		return false;
	}
	else{
		return true;
	}
}

function Controller_IsChoiceRequestValid(target){
	
	var targetActivity = this.ParseTargetStringIntoActivity(target);
	var targetItemIdentifier = targetActivity.GetItemIdentifier();
	
	for (var i=POSSIBLE_NAVIGATION_REQUEST_INDEX_CHOICE; i < this.PossibleNavigationRequests.length; i++){
		
		if (this.PossibleNavigationRequests[i].TargetActivityItemIdentifier == targetItemIdentifier){
			return this.PossibleNavigationRequests[i].WillSucceed;
		}
	}
	
	return false;
}

function Controller_IsContinueRequestValid(){
	return this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_CONTINUE].WillSucceed;
}

function Controller_IsPreviousRequestValid(){
	return this.PossibleNavigationRequests[POSSIBLE_NAVIGATION_REQUEST_INDEX_PREVIOUS].WillSucceed;
}

function Controller_ParseTargetStringIntoActivity(value){
	
	var itemIdentifier = this.ParseTargetStringFromChoiceRequest(value);
		
	var targetActivity = this.Activities.GetActivityFromIdentifier(itemIdentifier);
	
	return targetActivity;
}

function Controller_ParseTargetStringFromChoiceRequest(value){
	//TODO - test this....make more robust...check for case indexOf("=") < 0
	return value.substring(value.indexOf("=") + 1, value.indexOf("}"));
}


function Controller_CloneSequencer(sequencerToClone, resultIsLookAhead){

	var sequencerReturn = new Sequencer(resultIsLookAhead, sequencerToClone.Activities.Clone());
	
	if (sequencerToClone.SuspendedActivity === null){
		sequencerReturn.SuspendedActivity = null;
	}
	else{
		sequencerReturn.SuspendedActivity = sequencerReturn.Activities.GetActivityFromIdentifier(sequencerToClone.SuspendedActivity.GetItemIdentifier());
	}
	
	if (sequencerToClone.CurrentActivity === null){
		sequencerReturn.CurrentActivity = null;
	}
	else{
		sequencerReturn.CurrentActivity = sequencerReturn.Activities.GetActivityFromIdentifier(sequencerToClone.CurrentActivity.GetItemIdentifier());
	}
	
	//this.GlobalObjectives = new Array();	
	sequencerReturn.GlobalObjectives = new Array();
	
	for (var objId in sequencerToClone.GlobalObjectives){
		sequencerReturn.GlobalObjectives[sequencerReturn.GlobalObjectives.length] = sequencerToClone.GlobalObjectives[objId].Clone();
	}

	//TODO - find a cleaner way to copy properties that are specific to one sequencer or another
	if (sequencerToClone.AtEndOfCourse !== undefined){
		sequencerReturn.AtEndOfCourse = sequencerToClone.AtEndOfCourse;
		sequencerReturn.AtStartOfCourse = sequencerToClone.AtStartOfCourse;
	}

	sequencerReturn.NavigationRequest = null;
	sequencerReturn.ChoiceTargetIdentifier = null;

	sequencerReturn.Exception = null;
	sequencerReturn.ExceptionText = null;
	
	return sequencerReturn;
}

function Controller_TearDownSequencer(sequencer){
	
	sequencer.LookAhead = null;
	
	if (sequencer.Activities !== null){
		sequencer.Activities.TearDown();
	}
	sequencer.Activities = null;
	
	sequencer.NavigationRequest = null;
	sequencer.ChoiceTargetIdentifier = null;
	
	sequencer.SuspendedActivity = null;
	sequencer.CurrentActivity = null;
	
	sequencer.Exception = null;
	sequencer.ExceptionText = null;
	
	sequencer.GlobalObjectives = null;
}

function Controller_WriteAuditLog(str){
	return Debug.WriteControlAudit(str);
}

function Controller_WriteDetailedLog(str, parent){
	Debug.WriteControlDetailed(str, parent);
}

function Controller_WriteDetailedLogError(str, parent){
	Debug.WriteControlDetailed(str, parent, true);
}


// Given a relative page href and the full href to the current delivery page,
// this function will construct a full href relative to the delivery page.
function BuildFullUrl(relativeHref, deliveryPageUrl) {
	
	if (deliveryPageUrl.indexOf("?") > -1) {
		var deliveryPagePathWithoutQueryString = deliveryPageUrl.substr(0, deliveryPageUrl.indexOf("?"));
	} else {
		var deliveryPagePathWithoutQueryString = deliveryPageUrl
	}

	var baseHref = deliveryPageUrl.substr(0, deliveryPagePathWithoutQueryString.lastIndexOf("/"));
	
	// The relative href might contain ../ to move up a directory.  If so, we must adjust
	// the baseHref appropriately
	while (relativeHref.indexOf("../") > -1) {
		relativeHref = relativeHref.substr(3, relativeHref.length);
		baseHref = baseHref.substr(0, baseHref.lastIndexOf("/"));
	}
	
	return baseHref + "/" + relativeHref;
}
