//////////////// dependencies  ///////////////////////////////
//	must have "ExtendArray.js" included
//	must have "ArrayList.js" included
//	must have "CMITimespan.js" included
//	must have "jdCourse.js" included
//	must have "CMIScore.js" included
//	must have "SCOObjective.js" included
//	must have "SCOObjectives.js" included
//
///////////////////////////////////////////////////////////////

// 2007/8/8, Jalen: Code refactored.
// 2007/3/12, Jalen: "cmi.comments" supported.
// 2006/5/9, Jalen: Property "Type" added to indicate the course type.
// 2005/12/2, Jalen: "cmi.objectives" is utilized.
// Revised on 2005/8/10, Jalen: function 'isAPIFound' added.
// Revised on 2005/7/11, Jalen: function 'getUserID' added.
// Revised on 2005/5/30, Jalen

//	Strange Issue Notes:
//		Sometimes 'SCOInitialize()' gets called after 'SCOGetValue()' is called in IE. To let the course distinguish whether the returned "" is a valid value from the database, function 'SCOGetValue()' is modified so that it will return 'undefined' when SCO is not initialized yet.

///////////////// external calls ////////////////////
/*
SCOInitialize(dummy)
SCOFinish(dummy)
SCOGetValue(field)
SCOSetValue(field, value)
SCOCommit(dummy)
SCOGetLastError()
SCOGetErrorString(errorCode)
SCOGetDiagnostic(errorCode)
SCOSetLessonStatus(newStatus)
SCOSetSessionTime()
isAPIFound()

SCOCheckOptionalGet(field)
SCOCheckOptionalSet(field, value)
*/

//////////////// internal calls  ///////////////////
/*
getAPI(bDetectionMode)
findAPI(win)
SCOSetInitialStatus()
SCOSetScore(score)
LMSError(errorSource)
reportAPIError(errorSource, errorString) 
reportLMSError(errorSource, errorString)
displayError(errorSource, errorString)
APIFound()
checkSuccess(result)
*/


