// --------------------------------------- Self-Orienting Course
//
// Written by Brian Davies
// October 2001 - November 2002
// Copyright 2001-4, Cognitive Arts
//
// --------------

// --------------------------------------- Documentation

/*

// -------------- Overview

The self-orienting course is a module for handling LMS communication.
Cognitive Arts courses are self-orienting, meaning they are designed to
identify their environment and save user state appropriately.

The framework consists of an outer frameset, which has a visible frame
containing the course content and a hidden frame containing LMS functions.
The course content uses a small set of public methods to store and retrieve
data in the outer frameset.  The outer frameset uses a small set of public
methods to tell the LMS frame to open a connection, get and set data for a
particular user, and close the connection.

Note that this framework is not dependent on any specific LMS technology.
The chief advantage of the self-orienting course is that, by loading
different files that implement the same API into the LMS frame, the same
course can work with an AICC LMS, a SCORM LMS, or as a standalone application
with no modification.

When a course is launched, the outer frameset looks for a SCORM API object
in the launching LMS window.  If it does not find one, it looks for the two
URL parameters that indicate an AICC launch, aicc_url and aicc_sid.  If it
does not find those either, it will use a browser cookie to store user state.
Once the outer page has identified the appropriate mode, it dynamically writes
the frameset to use the appropriate LMS frame page.

This technology allows for easy transitions between different types of LMSs,
and allows courses designed for use with an LMS to be run independently.  It
also provides the ability to create plug-in modules, so that courses can be
modified easily to use unusual or emerging standards.

// -------------- Constants

This file requires several constants that define the course.

ENABLE_OFFLINE_SWITCHING
A flag that says whether the feature is enabled or not.

COURSE_ID
A text string that is used as the name of the user state cookie, to allow
users to enroll in multiple courses that use this code without clobbering
their state.

START_PATH
The relative pathname from the course folder to the default first page
of course content.  If the first page has a function displayStateMessage,
then that function will be called as soon as the course receives data
from the LMS with information about the user, so that the course can
display appropriate error messages, or have different behaviors for new
and returning users.

// -------------- Public Functions

	setVariable (name, value, convert)
	getVariable (name)
	clearVariable (name)
	variableDefined (name)

These functions allow pages to add, retrieve, delete, and test items in the
user state string, which is passed between the LMS, on-line version, and
off-line version.

Note that setVariable has a convert flag argument.  Saving an absolute URL
using setVariable will break if the course is moved.  If the flag is true,
the absolute URL will be converted to a URL relative to the course folder.

Virtually everything that comes in from the LMS is turned into an item in the
state string, plus several item names are used by this module, so be careful 
when naming new items.  Item names that are known to be reserved are:

* "lesson_location", "lesson_status", "score", "time" - AICC core data
* "starttime" - time tracking

If the course is using offline switching, these items are also reserved:

* "username", "lmsurl", "localurl", "serverurl", "offline", "desynched"

This code expects you to set lesson_status, lesson_location and score
manually.  When setting "lesson_location", it is important to set it to
document.location.toString (), as document.location will shift during code
processing, and to use the "convert" flag.

	setSessionVariable (name, value, convert)
	getSessionVariable (name)
	clearSessionVariable (name)
	sessionVariableDefined (name)

These functions behave as above, but define variables that will be discarded
when the course is closed, rather than stored to the LMS.
	
	sendStateToLMS ()

Automatically called on window close, departing from the online version, and
arriving back at the online version.  It can be called to force a send at any
time.

	start ()

A function that client code would call from the splash page.

	activatedebugtoggle ()

Pages can call this function to activate the alt-shift-D key command for
turning on debugging mode.



In progress is:
	addinteraction ();
*/

// --------------------------------------- Dependencies

//if (! window.COURSE_ID || ! window.START_PATH)
//add DEFAULT_ROW_LAYOUT, DEBUG_ROW_LAYOUT?
//	reportError ('This code requires that you specify course variables before loading soc.js.');

if (! window.UTILITIES_VERSION || (UTILITIES_VERSION < 1.9))
	reportError ('This code depends on the functions defined in utilities.js version 1.9.');

// --------------------------------------- Global Variables

var userstate = null;

var loadinprogress = true;
var switchinprogress = false;

var aicc_url = "course";
var aicc_sid = "student";

var base_url = "";

// --------------------------------------- Setting The Mode

function selforient ()
{
	var status = getParameter (unescape (location.search), "status");

	if (status == "arriveAtLocal")
	{
		return "lms-none-cookie.htm";
	}
	else if (status == "arriveAtServer")
	{
		// once switching works with SCORM, this will need to be smarter or disappear
		return decideAICCpage ();
	}
	else if (getAPI2004 ())
	{
		return "lms-scorm-2004.htm";
	}
	else if (getAPI ())
	{
		return "lms-scorm.htm";
	}
	else if (getURLParameter ("aicc_url"))
	{
		return decideAICCpage ();
	}
	else if (getURLParameter ("perl_url"))
	{
		return "lms-mock-perl.htm";
	}
	else
	{
		return "lms-none-cookie.htm";
	}
}

function decideAICCpage ()
{
	if ((MAC && IE5) || (MAC && SA) || (PC && IE5) || (PC && IE55) || (PC && IE6) || (PC && NN6))
		return "lms-aicc-script.htm";
	else if ((PC && NN4) || (PC && IE4))
		return "lms-aicc-applet.htm";
	else
		reportError ("This framework does not support AICC connections using your current browser.");
}

// --------------------------------------- Starting The Course

function loadStartPage ()
{
	var status = getParameter (unescape (location.search), "status");
	
	if (status == "arriveAtLocal")
	{
		arriveAtLocal (unescape (getStateTokenFromURL (location)));
	}
	else if (status == "arriveAtServer")
	{
		arriveAtServer (unescape (getStateTokenFromURL (location)));
	}
	else if (getAPI2004 ())
	{
		lmsframe.initialize (null, null);
		lmsframe.getuserstate ();
	}
	else if (getAPI ())
	{
		lmsframe.initialize (null, null);
		lmsframe.getuserstate ();
	}
	else if (getURLParameter ("aicc_url"))
	{
		convertParametersToVariables (document.location.href);
		
		lmsframe.initialize (aicc_url, aicc_sid);
		lmsframe.getuserstate ();
	}
	else if (getURLParameter ("perl_url") && getURLParameter ("perl_sid"))
	{
		lmsframe.initialize (getURLParameter ("perl_url"), getURLParameter ("perl_sid"));
		lmsframe.getuserstate ();
	}
	else  //orphan
	{
		lmsframe.initialize (COURSE_ID, aicc_sid);
		lmsframe.getuserstate ();
	}
}

function getStateTokenFromURL (url)
{
	// unfortunate but necessary
	// going local automatically escapes your URL params
	// if you unescape, your state gets subdivided by getURLParameter
	// if you don't unescape, your url is unreadable by getURLParameter
	
	return url.search.substring (url.search.indexOf ("state=") + 6)
}

// AICC URL Parsing
// This could use a good scouring

var recognizedAICCparameters = new Array ("aicc_sid", "aicc_url");

function convertParametersToVariables (uri)
{
	var uriParameterString = unescape (uri.substr (uri.indexOf ("?") + 1));
	var parameters = uriParameterString.split ('&');

	var parsingAICCURL = false;

	for (var i = 0; i < parameters.length; i++)
	{
		var name = parameters [i].substr (0, parameters [i].indexOf ('=')).toLowerCase ();
		var value = parameters [i].substr (parameters [i].indexOf ('=') + 1, parameters [i].length);

		if ((parsingAICCURL == true) && (! recognizedAICCparameterP (name)))
			aicc_url += ("&" + name + "=" + value);
		else
		{
			window [name] = unescape (value);
			parsingAICCURL = (name == "aicc_url");
		}
	}
}

function recognizedAICCparameterP (param)
{
	for (var i = 0; i < recognizedAICCparameters.length; i++)
		if (param == recognizedAICCparameters [i])
			return true;

	return false;
}

// --------------------------------------- LMS Frame Callbacks

function initializecomplete (result)
{
	if (lmsframe.getlasterror () > 0)
	{
		var message = "There was a problem initializing your session with the LMS:\n\n"
			+ lmsframe.getlasterror () + " - " + lmsframe.geterrorstring ();
		
		reportdebug (message);
		
		if (ALERT_USER_TO_LMS_ERRORS)
			alert (message);
	}
}

function getuserstatecomplete (result)
{
	userstate = result;
	
	var searchstring = document.location.search.toString ();
	var currenturl = document.location.href.toString ();
	
	var baseurl = currenturl.substring (0, currenturl.lastIndexOf (searchstring));
	baseurl = baseurl.substring (0, baseurl.lastIndexOf ("/") + 1);
	
	if (lmsframe.getlasterror () > 0)
	{
		var message = "There was a problem retrieving your state from the LMS:\n\n"
			+ lmsframe.getlasterror () + " - " + lmsframe.geterrorstring ();
		
		reportdebug (message);
		
		if (ALERT_USER_TO_LMS_ERRORS)
			alert (message);
	}
	else if (ENABLE_OFFLINE_SWITCHING)
	{
		setVariable ("lmsurl", aicc_url, false);
		setVariable ("username", aicc_sid);
		
		if (localP ())
			setVariable ("localurl", unescape (baseurl), false);
		else if (serverP ())
			setVariable ("serverurl", unescape (baseurl), false); // BMD this unescape has me a wee bit nervous
	}
	else
		base_url = unescape (baseurl);
	
	setVariable ("starttime", new Date () * 1);
	
	loadinprogress = false;
	
	splash ();
}