jdCourse_SCORM.prototype = new jdCourse();
function jdCourse_SCORM() {
	this.init = init;
	this.getLMSCurrentPage = getLMSCurrentPage;
	this.setLMSCurrentPage = setLMSCurrentPage;
	this.getLMSVisitedPages = getLMSVisitedPages;
	this.setLMSVisitedPages = setLMSVisitedPages;
	this.getLMSQuizScore = getLMSQuizScore;
	this.setLMSQuizScore = setLMSQuizScore;
	this.getMasteryScore = getMasteryScore;
	this.isSCOCredit = isSCOCredit;
	this.gIsPageVisited = gIsPageVisited;
	this.getUserID = getUserID;
	this.getUserFullName = getUserFullName;
	this.getObjectives = getObjectives;
	this.setObjectives = setObjectives;
	this.getLMSCommentsFromLearner = getLMSCommentsFromLearner;
	this.setLMSCommentsFromLearner = setLMSCommentsFromLearner;
	this.SCOInitialize = SCOInitialize;
	this.SCOSetInitialStatus = SCOSetInitialStatus;
	this.SCOFinish = SCOFinish;
	this.SCOGetValue = SCOGetValue;
	this.SCOSetValue = SCOSetValue;
	this.SCOSetScore = SCOSetScore;
	this.SCOCommit = SCOCommit;
	this.SCOGetLastError = SCOGetLastError;
	this.SCOGetErrorString = SCOGetErrorString;
	this.SCOGetDiagnostic = SCOGetDiagnostic;
	this.SCOCheckOptionalGet = SCOCheckOptionalGet;
	this.SCOCheckOptionalSet = SCOCheckOptionalSet;
	this.SCOSetLessonStatus = SCOSetLessonStatus;
	this.SCOSetSessionTime = SCOSetSessionTime;
	this.LMSError = LMSError;
	this.isAPIFound = isAPIFound;
	
	// API error messages
	var API_MISSING_MSG = "API object undefined";
	var API_NOT_FOUND_MSG = "Cannot find API in the frame hierarchy.";
	var API_TOO_DEEP_MSG = "Cannot find API - the frame hierarchy is too deep.";
	var INIT_FAILED_MSG = "Found API, but LMSInitialize failed.";
	// LMS error codes
	var API_MISSING_ERR = -1;
	var LMS_NO_ERROR_ERR = 0;
	var LMS_GENERAL_EXCEPTION_ERR = 101;
	var LMS_SERVER_BUSY_ERR = 102;
	var LMS_INVALID_ARGUMENT_ERR = 201;
	var LMS_ELEMENT_CANNOT_HAVE_CHILDREN_ERR = 202;
	var LMS_ELEMENT_NOT_ARRAY_ERR = 203;  
	var LMS_NOT_INITIALIZED_ERR = 301;
	var LMS_NOT_IMPLEMENTED_ERR = 401;
	var LMS_INVALID_SET_VALUE_ERR = 402;
	var LMS_ELEMENT_READ_ONLY_ERR = 403;
	var LMS_ELEMENT_WRITE_ONLY_ERR = 404;
	var LMS_INCORRECT_DATA_TYPE_ERR = 405;
	//	Debugging flags, setting to true will display all API and LMS errors as javascript alerts in the browser
	var showAPIErrors = true;
	var showLMSErrors = true;
	// SCO score management - change the values as required by the SCO
	var minScore = 0;
	var maxScore = 100;
	var minScoreCurrent = false;		// flags to indicate if LMS values are current
	var maxScoreCurrent = false;
	// API search
	var findAPITries = 0;
	var findAPITriesLimit = 100;		// depth of search
	var API = null;					// the handle for the LMS API object
	// Flag to avoid multiple LMSCommit calls - can impede performance
	var SCOCommitDone = false;
	
	var iMasteryScore, oObjectives;
	
	this.init();

	function init() {
		jdCourse_SCORM.prototype.init.call(this);	// Generates error on IE5, so it's commented just for IE5.
		// Added Properties
		this.Type = "SCORM";
	}
	
	function getLMSCurrentPage() {
		var result = this.SCOGetValue("cmi.core.lesson_location");
		//if (this.isDebugMode()) alert("cmi.core.lesson_location: " + result);
		return result;
	}
	
	function setLMSCurrentPage(sPageID) {
		this.SCOSetValue("cmi.core.lesson_location", sPageID);
	}
	
	function getLMSVisitedPages() {
		return this.SCOGetValue("cmi.suspend_data");
	}
	
	function setLMSVisitedPages(sPages) {
		this.SCOSetValue("cmi.suspend_data", sPages);
	}
	
	function getLMSQuizScore() {
		return this.SCOGetValue("cmi.core.score.raw");
	}

	function setLMSQuizScore(iScore) {
		this.SCOSetValue("cmi.core.score.raw", iScore);
	}
	
	// Returns an integer or null.
	//		Returns null value in 2 cases:
	//			1. LMS does not support "mastery_score" and the default value is not set at all.
	//			2. LMS supports "mastery_score" but the value is not set properly.
	function getMasteryScore() {
		if (iMasteryScore == null) {
			/*if (this.SCOCheckOptionalGet("cmi.student_data.mastery_score")) {
				var passScoreLMS = parseInt(this.SCOGetValue("cmi.student_data.mastery_score"), 10);
				if (!isNaN(passScoreLMS)) {
					iMasteryScore = passScoreLMS;
				}
			} else {
			*/	iMasteryScore = this.defaultMasteryScore;
			//}
		}
		return iMasteryScore;
	}
	
	function isSCOCredit() {
		var sCredit = this.SCOGetValue("cmi.core.credit");
		return (this.getMasteryScore() != null && sCredit.toLowerCase() == "credit");
	}
	
	function gIsPageVisited(sPageName) {		// Needs to be re-factored.
		//return true if the visited pages contains the page name;
		// sPageName is without file extension .htm;
		var vVisitedPages = this.getVisitedPages();
		if ((vVisitedPages == null) || (vVisitedPages == "")) return false;
		var aVisitedPages = vVisitedPages.split("|");
		return (isStringInArray(sPageName, aVisitedPages, true));
	}
	
	function getUserID() {
		return this.SCOGetValue("cmi.core.student_id");
	}

	function getUserFullName() {
		return this.SCOGetValue("cmi.core.student_name");
	}

	function getObjectives() {
		if (oObjectives == null) {
			oObjectives = new SCOObjectives();
			var oItem;
			var iCount = this.SCOGetValue("cmi.objectives._count");
			for (var i = 0; i < iCount; i++) {
				oItem = new SCOObjective();
				oItem.id = this.SCOGetValue("cmi.objectives." + i + ".id");
				oItem.score = new CMIScore();
				oItem.score.scaled = "";	// Reserved for SCORM 1.3
				oItem.score.raw = this.SCOGetValue("cmi.objectives." + i + ".score.raw");
				oItem.score.min = this.SCOGetValue("cmi.objectives." + i + ".score.min");
				oItem.score.max = this.SCOGetValue("cmi.objectives." + i + ".score.max");
				oItem.status = this.SCOGetValue("cmi.objectives." + i + ".status");
				oObjectives.Add(oItem);
			}
		}
		return oObjectives;
	}
	
	function setObjectives(obj) {
		oObjectives = obj;
		for (var i = 0; i < oObjectives.count(); i++) {
			this.SCOSetValue("cmi.objectives." + i + ".id", oObjectives.Items(i).id);
			//this.SCOSetValue("cmi.objectives." + i + ".score.scaled", oObjectives.Items(i).score.scaled);
			this.SCOSetValue("cmi.objectives." + i + ".score.raw", oObjectives.Items(i).score.raw);
			this.SCOSetValue("cmi.objectives." + i + ".score.min", oObjectives.Items(i).score.min);
			this.SCOSetValue("cmi.objectives." + i + ".score.max", oObjectives.Items(i).score.max);
			this.SCOSetValue("cmi.objectives." + i + ".status", oObjectives.Items(i).status);
		}
	}

	function getLMSCommentsFromLearner() {
		return this.isDebugMode() ? "comments from learner by jdCourse_SCORM.getLMSCommentsFromLearner()" : this.SCOGetValue("cmi.comments");
	}

	function setLMSCommentsFromLearner(sComments) {
		this.SCOSetValue("cmi.comments", sComments);
	}


	/****************************
		Main LMS functions 
	****************************/
	
	function isAPIFound() {
	   return (getAPI(true) != null);
	}

	/**
	   getAPI()
	   - searches for the API object in following order, top-down:
		 1) full frame hierarchy of sco's top window
		2) full hierarchy of sco's top opener's top window
		3) etc. for opener's of top windows in a chain of openers
	   Returns the API object if found, otherwise null
	
	   Note: Assumes that window is defined and not null
	   - the ADL and Click2Learn sample code fail to find the API on several
		 LMSs because the search is too limiting - it's bottom-up and doesn't
		cover all reasonable ways of organizing frames by an LMS.
	   - This code modifies the search method to exhaustive, top-down approach,
		 guaranteed to find the API object if it exists.
	   - Since the 2 searches work in opposite ways, there is a slight danger
		 that a different API object might be found if more than 1 exists in the
		frame hierarchy. This danger is practically non-existent in current
		LMSs.
	**/
	function getAPI(bDetectionMode) {
		var topWin = window.top;
	
		// 1) search full hierarchy of sco window, from top
		var theAPI = findAPI(topWin);
	   // 2+3) search opener's chain
		while (theAPI == null && typeof(topWin.opener) != "undefined" && topWin.opener != null) {
			topWin = topWin.opener.top;
			theAPI = findAPI(topWin);
		}
	
		if (!bDetectionMode && (theAPI == null)) {
			var errorSource = "getAPI(" + ")";
			reportAPIError(errorSource, API_NOT_FOUND_MSG);
		}
		return theAPI;
	}
	
	/**
	   findAPI(win)
	   - auxiliary function for getAPI
	   - searches rescursively for the API object top-down in the frame hierarchy 
		 starting from win
	   - search depth currently not limited
	   Returns the API object if found, otherwise null
	**/
	function findAPI(win) {
		// recursion end case - has API
		if (typeof(win.API) != "undefined" && win.API != null)
			return win.API;
		// recursive step - search children frames
		var theAPI = null;
		if (win.frames.length > 0) {
			for (var i=0; i < win.frames.length; i++) {
				theAPI = findAPI(win.frames[i]);
				if (theAPI != null) {
					return theAPI;
				}
			}
		}
		// end on failure - no API
		return null;
	}
	
	/**
	   SCOInitialize(dummy)
	   - calls LMSInitialize, sets initial lesson_status and data (optional)
	   - starts measuring session time in SCO
	   - sets SCOInitialized flag to avoid multiple initializations and
		 to enable other LMS functions
	   - the dummy parameter is not used now, included for future
		 extension of SCORM
	   Returns "true" if initialization successful, "false" in all other cases
	   (failed, already initialized, API not found)
	**/
	function SCOInitialize(dummy) {
		var success = false;
		if (!this.SCOInitialized) {
			API = getAPI();
	
			if (APIFound()) {
				success = API.LMSInitialize("");
	
				if (checkSuccess(success)) {
					this.SCOInitialized = true;
					this.SCOSetInitialStatus();
					// optional data initializations
					this.getCurrentPage();
					this.getVisitedPages();
					this.getQuizScore();
					/*start modification: 20040126, SCOInitializeData() not defined anywhere is in the supplied wrapper files
					if (typeof(SCOInitializeData) != "undefined") {
						SCOInitializeData();
					}
					end modidication: 20040126 */
					// timestamp for beginning of session
					this.sessionStart = new Date();
				} else {
					// possibly handle initialization error, for now just alert
					var errorSource = "SCOInitialize(" + ")";
					reportAPIError(errorSource, INIT_FAILED_MSG);
				}
			} // else: API not found - reported by getAPI, so do nothing
		}
		// else: already initialized before, so do nothing
	
		// Click 2 Learn had sessionStart and SCOInitializeData here, but those only
		// make sense if initialization successful, so putting them up
	
		return (success + "");	  // Force type to string
	}
	
	/****************************
		Supporting LMS functions 
	****************************/
	
	/**
	   SCOSetInitialStatus()
	   - sets lesson_status on initialization according to lesson_mode:
		 - in browse mode, set to browsed if this is 1st attempt at SCO
		 - in other modes, set to incomplete unless is already set to
		   stronger status (completed, passed, failed)
	   Returns "true" is SetValue succeeded, otherwise "false"
	   Note: called only when API defined and LMSInitialization succeeded
	**/
	function SCOSetInitialStatus() {
		var result = false;
		var mode = this.SCOGetValue("cmi.core.lesson_mode");
		var status = this.SCOGetValue("cmi.core.lesson_status");
		if (mode == this.BROWSE) {
			if (status == this.NOT_ATTEMPTED) {
				result = this.SCOSetValue("cmi.core.lesson_status", this.BROWSED);
			}
			// here could set to INCOMPLETE if status is BROWSED to make
			// sure BROWSED is only set on 1st attempt at SCO, but the
			// spec is quite unclear about this, and the BROWSED status
			// contains arguably more info, so leave it
		} else if (mode == this.NORMAL || mode == this.REVIEW) { 
			if (status == this.NOT_ATTEMPTED || status == this.BROWSED) {
				result = this.SCOSetValue("cmi.core.lesson_status", this.INCOMPLETE);
			}
		} else if (status == this.NOT_ATTEMPTED || status == this.BROWSED) {
			// SCOGetValue failed for mode, likely because not supported by LMS
			// treat as NORMAL mode
			result = this.SCOSetValue("cmi.core.lesson_status", this.INCOMPLETE);
		}
		return (result + "");		  // force to string
	}
	
	/**
	   SCOFinish(dummy)
	   - reports session time and optionally finalizes data in LMS
	   - calls LMSFinish and sets flag SCOInitialzied to false to 
		 prohibit other LMS calls after
	   - the dummy parameter is not used now, included for future
		 extension of SCORM
	   Returns "true" if LMSFinish call was issued and successful, 
	   "false" in all other cases (failed, already finished, API not found)
	**/
	function SCOFinish(dummy) {
		var success = false;
		if (APIFound() && this.SCOInitialized) {
			this.SCOSetSessionTime();
			/* //start modification 20040126, SCOFinalizeData() is not defined in supplied wrapper files, commented out
			if (typeof(SCOFinalizeData) != "undefined"){
				SCOFinalizeData();
			}
			//end modification 20040126 */
			// force save to LMS to make sure nothing is lost
			API.LMSCommit("");
			success = API.LMSFinish("");     
			if (checkSuccess(success)) {
				this.SCOInitialized = false;
			}
			window.close();
		}
		return (success + "");	  // Force type to string
	}
	
	
	/****************************************
	   Wrapper functions for LMS functions:
		- check that API is ok and call the corresponding LMS function
		 - make sure SCO is initialized
	   Return the result of the LMS function if successful, otherwise special value
	   The results can be either strings or undefined.
	*****************************************/
	
	// field is a CMI data element
	function SCOGetValue(field) {
		//alert("SCORM.SCOGetValue() get called. API: " + API + "; SCOInitialized: " + this.SCOInitialized);
		if (APIFound() && this.SCOInitialized) {
			var result = API.LMSGetValue(field);
			var errorSource = "LMSGetValue(" + field + ")";
			if (this.LMSError(errorSource))
				result = "";
			return (result + "");		  // force to string
		} else {
			//  SCO is not initialized yet. Returns the 'undefined' instead of the "".
			return;
		}
	}
	
	// More complicated logic is needed fo SetValue, mainly because of scoring
	/**
	   SCOSetValue(field, value)
		- sets field in LMS to value
		 - score has to be handled as a special case
		  - for min and max scores updates the remembered value
	   Returns "true" if value was set successfully, 
	   "false" in all other cases (failed, API not found)
	**/
	function SCOSetValue(field, value) {
		var success = "false";
		if (APIFound() && this.SCOInitialized) {
			// process different cases
			if (field == "cmi.core.score.raw") {
				success = this.SCOSetScore(value);
			} else {
				success = API.LMSSetValue(field, value.toString());
				// check for errors
				var errorSource = "LMSSetValue(" + field + ", " + value + ")";
				this.LMSError(errorSource);
	
				// set local flags and vars for score range
				if (field == "cmi.core.score.min" && success == "true") {
					minScoreCurrent = true;
					minScore = value;
				} else if (field == "cmi.core.score.max" && success == "true") {
					maxScoreCurrent = true;
					maxScore = value;
				}
			}
			// data was saved, so a commit needs to be done
			if (checkSuccess(success)) SCOCommitDone = false;
		}
		return (success + "");	  // force type to string
	}
	
	/**
	   SCOSetScore(score) 
	   - auxiliary function called by SCOSetValue
	   - calculates raw score based on min and max scores
	   - makes sure values in LMS are current
	   Returns "true" if all LMS updates succeed, otherwise  "false"
	*/
	function SCOSetScore(score) {
		if (isNaN(score)) return "false";
		if (!minScoreCurrent) {
			if (API.LMSSetValue("cmi.core.score.min", minScore.toString()) != "true") 
				return "false";
			minScoreCurrent = true;
		}
		if (!maxScoreCurrent) {
			if (API.LMSSetValue("cmi.core.score.max", maxScore.toString()) != "true") 
				return "false";
			maxScoreCurrent = true;
		}
		if (API.LMSSetValue("cmi.core.score.raw", score.toString()) != "true")	{
			return "false";
		}
		
		return "true";
	}
	
	// the parameter is not used in current (1.2) version of SCORM
	function SCOCommit(dummy) {
		var success = false;
		if (APIFound() && this.SCOInitialized && !SCOCommitDone) {
			success = API.LMSCommit("");
			if (checkSuccess(success)) SCOCommitDone = true;
		}
		return (success + "");	  // force to string
	}
	
	function SCOGetLastError() {
		var errorCode = API_MISSING_ERR;
		if (APIFound() && this.SCOInitialized) {
			errorCode = API.LMSGetLastError();
		}
		return (errorCode + "");	  // force to string
	}
	
	// parameter is error code for LMS and API errors
	function SCOGetErrorString(errorCode) {
		var errorMsg = API_MISSING_MSG;
		if (APIFound() && this.SCOInitialized) {
			errorMsg = API.LMSGetErrorString(errorCode);
		}
		return (errorMsg + "");    // force to string
	}
	
	// parameter is error code for LMS and API errors
	function SCOGetDiagnostic(errorCode) {
		var diagnosticMsg = API_MISSING_MSG;
		if (APIFound() && this.SCOInitialized) {
			diagnosticMsg = API.LMSGetDiagnostic(errorCode);
		}
		return (diagnosticMsg + "");	  // force to string
	}
	
	
	
	/**********************************************
	   Check support for optional data fields in LMS 
		 - Following 2 functions check LMS' support for optional data fields by
		   calling LMSGetValue or LMSSetValue on the field and checking the
		   error code after for LMS_NOT_IMPLEMENTED_ERR.
	   Return Boolean
	   Note: 2 functions are necessary because some field are read-only or
			 write-only   
	***********************************************/
	
	// field is CMI data element
	function SCOCheckOptionalGet(field) {
		var result = false;
		if (APIFound() && this.SCOInitialized) {
			var dummy = API.LMSGetValue(field);
			if (API.LMSGetLastError() != LMS_NOT_IMPLEMENTED_ERR) 
				result = true;
		}
		return result;
	}
	
	// field is CMI data element, value is its value
	// This will set the value if the element is supported, but that
	// won't matter as it will be then overwritten by the proper value if
	// we are careful to call this when content wants to set the field!
	function SCOCheckOptionalSet(field, value) {
		var result = false;
		if (APIFound() && this.SCOInitialized) {
			API.LMSSetValue(field, value);
			if (API.LMSGetLastError() != LMS_NOT_IMPLEMENTED_ERR) 
				result = true;
		}
		return result;
	}
	
	
	/**
	   SCOSetLessonStatus(newStatus)
	   - sets lesson_status to newStatus (completed, passed, or failed)
	   - only done in review or normal modes, not browse
	
	   - completed will not override stronger states (passed, failed)
	   Returns "true" is SetValue succeeded, otherwise "false"
	**/
	function SCOSetLessonStatus(newStatus) {
		var result = false;
		var mode = this.SCOGetValue("cmi.core.lesson_mode");
		var status = this.SCOGetValue("cmi.core.lesson_status");  
	
		// check that GetValue succeeded and we are in the right mode
		if (mode != this.BROWSE && mode != "" && status != "") {
			if (newStatus == this.COMPLETED) {
				if (status != this.COMPLETED && status != this.PASSED && status != this.FAILED) {
					result = this.SCOSetValue("cmi.core.lesson_status", this.COMPLETED);
				}
			} else if (newStatus == this.PASSED || newStatus == this.FAILED) {
				result = this.SCOSetValue("cmi.core.lesson_status", newStatus);
			}
			// else do nothing for the 3 remaining states - NOT_ATTEMPTED,
			// BROWSED, and INCOMPLETE. Those are handled by other functions
			// automatically.
		}
		return (result + "");		  // force to string
	}
	
	/**
	   SCOSetSessionTime()
	   - sets time spent by learner in this attempt (session) at SCO
	   Returns "true" if SetValue call succeeded, "false" otherwise
	   Note: Called automatically by SCOFinish(), but can be called anytime
	**/
	function SCOSetSessionTime() {
		var sessionEnd = new Date();
		var sessionTime = sessionEnd.getTime() - this.sessionStart.getTime();
		return this.SCOSetValue("cmi.core.session_time", (new CMITimespan()).Parse(sessionTime));
	}
	
	/**
	   LMSError(errorSource)
	   - error handler
	   Returns true if data communication with LMS failed, otherwise false
	**/
	function LMSError(errorSource) {
		error = false;
		var errorCode = this.SCOGetLastError();
		if (errorCode != LMS_NO_ERROR_ERR) {
			error = true;
			reportLMSError(errorSource, this.SCOGetErrorString(errorCode));
		}
		return error;
	}
	
	
	/****************************
		Auxiliary functions 
	****************************/
	
	// report errors conditionally
	function reportAPIError(errorSource, errorString) {
	   if (showAPIErrors) displayError(errorSource, errorString);
	}
	
	function reportLMSError(errorSource, errorString) {
	   if (showLMSErrors) displayError(errorSource, errorString);
	}
	
	function displayError(errorSource, errorString) {
		alert("Error in " + errorSource + ":\n" + errorString);
	}
	
	// check that API is "active"
	// Note: cannot be "undefined", so test for "null" value only
	function APIFound() {
	   return (API != null);
	}
	
	// check that LMS operation succeeded
	function checkSuccess(result) {
	   return (result.toString() == "true");
	}

}