function setuserstatecomplete (result)
{
	if (lmsframe.getlasterror () > 0)
	{
		var message = "There was a problem saving your state to the LMS:\n\n"
			+ lmsframe.getlasterror () + " - " + lmsframe.geterrorstring ();
		
		reportdebug (message);
		
		if (ALERT_USER_TO_LMS_ERRORS)
			alert (message);
	}
}

function finishcomplete (result)
{
	if (lmsframe.getlasterror () > 0)
	{
		var message = "There was a problem closing your session with the LMS:\n\n"
			+ lmsframe.getlasterror () + " - " + lmsframe.geterrorstring ();
		
		reportdebug (message);
		
		if (ALERT_USER_TO_LMS_ERRORS)
			alert (message);
	}
}

// Starting Behaviors

function splash ()
{
	var customstartpage = getURLParameter ("page");
	
	if (customstartpage)
	{
		gotopage (customstartpage);
	}
	else if (courseframe.displayStateMessage)
	{
		var error = (lmsframe.getlasterror ());
		var errorstring = (lmsframe.geterrorstring (error));
		var newuser = (variablehasnovalue ("lesson_location"));
		var switchingenabled = variableDefined ("offline");
		var online = serverP ();
		
		var shouldbeonline = ! variableDefined ("offline")
			|| (getVariable ("offline") == false);
		
		var desynched = (online && switchingenabled && ! shouldbeonline)
			|| (! online && switchingenabled && shouldbeonline);
	
		courseframe.displayStateMessage (error, errorstring, newuser, switchingenabled, online, desynched);
	}
	else
		start ();
}

function start ()
{
	if (variablehasnovalue ("lesson_location"))
		setVariable ("lesson_location", START_PATH);  /* LOCALIZE */

	gotopage (unescape (baseURL ()) + getVariable ("lesson_location"));  /* LOCALIZE */
}

function gotopage (page)
{
	try
	{
		if (courseframe.location.href != page)
			courseframe.location.href = page;
	}
	catch (err)
	{
		alert ("Could not go to page: " + page + ".  Probably file is missing.");
	}
}


function variablehasnovalue (variable)
{
	return ! variableDefined (variable)
		|| (getVariable (variable) == "null")  // Saba has null lesson_location
		|| (getVariable (variable) == "")
		|| (getVariable (variable) == "0");  // Thinq has lesson_location 0
}

// --------------------------------------- Stopping The Course

function unloadStartPage ()
{
	if (switchinprogress == false)
	{
		sendStateToLMS ();  // pull this if sending ExitAU is important
		lmsframe.finish ();
	}
}

function sendStateToLMS ()
{
	setElapsedTime ();
	lmsframe.setuserstate (userstate, interactions);
	
	clearinteractions ();
}

function setElapsedTime ()
{
	if (variableDefined ("starttime"))
	{
		var totalSeconds = Math.floor ((new Date () - getVariable ("starttime")) / 1000);

		var hrs = (Math.floor (totalSeconds / 3600)).toString ();
		var min = (Math.floor ((totalSeconds % 3600) / 60)).toString ();
		var sec = (totalSeconds % 60).toString ();

		if (hrs.length == 1) hrs = "0" + hrs;
		if (min.length == 1) min = "0" + min;
		if (sec.length == 1) sec = "0" + sec;

		setVariable ("time", hrs + ":" + min + ":" + sec);
	}
}

// --------------------------------------- User State

// State Accessors

function setVariable (name, value, convert)
{
	if ((convert == null) && (typeof value == "string") && (value.indexOf ("http://") == 0))
		alert ("Dev fascists say: calling setVariable with a URL value without explicitly setting 'convert' is wrong. "
		+ "\n\nName: " + name + ", Value: " + value);
	
	if (convert && (baseURL () != null))  /* LOCALIZE */
	{
		value = unescape (value);
		
		var startindex = value.indexOf (baseURL ()) + baseURL ().length;
		var endindex = value.length;
		
		value = value.substring (startindex, endindex);
	}
	
	userstate = setTokenListValue (userstate, name, value);
}

function getVariable (name)
{
	
	if (findTokenListToken (userstate, name))
		return getTokenListValue (userstate, name);
	else
		reportError ("Variable '" + name + "' not found.");
}

function clearVariable (name)
{
	if (findTokenListToken (userstate, name))
		userstate = clearTokenListValue (userstate, name);
	else
		reportError ("Variable '" + name + "' not found.");
}

function variableDefined (name)
{
	if (findTokenListToken (userstate, name))
		return true;
	else
		return false;
}

// Session Accessors

var tempstate = null;

function setSessionVariable (name, value, convert)
{
	if ((convert == null) && (typeof value == "string") && (value.indexOf ("http://") == 0))
		alert ("Dev fascists say: calling setSessionVariable with a URL value without explicitly setting 'convert' is wrong. "
		+ "\n\nName: " + name + ", Value: " + value);
	
	if (convert && (baseURL () != null))  /* LOCALIZE */
	{
		value = unescape (value);
		
		var startindex = value.indexOf (baseURL ()) + baseURL ().length;
		var endindex = value.length;
		
		value = value.substring (startindex, endindex);
	}
	
	tempstate = setTokenListValue (tempstate, name, value);
}

function getSessionVariable (name)
{
	if (findTokenListToken (tempstate, name))
		return getTokenListValue (tempstate, name);
	else
		reportError ("Variable '" + name + "' not found.");
}

function clearSessionVariable (name)
{
	if (findTokenListToken (tempstate, name))
		tempstate = clearTokenListValue (tempstate, name);
	else
		reportError ("Variable '" + name + "' not found.");
}

function sessionVariableDefined (name)
{
	if (findTokenListToken (tempstate, name))
		return true;
	else
		return false;
}

// --------------------------------------- Debug Toggle

function activatedebugtoggle ()
{
	document.onkeydown = debugtogglehandler;

	for (var i = 0; i < document.frames.length; i ++)
		document.frames [i].document.onkeydown = debugtogglehandler;
}

function debugtogglehandler ()
{
	var globalevent = findeventacrossframes ();

	if (globalevent && globalevent.altKey && globalevent.shiftKey
			&& globalevent.keyCode == 68)  // alt-shift-D
		toggledebug ();
	
	return true;
}

function toggledebug ()
{
	var outerframe = document.getElementById ("main");
	
	if (outerframe.rows == DEFAULT_ROW_LAYOUT)
	{
		if (window.prompt ("This course requires an administrator password to enter debugging mode:", "")
				== ADMIN_PASSWORD)
			outerframe.rows = DEBUG_ROW_LAYOUT;
	}
	else
		outerframe.rows = DEFAULT_ROW_LAYOUT;
}

function findeventacrossframes ()
{
	if (window.event)
		return window.event;

	for (var i = 0; i < window.frames.length; i ++)
	{
		if (window.frames [i].event)
			return window.frames [i].event;
	}

	return null;
}

// --------------------------------------- User Location

// URL Accessors

function baseURL ()
{
	if (ENABLE_OFFLINE_SWITCHING)
		return localP () ? localURL () : serverURL ();
	else
		return base_url;
}

function serverURL ()
{
	if (variableDefined ("serverurl"))
		return getVariable ("serverurl");
	else
		return null;
}

function localURL ()
{
	if (variableDefined ("localurl"))
		return getVariable ("localurl");
	else
		return null;
}

// --------------------------------------- SCORM Interactions

// does not work with offline switching
// completely ignored in the AICC case
// student_response -> learner_response in SCORM 2004?


// Interaction Class

function interaction (id, type, student_response, result)
{
	this.id = id;

	this.type = type;
	this.student_response = student_response;
	this.result = result;
}

// Adding Interactions

var interactions = new Array ();

function addinteraction (id, type, student_response, result)
{
	if (validchoicetype (type) && validresulttype (result))
		interactions.add (new interaction (id, type, student_response, result));
}

function clearinteractions ()
{
	interactions = new Array ();
}

// Data Type Checking

function validchoicetype (choicetype)
{
	var validchoicetypes = new Array ("true-false", "choice", "fill-in", "long-fill-in",
		"matching", "performance", "sequencing", "likert", "numeric", "other");

	for (var i = 0; i < validchoicetypes.length; i ++)
		if (choicetype == validchoicetypes [i])
			return true;

	return false;
}

function validresulttype (resulttype)
{
	if (typeof resulttype == "number")
		return true;

	var validresulttypes = new Array ("correct", "incorrect", "unanticipated", "neutral");

	for (var i = 0; i < validresulttypes.length; i ++)
		if (resulttype == validresulttypes [i])
			return true;

	return false;
}

/*
cmi.interactions.n.id

cmi.interactions.n.type

true-false
choice (decision-list, decision-area, assessment)
fill-in
matching
performance (decision-radio, decision-checkbox)
sequencing
likert (scale of 1-5)
numeric (decision-number)

long-fill-in (decision-text)

cmi.interactions.n.weighting = 1

cmi.interactions.n.student_response = answer ( can be 1.a,2.b,3.c,4.d )

cmi.interactions.n.result

*/

// --------------------------------------- End Of File
