/*
 * @(#)lmsapi.js 1.3.2 2004-06-18
 *
 * Copyright (c) 2003 Werner Randelshofer
 * Staldenmattweg 2, Immensee, CH-6405, Switzerland
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of 
 * Werner Randelshofer. ("Confidential Information").  You shall not
 * disclose such Confidential Information and shall use it only in
 * accordance with the terms of the license agreement you entered into
 * with Werner Randelshofer.
 */ 
 
/**
 * This file contains the implementation of a minimal Learning Management
 * System that complies to the Sharable Content Object Reference Model
 * (SCORM™) 1.2 specification.  
 *
 * This file is intended to be included in the top level frameset of a 
 * eLearning course generated by TinyLMS.
 * Please note that the sequence of the following list of JavaScript statements
 * must not be changed.
 *
 * Example (Use of TinyLMS as a standalone LMS):
 * <html>
 *   <head>
 *     <title>Learning Management System</title>
 *     <script language="JavaScript" src="tinylms/lib/listlogger.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" src="tinylms/lib/collections.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" src="tinylms/lib/cookies.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" src="tinylms/lib/sha1.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" src="tinylms/lib/lmsparentstub.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" src="tinylms/lib/lmscam.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" src="tinylms/lib/lmsapi.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" src="tinylms/lib/lmslabels_en.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" src="imsmanifest.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" type="text/JavaScript">API.start()</script>
 *   </head>
 *   <frameset cols="80,*" frameborder="NO" border="0" framespacing="0">
 *     <frame src="lmstoc.html" name="leftFrame" scrolling="NO" noresize>
 *     <frame src="lmslogin.html" name="lmsContentFrame">
 *   </frameset>
 * </html>
 *
 * Example (Use of TinyLMS as a SCORM adapter):
 * <html>
 *   <head>
 *     <title>Learning Management System</title>
 *     <script language="JavaScript" src="tinylms/lib/logging.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" src="tinylms/lib/collections.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" src="tinylms/lib/cookies.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" src="tinylms/lib/sha1.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" src="tinylms/lib/lmsparentstub.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" src="tinylms/lib/lmscam.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" src="tinylms/lib/lmsapi.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" src="tinylms/lib/lmslabels_en.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" src="imsmanifest.js" type="text/JavaScript"></script>
 *     <script language="JavaScript" type="text/JavaScript">API.start()</script>
 *   </head>
 *   <frameset cols="80,*" frameborder="NO" border="0" framespacing="0">
 *     <frame src="lmstoc.html" name="leftFrame" scrolling="NO" noresize>
 *     <frame src="lmslogin.html" name="lmsContentFrame">
 *   </frameset>
 * </html>
 *
 *
 *
 *
 * Reference:
 * ADL (2001a). Advanced Distributed Learning.
 * Sharable Content Object Reference Model (SCORM(TM)) Version 1.2. 
 * The SCORM Overview. October 1, 2001.
 * Internet (2003-01-20): http://www.adlnet.org
 *
 * ADL (2001b). Advanced Distributed Learning.
 * Sharable Content Object Reference Model (SCORM(TM)) Version 1.2. 
 * The SCORM Runtime Environment. October 1, 2001.
 * Internet (2003-01-20): http://www.adlnet.org
 *
 * ADL (2001c). Advanced Distributed Learning.
 * Sharable Content Object Reference Model (SCORM(TM)) Version 1.2. 
 * The SCORM Content Aggregation Model. October 1, 2001.
 * Internet (2003-01-20): http://www.adlnet.org
 *
 * @author Werner Randelshofer, Staldenmattweg 2, Immensee, CH-6405, Switzerland
 * @version 1.3.2 2004-06-18 Moved hardcoded relative path to course directory into instance variable coursePath.
 * Expanding and collapsing of tree nodes in the TOC did not work with some browsers.
 * 1.3.1 2004-06-14 Logging output did not work. 
 * 1.3 2004-06-13 TinyLMS did not work when it was used as a frame inside a frameset.
 * New logging script by Kent Ogletree added.
 * 1.2 2003-01-06 TinyLMS can now be used as an SCO inside a parent SCORM LMS.
 * 1.1.8 2003-11-20 Function gotoResumeItem() must try to resume layer by layer
 * when layered structuring is used. Moved functions getNextRow/getPreviousRow from file
 * lmslayernav.js into this file. Fixed a bug in function LMSInitialize, which caused the
 * LMS to 'hang', if an SCO 'forgets' to call LMSFinish.
 * 1.1.6 2003-11-05 Automatic sequencing and Debugging can be switched on
 * and off now from manifest.js.
 * 1.1.1 2003-11-04 Login in "any"-mode did not work. Fixed a bug in LMSInitialize, which
 * occured, when the SCO changed its URL before calling LMSInitialize. After login
 * we resume the course at the first non-completed item.
 * 1.1 2003-11-01 Security added (Password check for log in).
 * 1.0.3 2003-10-30 Function LMSAPI_saveDatabase and LMSAPI_loadDatabase
 * did not always save the lession_location when the organization had only one
 * item.
 * 1.0.2 2003-10-09 Script error in function LMSAPI_gotoPreviousItem
 * fixed.
 * 1.0.1 2003-09-29 Login as 'Guest' user, if the userid is an empty String ''.
 * 1.0 2003-09-12 Support for localization of labels added. If the
 * organization structure of a course consists of a single item element only, we
 * attempt to make its lesson_location attribute persistent as well.
 * Save the database every time LMSSetValue is called.
 * 0.29 2003-06-11 Automatic sequencing did not work.
 * 0.24 2003-05-07 LMSAPI_gotoNextItem() and LMSAPI_gotoPreviousItem()
 * don't go automatically to the menu page anymore, when the user tries to 
 * go over the last or the first page of the tutorial.
 * 0.20 2003-04-08 Cookie format changed.
 * 0.19 2003-03-28 Revised.
 * 0.18 2003-03-26 Support for layered organization structures added.
 * 0.17 2003-03-16 Naming conventions for CAM elements streamlined with Java implementation.
 * 0.3 2003-03-05 Revised.
 * 0.1 2003-01-26 Created.
 */


/**
 * Reads the CMI data model from the specified CAM resource.
 * 
 * @param itemElement A CAM item from the this.cam data model.
 */
function LMSAPI_readCMIdataModel(itemElement) {
    // We read the CMI data model from the CAM resource object.
	
	var resource = itemElement.getResource();
	if (resource != null) {
		this.setValue("cmi.core.lesson_status", resource.cmi_core_lesson_status);
		this.setValue("cmi.core.entry", (resource.cmi_core_lesson_status == "not attempted") ?	"ab-initio" : "");
	} else {
		this.setValueToDefault("cmi.core.lesson_status");
		this.setValueToDefault("cmi.core.entry", "ab-initio");
	}
    this.setValue("cmi.launch_data", itemElement.dataFromLMS);

	if (resource == null || resource.cmi_core_lesson_status == "not attempted") {
		this.setValueToDefault("cmi.core.lesson_location");
		this.setValueToDefault("cmi.core.credit");
		this.setValueToDefault("cmi.core.score.raw");
		this.setValueToDefault("cmi.core.total_time");
		this.setValueToDefault("cmi.core.exit");
		this.setValueToDefault("cmi.core.session_time");
		this.setValueToDefault("cmi.suspend_data");
	} else {
		this.setValue("cmi.core.lesson_location", resource.cmi_core_lesson_location);
		this.setValue("cmi.core.credit", resource.cmi_core_credit);
		this.setValue("cmi.core.score.raw", resource.cmi_core_score_raw );
		this.setValue("cmi.core.total_time", resource.cmi_core_total_time);
		this.setValue("cmi.core.exit", resource.cmi_core_exit);
		this.setValue("cmi.core.session_time", resource.cmi_core_session_time);
		this.setValue("cmi.suspend_data", resource.cmi_suspend_data);
	}
}
/**
 * Writes the CMI data model into the specified CAM resource.
 * 
 * @param itemElement A CAM item from the this.cam data model.
 */
function LMSAPI_writeCMIdataModel(itemElement) {
	if (itemElement.getResource() != null) {
	  var resource = itemElement.getResource();

		resource.cmi_core_lesson_location = this.getValue("cmi.core.lesson_location");
		resource.cmi_core_credit = this.getValue("cmi.core.credit");
		resource.cmi_core_lesson_status = this.getValue("cmi.core.lesson_status");
		resource.cmi_core_entry = this.getValue("cmi.core.entry");
		resource.cmi_core_score_raw = this.getValue("cmi.core.score.raw");
		resource.cmi_core_total_time = this.getValue("cmi.core.total_time");
		resource.cmi_core_exit = this.getValue("cmi.core.exit");
		resource.cmi_core_session_time = this.getValue("cmi.core.session_time");
		resource.cmi_suspend_data = this.getValue("cmi.suspend_data");
	}
}
/**
 * Loads the database of the LMS from persistent storage.
 * The data is loaded into the resource objects of the this.cam data model.
 *
 * XXX - TinyLMS provides only very limited persistence for a course.
 *       For more information see the comments on the LMSAPI_saveDatabase()
 *       function.
 */
function LMSAPI_loadDatabaseFromCookie() {
  logger.write(logger.INTERNAL,"lms.loadDatabaseToCookie");
  this.resetDatabase();

  var cookie_name ="TinyLMS."+this.cam.identifier+"."+this.getValue("cmi.core.student_id");
  var cookie = getCookie(cookie_name);
  var logmsg = "lms.loadDatabaseFromCookie()\n<br>&nbsp; cookie_name="+cookie_name+"\n<br>&nbsp; cookie_value="+cookie;
	var map = new Map();
	if (cookie != null) map.importFromStringDelim(cookie,".");

  var node = this.cam.resources.getFirstLeaf();
	while (node != null && node != this.cam.resources) {
	  if (node.isResourceElement) {
			switch (map.get(node.identifier)) {
				case "p" : node.cmi_core_lesson_status = "passed"; break;
				case "c" : node.cmi_core_lesson_status = "completed"; break;
				case "f" : node.cmi_core_lesson_status = "failed"; break;
				case "i" : node.cmi_core_lesson_status = "incomplete"; break;
				case "b" : node.cmi_core_lesson_status = "browsed"; break;
				case "n" :
				default : node.cmi_core_lesson_status = "not attempted"; break;
			}
		}
	  node = node.getNextNode();
	}

       // If the organization consists of only one item element,
	// we attempt to load its lesson location as well.
	if (this.cam.organizations.getFirstLeaf() == this.cam.organizations.getLastLeaf()) {
      node = this.cam.organizations.getFirstLeaf().getResource();
  		cookie_name ="TinyLMS."+this.cam.identifier+"."+this.getValue("cmi.core.student_id")+".loc";
	  node.cmi_core_lesson_location = getCookie("TinyLMS."+this.cam.identifier+"."+this.getValue("cmi.core.student_id")+".loc");
  logmsg += "<br>&nbsp; cookie_name="+cookie_name+"\n<br>&nbsp; cookie_value="+node.cmi_core_lesson_location;
	  if (node.cmi_core_lesson_location == null) node.cmi_core_lesson_location = "";
	}
	logger.write(logger.INTERNAL_SUCCESS, logmsg);
}
/**
 * Saves the database of the LMS into persistent storage.
 * The data is saved from the resource objects of the this.cam data model.
 *
 * XXX - TinyLMS provides only very limited persistence for a course.
 *       If a course contains more than one Item, only the
 *       cmi.core.lesson_status of each SCO is made persistent.
 *       If a course contains exactly one Item, then only the 
 *       cmi.core.lesson_status and the cmi.core.lesson_lccation
 *       are made persistent.
 */
function LMSAPI_saveDatabaseToCookie() {
  logger.write(logger.INTERNAL,"lms.saveDatabaseToCookie");
	var map = new Map();

  var node = this.cam.resources.getFirstLeaf();
	while (node != null && node != this.cam.resources) {
	  if (node.isResourceElement) {
			switch (node.cmi_core_lesson_status) {
				case "passed"     : map.put(node.identifier, "p"); break;
				case "completed"  : map.put(node.identifier, "c"); break;
				case "failed"     : map.put(node.identifier, "f"); break;
				case "incomplete" : map.put(node.identifier, "i"); break;
				case "browsed"    : map.put(node.identifier, "b"); break;
				case "not attempted" :
				default : break;
			}
		}
	  node = node.getNextNode();
	}

	var expires = new Date();
	expires.setYear(expires.getYear() + 1);
	var cookie_name = "TinyLMS."+this.cam.identifier+"."+this.getValue("cmi.core.student_id");
	var cookie_value = map.exportToStringDelim(".");
	setCookie(cookie_name, cookie_value, expires);
	var logmsg = "lms.saveDatabaseToCookie()\n<br>&nbsp; cookie_name="+cookie_name+"\n<br>&nbsp; cookie_value="+cookie_value;

  // If the organization consists of only one item element,
	// we store the lesson location as well.
	if (this.cam.organizations.getFirstLeaf() == this.cam.organizations.getLastLeaf()) {
      node = this.cam.organizations.getFirstLeaf().getResource();
  		var cookie_name = "TinyLMS."+this.cam.identifier+"."+this.getValue("cmi.core.student_id")+".loc";
		var cookie_value = node.cmi_core_lesson_location;
	    setCookie(cookie_name, cookie_value, expires);
       logmsg += "<br>&nbsp; cookie_name="+cookie_name+"\n<br>&nbsp; value="+cookie_value;
	}
	logger.write(logger.INTERNAL_SUCCESS, logmsg);
}
/**
 * Loads the database of the LMS from persistent storage.
 * The data is loaded into the resource objects of the this.cam data model.
 *
 * XXX - TinyLMS provides only very limited persistence for a course.
 *       For more information see the comments on the LMSAPI_saveDatabase()
 *       function.
 */
function LMSAPI_loadDatabaseFromParentLMS() {
  this.resetDatabase();

	// Note: The following statements use the variable 'parentstub' defined in lmsparentstub.js
	var map = new Map();
  var persistentData = parentstub.LMSGetValue("cmi.core.lesson_location");
	var errorCode = parentstub.LMSGetLastError();
	if (errorCode == "0") {
		map.importFromStringDelim(persistentData,".");
	} else {
	  write(5,"Error","LMSAPI_loadDatabaseFromParentLMS failed\nerrorCode="+errorCode);
	}
	
  var node = this.cam.resources.getFirstLeaf();
	while (node != null && node != this.cam.resources) {
	  if (node.isResourceElement) {
			switch (map.get(node.identifier)) {
				case "p" : node.cmi_core_lesson_status = "passed"; break;
				case "c" : node.cmi_core_lesson_status = "completed"; break;
				case "f" : node.cmi_core_lesson_status = "failed"; break;
				case "i" : node.cmi_core_lesson_status = "incomplete"; break;
				case "b" : node.cmi_core_lesson_status = "browsed"; break;
				case "n" :
				default : node.cmi_core_lesson_status = "not attempted"; break;
			}
		}
	  node = node.getNextNode();
	}
	/*
  // If the organization consists of only one item element,
	// we attempt to load its lesson location as well.
	if (this.cam.organizations.getFirstLeaf() == this.cam.organizations.getLastLeaf()) {
      node = this.cam.organizations.getFirstLeaf().getResource();
	  node.cmi_core_lesson_location = getCookie("TinyLMS."+this.cam.identifier+"."+this.getValue("cmi.core.student_id")+".loc");
	  if (node.cmi_core_lesson_location == null) node.cmi_core_lesson_location = "";
	}*/
}
/**
 * Saves the database of the LMS into persistent storage.
 * The data is saved from the resource objects of the this.cam data model.
 *
 * XXX - TinyLMS provides only very limited persistence for a course.
 *       If a course contains more than one Item, only the
 *       cmi.core.lesson_status of each SCO is made persistent.
 *       If a course contains exactly one Item, then only the 
 *       cmi.core.lesson_status and the cmi.core.lesson_location
 *       are made persistent.
 */
function LMSAPI_saveDatabaseToParentLMS() {
  logger.write(logger.INTERNAL,"LMSAPI_saveDatabaseToParentLMS");
	var map = new Map();

  var node = this.cam.resources.getFirstLeaf();
	while (node != null && node != this.cam.resources) {
	  if (node.isResourceElement) {
			switch (node.cmi_core_lesson_status) {
				case "passed"     : map.put(node.identifier, "p"); break;
				case "completed"  : map.put(node.identifier, "c"); break;
				case "failed"     : map.put(node.identifier, "f"); break;
				case "incomplete" : map.put(node.identifier, "i"); break;
				case "browsed"    : map.put(node.identifier, "b"); break;
				case "not attempted" :
				default : break;
			}
		}
	  node = node.getNextNode();
	}

	// Note: The following statements use the variable 'parentstub' defined in lmsparentstub.js
	parentstub.LMSSetValue("cmi.core.lesson_location", map.exportToStringDelim("."));
	var errorCode = parentstub.LMSGetLastError();
	if (errorCode != "0") {
	  logger.write(logger.INTERNAL_FAILURE,"LMSAPI_loadDatabaseFromParentLMS failed\nerrorCode="+errorCode);
	}

	/*
        // If the organization consists of only one item element,
	// we store the lesson location as well.
	if (this.cam.organizations.getFirstLeaf() == this.cam.organizations.getLastLeaf()) {
            node = this.cam.organizations.getFirstLeaf().getResource();
	  setCookie("TinyLMS."+this.cam.identifier+"."+this.getValue("cmi.core.student_id")+".loc", node.cmi_core_lesson_location, expires);
	}*/
}
/**
 * Resets the database of the LMS for the current user.
 * The database is stored in the resource objects of the this.cam data model.
 */
function LMSAPI_resetDatabase() {
  var node = this.cam.resources.getFirstLeaf();
	while (node != null && node != this.cam.resources) {
	  if (node.isResourceElement) {
			node.cmi_core_lesson_location = ""; // "cmi.core.lesson_location" is used by lmsapi.js
			node.cmi_core_credit = "credit"; // "cmi.core.credit" is used by lmsapi.js
			node.cmi_core_lesson_status = "not attempted"; // "cmi.core.lesson_status" is used by lmsapi.js
			node.cmi_core_entry = ""; // "cmi.core.entry" is used by lmsapi.js
			node.cmi_core_score_raw = ""; // "cmi.core.score.raw" is used by lmsapi.js
			node.cmi_core_total_time = "0000:00:00.00"; // "cmi.core.total_time" is used by lmsapi.js
			node.cmi_core_exit = ""; // "cmi.core.exit" is used by lmsapi.js
			node.cmi_core_session_time = ""; // "cmi.core.session_time" is used by lmsapi.js
			node.cmi_suspend_data = ""; // "cmi.suspend_data" is used by lmsapi.js
		}
	  node = node.getNextNode();
	}
}
/**
 * Resets the database of the LMS for the current user
 * and erases his/her contents of the persistent storage.
 * The database is stored in the resource objects of the this.cam data model.
 */
function LMSAPI_clearDatabase() {
  this.resetDatabase();
	
	var expires = new Date();
	expires.setYear(expires.getYear() + 1);
	setCookie("TinyLMS."+this.cam.identifier+"."+this.getValue("cmi.core.student_id"), "", expires);
	setCookie("TinyLMS."+this.cam.identifier+"."+this.getValue("cmi.core.student_id")+".loc", "", expires);
}


/**
 * Initializes communication with the LMS.
 *
 * @return Returns the String "true" if the initialization was successful, or
 * "false" if the initialization failed.
 */
function LMSAPI_LMSInitialize(anEmptyString) {
  logger.write(logger.API,"api.LMSInitialize("+anEmptyString+")");
	var initializeCalledMoreThanOnce = false;

  // This function requires one argument.
  if (LMSAPI_LMSInitialize.arguments.length != 1) {
	this.lastError = this.ERROR_INVALID_ARGUMENT;
	logger.write(logger.API_FAILURE,"api.LMSInitialize():false reason:invalid argument count");
	return "false";
  }

  // The String must be null or empty
  if (anEmptyString != null && anEmptyString != "") {
	this.lastError = this.ERROR_INVALID_ARGUMENT;
	  logger.write(logger.API_FAILURE,"api.LMSInitialize():false reason:invalid argument="+anEmptyString);
	return "false";
  }

  // LMSInitialize must be called only once per SCO
	// Note: We still do the initialize the SCO as requested, but
	//       we return "false" and report an error.
	//       We have to do this, because if one SCO
	//       'forgets' to call LMSFinish, we still have to make
	//       sure, that subsequent SCO's are able initialize.
	//       If we were overly strict here, the LMS would just 
	//       'hang'.
  if (this.scoState != this.STATE_NOT_INITIALIZED) {
		initializeCalledMoreThanOnce = true;
  }
  
  // User must be logged in
  if (this.getValue("cmi.core.student_id") == null) {
    this.gotoLogin();
		this.lastError = this.ERROR_GENERAL;
	  logger.write(logger.API_FAILURE,"api.LMSInitialize():false reason:not logged in");
	 	return "false";
  }
   
	// Determine the current SCO and the current organization item
  var href = window.lmsContentFrame.location.href;
	var p = href.indexOf(this.coursePath);
	if (p == -1) {
	  logger.write(logger.API_FAILURE,"api.LMSInitialize()\nFATAL ERROR: "+href+"\n must be in the course/ folder.");
		return "false";
	}
	href = href.substring(p+7);
	//if (this.anticipatedItem != null && this.anticipatedItem != null && this.anticipatedItem.getHRef() == href) {
	if (this.anticipatedItem != null) {
	    this.currentItem = this.anticipatedItem;
		this.currentSCO = this.anticipatedItem.getResource();
		/*
		this.currentSCO = this.cam.resources.findByHRef(href);
		if (this.anticipatedItem.getResource() == this.currentSCO) {
		  this.currentItem = this.anticipatedItem
		} else {
			this.currentItem = this.cam.organizations.findByIdentifierref(this.currentSCO.identifier);
		}*/
	} else {
		this.currentSCO = this.cam.resources.findByHRef(href);
		this.currentItem = this.cam.organizations.findByIdentifierref(this.currentSCO.identifier);
	}
	this.anticipatedItem = null;
	 
  // Read the CMI data model for the sco
	this.readCMIdataModel(this.currentItem);


  this.scoState = this.STATE_INITIALIZED;
  this.lastError = this.NO_ERROR;
	
	var currentOrganization = this.getCurrentOrganization();
	var columnItem = currentOrganization.getColumnOfItem(this.currentItem);
	if (columnItem != null) this.setCurrentColumnName(columnItem.title);
	
	this.fireUpdateTOC();

	
  // LMSInitialize must be called only once per SCO   
  if (initializeCalledMoreThanOnce) {
    this.lastError = this.ERROR_GENERAL;
	logger.write(logger.API_FAILURE,"api.LMSInitialize():false reason: initialize called more than once");
    return "false";
  } else {
	logger.write(logger.MILESTONE, "Item initialized. item("+this.currentItem.identifier+")=\""+this.currentItem.title+"\",  resource("+this.currentSCO.identifier+")=\""+this.currentSCO.href+"\"");
    logger.write(logger.API_SUCCESS,"api.LMSInitialize():true");
    return "true";
	}
}

/**
 * Closes communication with the LMS.
 *
 * @return Returns the String "true" if successful, "false" if failed.
 */
function LMSAPI_LMSFinish(anEmptyString) {
  logger.write(logger.API,"api.LMSFinish("+anEmptyString+")");

	// Fail if the LMS is not initialized
	if (this.scoState != this.STATE_INITIALIZED) {
		this.lastError = this.ERROR_NOT_INITIALIZED;
		logger.write(logger.API_FAILURE,"api.LMSFinish("+anEmptyString+"):false reason:state="+this.scoState);
		return "false";
	}

  // This function requires one argument.
  if (LMSAPI_LMSFinish.arguments.length != 1) {
	this.lastError = this.ERROR_INVALID_ARGUMENT;
	logger.write(logger.API_FAILURE,"api.LMSFinish():false reason:invalid argument count");
	return "false";
  }

  // The String must be null or empty
  if (anEmptyString != null && anEmptyString != "") {
	this.lastError = this.ERROR_INVALID_ARGUMENT;
	return "false";
  }


  if (this.scoState != this.STATE_INITIALIZED) {
    this.lastError = this.ERROR_NOT_INITIALIZED;
		
  	logger.write(logger.API_FAILURE,"api.LMSFinish():false reason:state="+this.scoState);
		return "false";
  }

	var exit = this.getValue("cmi.core.exit");
	if (exit == "") {
    this.setValue("cmi.core.entry", "");
  } else if (exit == "time-out") {
    this.setValue("cmi.core.entry", "");
  } else if (exit == "suspend") {
    this.setValue("cmi.core.entry", "resume");
  } else if (exit == "logout") {
    this.setValue("cmi.core.entry", "");
  } else {
    this.setValue("cmi.core.entry", "");
  }
  if (this.getValue("cmi.core.lesson_status") == "not attempted") {
	  this.setValue("cmi.core.lesson_status","browsed");
	}

	var sessionTime = this.getValue("cmi.core.session_time");
	if (sessionTime != null && sessionTime.length == 13) {
		this.setValue("cmi.core.total_time",
			this.addTimespan(this.getValue("cmi.core.total_time"), sessionTime)
		);
	}
	
  this.LMSCommit("");
  this.lastError = this.NO_ERROR;
  this.scoState = this.STATE_NOT_INITIALIZED;
	this.currentSCO = null;
	var finishedItem = this.currentItem;
	this.currentItem = null; 
   
  if (exit == "logout") {
    this.setValue("cmi.core.student_id", null);
    this.setValue("cmi.core.student_name", null);
	 	this.gotoLogin();
  } else {
	
		if (this.mode == this.MODE_COURSE) {
		  // Do automatic sequencing in Course mode
		  if (this.anticipatedItem == null && this.isAutomaticSequencing) {
			 this.gotoItemAfter(finishedItem);
		  }
		} else {
		   this.fireUpdateTOCLater();
		}
	}
	
	 
	logger.write(logger.API_SUCCESS,"api.LMSFinish():true");	
	logger.write(logger.INFO, "Item finished. Item("+finishedItem.identifier+"):\""+finishedItem.title+"\"");
  	return "true";
}

/**
 * Gets a CMI data model value from the LMS.
 *
 * @param  key  String representing the cmi data model defined category or
 *             element (e.g. cmi.core.student_id)
 *
 * @return The value presently assigned by the LMS to the cmi data model
 *       element defined by the element or category identified by the name
 *       input value.
 */
function LMSAPI_LMSGetValue(key) {
	logger.write(logger.API,"api.LMSGetValue("+key+")");
	if (this.scoState != this.STATE_INITIALIZED) {
		this.lastError = this.ERROR_NOT_INITIALIZED;
		logger.write(logger.API_FAILURE,"api.LMSGetValue("+key+"):'' reason:state="+this.scoState);
		return "";
	}

  // This function requires one argument.
  if (LMSAPI_LMSGetValue.arguments.length != 1) {
	this.lastError = this.ERROR_INVALID_ARGUMENT;
	logger.write(logger.API_FAILURE,"api.LMSGetValue():false reason:invalid argument count");
	return "false";
  }

   
    if (key == null) {
		this.lastError = this.ERROR_INVALID_ARGUMENT;
		return "";
	}
   
	for (var i=0; i < this.dataModel.length; i++) {
		if (this.dataModel[i].key == key) {
			if (! this.dataModel[i].isReadable) {
				this.lastError = this.ERROR_ELEMENT_IS_WRITE_ONLY;
				logger.write(logger.API_FAILURE,"api.LMSGetValue("+key+"):'' reason:element is write only");
				return "";
			} else {
				this.lastError = this.NO_ERROR;
				logger.write(logger.API_SUCCESS,"api.LMSGetValue("+key+"):"+this.dataModel[i].value);
				return this.dataModel[i].value; 
			}
		}
	}
	
	// Provide detailled error messages.
	var p = key.lastIndexOf("._children");
	if (p != -1) {
	  var key2 = key.substring(0, p);
  	  for (var i=0; i < this.dataModel.length; i++) {
		if (this.dataModel[i].key == key2) {
		  this.lastError = this.ERROR_ELEMENT_CANNOT_HAVE_CHILDREN;
 		  logger.write(logger.API_FAILURE,"api.LMSGetValue("+key+"):'' reason:cannot have children");
		  return "";
		}
	  }
	} 
	var p = key.lastIndexOf("._count");
	if (p != -1) {
	  var key2 = key.substring(0, p);
  	  for (var i=0; i < this.dataModel.length; i++) {
		if (this.dataModel[i].key == key2) {
		  this.lastError = this.ERROR_ELEMENT_IS_NOT_AN_ARRAY;
 		  logger.write(logger.API_FAILURE,"api.LMSGetValue("+key+"):'' reason:not an array");
		  return "";
		}
	  }
	} 
	
	this.lastError = this.ERROR_NOT_IMPLEMENTED;
	logger.write(logger.API_FAILURE,"api.LMSGetValue("+key+"):'' reason:not implemented");
	return "";
}
/**
 * Gets a CMI data model value from the LMS.
 * This function is for internal use only.
 *
 * @param  key  String representing the cmi data model defined category or
 *             element (e.g. cmi.core.student_id)
 *
 * @return The value presently assigned by the LMS to the cmi data model
 *       element defined by the element or category identified by the name
 *       input value.
 */
function LMSAPI_getValue(key) {
	logger.write(logger.INTERNAL,"lms.getValue("+key+")");
  var result = null;
  for (var i=0; i < this.dataModel.length; i++) {
    if (this.dataModel[i].key == key) result = this.dataModel[i].value; 
  }
  logger.write(logger.INTERNAL_SUCCESS,"lms.getValue("+key+"):"+result);
  return result;
}


/**
 * Sets a CMI data model value in the LMS.
 *
 * @param key String representing the data model defined category or element.
 * @param value The value that the named element or category will be assigned.
 *
 * @return Returns the String "true" if successful, "false" if failed.
 */
function LMSAPI_LMSSetValue(key, value) {
	logger.write(logger.API,"api.LMSSetValue("+key+","+value+")");
	
	// Fail if the LMS is not initialized
	if (this.scoState != this.STATE_INITIALIZED) {
		this.lastError = this.ERROR_NOT_INITIALIZED;
			logger.write(logger.API_FAILURE,"api.LMSSetValue("+key+","+value+"):false reason:state="+this.scoState);
		return "false";
	}
	
	// This function requires two arguments.
	if (LMSAPI_LMSSetValue.arguments.length != 2) {
		this.lastError = this.ERROR_INVALID_ARGUMENT;
		logger.write(logger.API_FAILURE,"api.LMSSetValue():false reason:invalid argument count");
		return "false";
	}

   
	// Search for the data model entry
	for (var i=0; i < this.dataModel.length; i++) {
	
		// We found the data model entry!
		if (this.dataModel[i].key == key) {
		
			// Fail if the SCO may not write the data model entry
			if (! this.dataModel[i].isWriteable) {
				this.lastError = this.ERROR_ELEMENT_IS_READ_ONLY;
					logger.write(logger.API_FAILURE,"api.LMSSetValue("+key+","+value+"):false reason:element is read only");
				return "false";
			
			// Special treatment for the CMIVocabulary type	 
			} else if (this.dataModel[i].type == "CMIVocabulary") {
				for (var j=0; j < this.dataModel[i].vocabulary.length; j++) {
					if (this.dataModel[i].vocabulary[j] == value) {
						this.lastError = this.NO_ERROR;
						this.dataModel[i].value = value;
							logger.write(logger.API_SUCCESS,"api.LMSSetValue("+key+","+value+"):true");
						this.writeCMIdataModel(this.currentItem);
						this.saveDatabase();
						return "true";
					}
				}
				this.lastError = this.ERROR_INCORRECT_DATA_TYPE;
					logger.write(logger.API_FAILURE,"api.LMSSetValue("+key+","+value+"):false reason:invalid CMIVocabulary value");
				return "false";
			 
			// Special treatment for the CMIString255 type	 
			} else if (this.dataModel[i].type == "CMIString255") {
			    if (value.length > 255) {
  					this.lastError = this.ERROR_INCORRECT_DATA_TYPE;
						logger.write(logger.API_FAILURE,"api.LMSSetValue("+key+","+value+"):false reason:invalid CMIString255 value");
					return "false";
				}

			// Special treatment for the CMIString4096 type	 
			} else if (this.dataModel[i].type == "CMIString4096") {
			    if (value.length > 4096) {
  					this.lastError = this.ERROR_INCORRECT_DATA_TYPE;
						logger.write(logger.API_FAILURE,"api.LMSSetValue("+key+","+value+"):false reason:invalid CMIString255 value");
					return "false";
				}

			// Special treatment for the CMIInteger type
			} else if (this.dataModel[i].type == "CMIDecimal") {
			    if (! /^-?(\d+|\d*\.\d+)$/.test(value)
				|| value < this.dataModel[i].vocabulary[0] 
				|| value > this.dataModel[i].vocabulary[1]) {
  					this.lastError = this.ERROR_INCORRECT_DATA_TYPE;
						logger.write(logger.API_FAILURE,"api.LMSSetValue("+key+","+value+"):false reason:invalid CMIInteger value");
					return "false";
				}
			 
			// Special treatment for the CMITimespan type HHHH:MM:SS.SS	 
			} else if (this.dataModel[i].type == "CMITimespan") {
			    if (! /^\d{2,4}:\d{2}:\d{2}(\.\d{1,2})?$/.test(value)) {
  					this.lastError = this.ERROR_INCORRECT_DATA_TYPE;
						logger.write(logger.API_FAILURE,"api.LMSSetValue("+key+","+value+"):false reason:invalid CMITimespan value");
					return "false";
				}
			} 

			// Set the value 
			this.lastError = this.NO_ERROR;
			this.dataModel[i].value = value;
			logger.write(logger.API_SUCCESS,"api.LMSSetValue("+key+","+value+"):true");
			this.writeCMIdataModel(this.currentItem);
			this.saveDatabase();
			return "true"; 
		}
	}
	this.lastError = this.ERROR_NOT_IMPLEMENTED;
	logger.write(logger.API_FAILURE,"api.LMSSetValue("+key+","+value+"):false reason:not implemented");
	return "false";
}
/**
 * Sets a CMI data model value in the LMS.
 * This function is for internal use only.
 *
 * @param key String representing the data model defined category or element.
 * @param value The value that the named element or category will be assigned.
 *
 * @return Returns the String "true" if successful, "false" if failed.
 */
function LMSAPI_setValue(key, value) {
	logger.write(logger.INTERNAL,"lms.setValue("+key+","+value+")");
	for (var i=0; i < this.dataModel.length; i++) {
		if (this.dataModel[i].key == key) {
			this.dataModel[i].value = value; 
  			logger.write(logger.INTERNAL_SUCCESS,"lms.setValue("+key+","+value+"):true");
			return "true";
		}
	}
	
	logger.write(logger.INTERNAL_FAILURE,"lms.setValue("+key+","+value+"):false reason:invalid key");
	return "false";
}
/**
 * Sets a CMI data model value in the LMS to its default value.
 * This function is for internal use only.
 *
 * @param key String representing the data model defined category or element.
 *
 * @return Returns the String "true" if successful, "false" if failed.
 */
function LMSAPI_setValueToDefault(key) {
	logger.write(logger.INTERNAL,"lms.setValueToDefault("+key+")");
	for (var i=0; i < this.dataModel.length; i++) {
		if (this.dataModel[i].key == key) {
			this.dataModel[i].value = this.dataModel[i].defaultValue; 
			logger.write(logger.INTERNAL_SUCCESS,"lms.setValueToDefault("+key+") value="+this.dataModel[i].defaultValue);
			return "true";
		}
	}
	logger.write(logger.INTERNAL_FAILURE,"lms.setValueToDefault("+key+"):false reason:invalid key");
	return "false";
}

/**
 * Displays debugging info about the current SCO.
 */
function LMSAPI_showBugInfo() {
	var currentOrganization = this.getCurrentOrganization();
	var currentItem = this.getCurrentItem();
	var currentSCO = (currentItem == null) ? null : currentItem.getResource();
	
	if (currentItem == null) {
		alert(
		"TinyLMS "+this.version+"\n\n"
		+"Course: "+this.cam.identifier+" "+this.cam.version+"\n"
		+"Organization: "+currentOrganization.title+" "+currentOrganization.identifier+"\n"
		);
	} else {
		alert(
		"TinyLMS "+this.version
		+"Course: "+this.cam.identifier+" "+this.cam.version+"\n"
		+"\nOrganization: "+currentOrganization.title+" "+currentOrganization.identifier
		+"\nItem ("+currentItem.identifier+"): "+currentItem.title
		+"\nResource ("+currentSCO.identifier+"): "+currentSCO.href
		+"\n\nCMI Data:"
		+"\n  cmi.core.entry: "+this.getValue("cmi.core.entry")
		+"\n  cmi.core.exit: "+this.getValue("cmi.core.exit")
		+"\n  cmi.core.credit: "+this.getValue("cmi.core.credit")
		+"\n  cmi.core.lesson_status: "+this.getValue("cmi.core.lesson_status")
		+"\n  cmi.core.lesson_location: "+this.getValue("cmi.core.lesson_location")
		+"\n  cmi.core.score.raw: "+this.getValue("cmi.core.score.raw")
		+"\n  cmi.core.session_time: "+this.getValue("cmi.core.session_time")
		+"\n  cmi.core.total_time: "+this.getValue("cmi.core.total_time")
		+"\n  cmi.suspend_data: "+this.getValue("cmi.suspend_data")
		);
	}
}

/**
 * Commits changes.
 *
 * @return Returns the String "true" if successful, "false" if failed.
 */
function LMSAPI_LMSCommit(anEmptyString) {
	logger.write(logger.API,"api.LMSCommit('"+anEmptyString+"')");
	
	if (this.scoState != this.STATE_INITIALIZED) {
		this.lastError = this.ERROR_NOT_INITIALIZED;
		logger.write(logger.API_FAILURE,"api.LMSCommit():false reason:state="+this.scoState);
		return "false";
	}
  // This function requires one argument.
  if (LMSAPI_LMSCommit.arguments.length != 1) {
	this.lastError = this.ERROR_INVALID_ARGUMENT;
	logger.write(logger.API_FAILURE,"api.LMSCommit():false reason:invalid argument count");
	return "false";
  }
  // The argument must be an empty string.
  if (anEmptyString != "") {
	this.lastError = this.ERROR_INVALID_ARGUMENT;
	logger.write(logger.API_FAILURE,"api.LMSCommit():false reason:invalid argument:"+anEmptyString);
	return "false";
  }
	
	this.writeCMIdataModel(this.currentItem);
	this.saveDatabase();
	
   
	this.lastError = this.NO_ERROR;
	logger.write(logger.API_SUCCESS,"api.LMSCommit():true");
	return "true";
}


/**
 * Returns the error code that was set by the last LMS function call as an integer number.
 */
function LMSAPI_LMSGetLastError() {
  logger.write(logger.API,"api.LMSGetLastError()");
  logger.write(logger.API_SUCCESS,"api.LMSGetLastError():"+this.lastError);
  
  return this.lastError;
}

/**
 * Returns the textual representation of an error code.
 * @param errorCode A String representing an error code.
 * @return The textual description that corresponds to the input error code.
 */
function LMSAPI_LMSGetErrorString(errorCode) {
  logger.write(logger.API,"api.LMSGetErrorString("+errorCode+")");

    if (errorCode == null) {
		errorCode = this.lastError;
	}
    for (var i=0; i < this.ERROR_MATRIX.length; i++) {
      if (errorCode == this.ERROR_MATRIX[i][this.ERROR_MATRIX_KEY]) {
 	 		  logger.write(logger.API_SUCCESS,"api.LMSGetErrorString("+errorCode+"):'"+this.ERROR_MATRIX[i][this.ERROR_MATRIX_VALUE]+"'");
			  return this.ERROR_MATRIX[i][this.ERROR_MATRIX_VALUE];
		  }
    }

  // Return No Error if we don't know the error.
  logger.write(logger.API_SUCCESS,"api.LMSGetErrorString("+errorCode+"):No Error");
  return "No Error"; 
}

/**
 * Returns the vendor specific textual representation of an error code.
 *
 * @param  errorCode - Error Code(String), or null
 * @return The vendor specific textual description that corresponds to the
 *         input error code
 */
function LMSAPI_LMSGetDiagnostic(errorCode) {
	logger.write(logger.API,"api.LMSGetDiagnostic("+errorCode+")");
	
	if (this.scoState != this.STATE_INITIALIZED) {
		this.lastError = this.ERROR_NOT_INITIALIZED;
		logger.write(logger.API_FAILURE,"api.LMSGetDiagnostic("+errorCode+"):'no diagnostic' reason:state="+this.scoState);
		return "no diagnostic";
	}
	
	if (errorCode == null) {
		errorCode = this.lastError;
	}
	
	for (var i=0; i < this.ERROR_MATRIX.length; i++) {
		if (errorCode == this.ERROR_MATRIX[i][this.ERROR_MATRIX_KEY]) {
			logger.write(logger.API_SUCCESS,"api.LMSGetDiagnostic("+errorCode+"):"+this.ERROR_MATRIX[i][this.ERROR_MATRIX_VALUE]);
			return this.ERROR_MATRIX[i][this.ERROR_MATRIX_VALUE];
		}
	}
	logger.write(logger.API_FAILURE,"api.LMSGetDiagnostic("+errorCode+"):'unknown error code'");
	return "unknown error code"; 
}

/**
 * Starts the LMS.
 * This function is for internal use only.
 */
function LMSAPI_start() {
  logger.write(logger.INTERNAL,"lms.start()");

  if (this.started == false) {
    this.started = true;

    // Determine the top level URL 
		// Remove the filename of the frameset at the end, but preserve
		// the final slash.
		this.topURL = window.location.href;
		this.topURL = this.topURL.substring(0, this.topURL.lastIndexOf("/")+1);

     // Set all data to default values
     for (var i=0; i < this.dataModel.length; i++) {
	     this.dataModel[i].value = this.dataModel[i].defaultValue;
     }
  }
}
/**
 * Returns the current CAM item.
 * This function is for internal use only.
 */
function LMSAPI_getCurrentItem() {
	return this.currentItem;
}
/**
 * Returns the anticipated CAM item.
 * The LMS anticipates a CAM item when the user selects the item
 * in the TOC.
 * This function is for internal use only.
 */
function LMSAPI_getAnticipatedItem() {
	var href = window.lmsContentFrame.location.href;
	if (this.anticipatedItem != null) return this.anticipatedItem;
	else if (this.currentItem != null && href == this.currentItem.href) return this.currentItem;
	else return this.cam.organizations.findByHRef(href);
}
/**
 * Returns the current CAM organization.
 * This function is for internal use only.
 */
function LMSAPI_getCurrentOrganization() {
	if (this.currentItem == null) {
	  return this.cam.organizations.getChildAt(0);
	} else {
	  var parent = this.currentItem.getParent();
		while (parent != null && ! parent.isOrganizationElement) {
		  parent = parent.getParent();
		}
		return parent;
	}
}
/**
 * Returns the current column name (aka layer name).
 * Returns null if no layer is active.
 * This function is for internal use only.
 * @return One of the String's contained in LMSCAMcolumns in lmscam.js
 * or null.
 */
function LMSAPI_getCurrentColumnName() {
  return this.currentColumnName;
}
/**
 * Sets the current column name (aka layer name).
 * Set this to null, if no layer should be active.
 * This function is for internal use only.
 *
 * @param column This must be one of the String's contained in
 * LMSCAMcolumns in lmscam.js or null.
 */
function LMSAPI_setCurrentColumnName(column) {
  this.currentColumnName = column;
}
/**
 * Returns the first CAM item or null if there are no items.
 * This function is for internal use only.
 */
function LMSAPI_getFirstItem() {
  logger.write(logger.INTERNAL,"lms.geFirstItem()");
  var node = this.cam.organizations.getFirstLeaf();
	while (node != null && node.isItemElement && node.getHRef() == null) {
	 node = node.getNextNode();
	}
	return node;
}
/**
 * Returns the next CAM item or null if there is no next item.
 * This function is for internal use only.
 * It is used for sequencing of hierarchical organization structures.
 */
function LMSAPI_getNextItemOf(itemElement) {
 	logger.write(logger.INTERNAL,"lms.getNextItemOf("+itemElement.title+")");
	var node = itemElement;
 	do { 
		node = node.getNextNode();
	} while (node != null && (! node.isItemElement || node.getHRef() == null));
	return node;
}
/**
 * Returns the previous CAM item or null if there is no previous item.
 * This function is for internal use only.
 * It is used for sequencing of hierarchical organization structures.
 */
function LMSAPI_getPreviousItemOf(itemElement) {
 	logger.write(logger.INTERNAL,"lms.getPreviousItemOf("+itemElement.title+")");
	var node = itemElement;
	do {
		node = node.getPreviousNode();
	} while (node != null && node.isItemElement && node.getHRef() == null);
	return node;
}
/**
 * Returns the CAM item on the next row, or null if there is no next row.
 * This function is for internal use only.
 * It is used for sequencing of layered orgnization structures.
 */
function LMSAPI_getNextRowOf(itemElement) {
	var organization = this.getCurrentOrganization();
	var column = organization.getColumnOfItem(itemElement);

	if (column == null) {
	  return (itemElement == null) ? null : this.getNextItemOf(itemElement);
	} else {
		var node = itemElement;
		var nodeColumn = null;
	
		do {
			node = node.getNextNode();
			if (node.isItemElement) {
				nodeColumn = organization.getColumnOfItem(node);
			}
		} while (
				node != null 
				&& node.isItemElement 
				&& (node.getHRef() == null 
						|| nodeColumn == null 
						|| nodeColumn.title != column.title
						)
		);
	
		if (node != null && node.isItemElement ) {
	  	return node;
		} else {
	  	return null;
		}
	}
}
/**
 * Returns the CAM item on the previous row, or null if there is no
 * previous row.
 * This function is for internal use only.
 * It is used for sequencing of layered orgnization structures.
 */
function LMSAPI_getPreviousRowOf(itemElement) {
	var organization = this.getCurrentOrganization();
	var column = organization.getColumnOfItem(itemElement);

	//var rowItem = currentOrganization.getRowOfItem(currentItem);
	
	if (column == null) {
	  return (itemElement == null) ? null : this.getPreviousItemOf(itemElement);
	} else {
		var node = itemElement;
		var nodeColumn = null;
	
		do {
			node = node.getPreviousNode();
			if (node.isItemElement) {
				nodeColumn = organization.getColumnOfItem(node);
			}
		} while (
				node != null 
				&& node.isItemElement 
				&& (node.getHRef() == null 
						|| nodeColumn == null 
						|| nodeColumn.title != column.title
						)
		);
	
		if (node != null && node.isItemElement ) {
	  	return node;
		} else {
	  	return null;
		}
	}
}
/**
 * Goes to the first CAM item.
 * This function is for internal use only.
 */
function LMSAPI_gotoFirstItem() {
  logger.write(logger.INTERNAL,"lms.gotoFirstItem()");
  if (this.getValue("cmi.core.student_id") == null) {
		this.gotoLogin();
	} else {
		var node = this.getFirstItem();
		if (node == null) {
			logger.write(logger.INTERNAL_FAILURE,"lms.gotoFirstItem first=null");
		  window.lmsContentFrame.location.href='about:blank';
		} else {
		  this.anticipatedItem = node;
		  logger.write(logger.INTERNAL_SUCCESS,"lms.gotoFirstItem first="+node.getHRef());
		  window.lmsContentFrame.location.href=this.topURL+this.coursePath+node.getHRef();
		}
		//this.fireUpdateTOC();
	}
}
/**
 * Resumes the course at the first incompleted CAM item.
 * This function is for internal use only.
 */
function LMSAPI_gotoResumeItem() {
	logger.write(logger.INTERNAL,"lms.gotoResumeItem()");
	if (this.getValue("cmi.core.student_id") == null) {
		this.gotoLogin();
	} else {
		var node = this.getFirstItem();
		if (this.isAutomaticSequencing) {
			if (node == null) {
				logger.write(logger.INTERNAL_FAILURE,"lms.gotoFirstItem first=null");
				window.lmsContentFrame.location.href='about:blank';
			} else {
				var firstNode = node;
				while (node != null) {
					if (node.isItemElement && node.getResource() != null 
						&& (node.getResource().cmi_core_lesson_status == "not attempted"
							|| node.getResource().cmi_core_lesson_status == "failed"
							|| node.getResource().cmi_core_lesson_status == "incomplete"
							|| node.getResource().cmi_core_lesson_status == "browsed"
							)
						) {
						
						break;
					}
					if (this.organizationStructure == this.LAYERED_STRUCTURE) {
						node = this.getNextRowOf(node);
					} else {
						node = this.getNextItemOf(node);
					}
				}
				if (node == null) node = firstNode;			
			}
		}
		this.anticipatedItem = node;
		logger.write(logger.INTERNAL_SUCCESS,"lms.gotoResumeItem resume="+node.getHRef());
		window.lmsContentFrame.location.href=this.topURL+this.coursePath+node.getHRef();
		//this.fireUpdateTOC();
	}
}
/**
 * Goes to the next CAM item.
 * This function is for internal use only.
 */
function LMSAPI_gotoNextItem() {
 	logger.write(logger.INTERNAL,"lms.gotoNextItem()");
	this.gotoItemAfter(this.currentItem);
}
/**
 * Goes to the next CAM item after the specified item.
 * Goes to the specified item if it is the last item.
 * This function is for internal use only.
 */
function LMSAPI_gotoItemAfter(anItem) {
 	logger.write(logger.INTERNAL,"lms.gotoNextItemAfter("+anItem+")");
  if (this.getValue("cmi.core.student_id") == null) this.gotoLogin();
  else {
		var node = anItem;
		if (node == null || ! node.isItemElement) this.gotoFirstItem(); 
		else {
		  node = this.getNextItemOf(node);
		  if (this.currentItem != null && node != null) {
			logger.write(logger.INTERNAL_SUCCESS,"lms.gotoNextItem() currentItem="+this.currentItem+"@"+this.currentItem.identifier+"\nanticipatedItem="+node+"@"+node.identifier);
			} else {
			logger.write(logger.INTERNAL_SUCCESS,"lms.gotoNextItem() currentItem="+this.currentItem+"\nanticipatedItem="+node);
			}
			if (node == null || ! node.isItemElement) {
				this.gotoItem(anItem);
			} else {
			  this.gotoItem(node);
			}
		}
  }
}
/**
 * Goes to the previous CAM item.
 * This function is for internal use only.
 */
function LMSAPI_gotoPreviousItem() {
 	logger.write(logger.INTERNAL,"lms.gotoPreviousItem()");
	this.gotoItemBefore(this.currentItem);
}
/**
 * Goes to the CAM item before the specified item.
 * Goes to the specified item, if it is the first item.
 * This function is for internal use only.
 */
function LMSAPI_gotoItemBefore(anItem) {
  if (this.getValue("cmi.core.student_id") == null) this.gotoLogin();
  else {
		var node = anItem;
		if (node == null) this.gotoFirstItem(); 
		else {
		  node = this.getPreviousItemOf(node);
			if (node == null || ! node.isItemElement) {
				this.gotoItem(anItem);
			} else {
			   this.gotoItem(node);
			}
		}
  }
}
/**
 * Goes to the CAM item with the specified id.
 * This function is for internal use only.
 */
function LMSAPI_gotoItemWithID(itemElementID) {
 	logger.write(logger.INTERNAL,"lms.gotoPreviousItem()");
  if (this.getValue("cmi.core.student_id") == null) this.gotoLogin();
	else {
	  var node = this.cam.organizations.findByIdentifier(itemElementID);
		if (node != null) {
		  this.gotoItem(node);
		} else {
		  //this.gotoMenu();
		}
	}
}
/**
 * Goes to the specified CAM item element
 * This function is for internal use only.
 */
function LMSAPI_gotoItem(itemElement) {
  this.mode = this.MODE_COURSE;
 	logger.write(logger.INTERNAL,"lms.gotoItem("+itemElement+")");
  if (this.getValue("cmi.core.student_id") == null) this.gotoLogin();
	else {
	  this.anticipatedItem = itemElement;
		if (itemElement.parameters != null) {
		  if (itemElement.parameters.length > 0 && itemElement.parameters.charAt(0) == '?') {
		 		window.lmsContentFrame.location.href=this.topURL+this.coursePath+itemElement.getHRef()+itemElement.parameters;
			} else {
		 		window.lmsContentFrame.location.href=this.topURL+this.coursePath+itemElement.getHRef()+'?'+itemElement.parameters;
			}
		} else {
	  	window.lmsContentFrame.location.href=this.topURL+this.coursePath+itemElement.getHRef();
		}
		//this.fireUpdateTOC();
	}
}
/**
 * Goes to the menu page.
 * This function is for internal use only.
 */
function LMSAPI_gotoMenu() {
	if (this.isLoggedIn()) {
		this.mode = this.MODE_ADMIN;
		
		logger.write(logger.INTERNAL,"lms.gotoMenu()");
		this.anticipatedItem = null;
		window.lmsContentFrame.location.href=this.topURL+"tinylms/lmsmenu.html";
		//this.fireUpdateTOC();
	}
}
/**
 * Logs out and goes to the login page.
 * This function is for internal use only.
 */
function LMSAPI_gotoLogin() {
    this.mode = this.MODE_LOGGED_OUT;
	logger.write(logger.INTERNAL,"lms.gotoLogin():");
	this.anticipatedItem = null;
  	window.lmsContentFrame.location.href=this.topURL+this.labels.get("login.url");
	//this.setValue("cmi.core.student_id", null); <- this is done by the login page
	
	// We have to use fireUpdateTOCLater) here, because some browsers seem to be unable to change
	// the content frame and the TOC frames at the same time.
  	this.fireUpdateTOCLater();
}

/**
 * Remove Leading and Trailing Characters.
 * When a character (such as a space) appears 
 * multiple consecutive times in the given string,
 * removes the repeated occurrences.
 * FIXME - Move this function into a separate .js file.
 */
function removeLeadingAndTrailingChar(inputString, removeChar) {
	var returnString = inputString;
	if (removeChar.length)	{
	  while(''+returnString.charAt(0)==removeChar) {
		  returnString=returnString.substring(1,returnString.length);
		}
		while(''+returnString.charAt(returnString.length-1)==removeChar) {
	    returnString=returnString.substring(0,returnString.length-1);
	  }
	}
	return returnString;
}

/**
 * Logs in and goes to the first CAM item.
 * This function is for internal use only.
 */
function LMSAPI_login(userid,password) {
  logger.write(logger.INTERNAL,"lms.login("+userid+",*****)");
	
	userid = removeLeadingAndTrailingChar(userid, ' ');
	if (userid.length == 0) userid = "Guest";

  var user = null;
	if (this.userMap.size() == 0) {
	  // An empty user map means, that we allow everybody to log in.
		// Thus we create a new user on the fly.
	  user = new User(userid, null, userid+",,");
	} else {
	  // If the user map is non-empty, we only allow registered users
		// to log in.
	  user = this.userMap.get(userid);
	  if (user == null) {
	    logger.write(logger.INTERNAL_FAILURE,this.labels.get("login.baduserid"));
		  return;
	  }
	  if (! user.isPasswordValid(password)) {
	    logger.write(logger.INTERNAL_FAILURE,this.labels.get("login.badpassword"));
		  return;
	  }
	}

 	this.setValue("cmi.core.student_id", userid);
 	this.setValue("cmi.core.student_name", user.name);
	this.currentUser = user;
	this.mode = this.MODE_COURSE;
	this.loadDatabase();
		
	// No layer is active after login
	// (This is used for layered structures).
	this.currentColumnName = null;
		
	this.gotoResumeItem();
}
/**
 * Returns true if the user is logged in, returns false otherwise.
 * This function is for internal use only.
 */
function LMSAPI_isLoggedIn() {
  logger.write(logger.INTERNAL,"lms.isLoggedIn()");
  var userid = this.getValue("cmi.core.student_id");
	return this.mode != this.MODE_LOGGED_OUT && userid != null && userid != "";
}
/**
 * Logs out and goes to the login page.
 * This function is for internal use only.
 */
function LMSAPI_logout() {
	logger.write(logger.INTERNAL,"lms.logout():");
	this.gotoLogin();
}
/**
 * Adds two values of type CMITimespan.
 * Returns the sum in CMITimespan format.
 */
function LMSAPI_addTimespan(a, b) {
	if (a == null || a.length != 13 ||
		b == null || b.length != 13) {
		logger.write(logger.INTERNAL_FAILURE, "Illegal arguments: LMSAPI_addTimespan("+a+","+b+")");
		return "0000:00:00.00";
	}
  var aElements = a.split(":");
  var bElements = b.split(":");


  // remove leading '0' characters from the timespan elements
  for (var i=0; i < aElements.length; i++) {
     while (aElements[i].length > 1 && aElements[i].charAt(0) == '0') {
	   aElements[i] = aElements[i].substr(1);
	 }
  }
  for (var i=0; i < bElements.length; i++) {
     while (bElements[i].length > 1 && bElements[i].charAt(0) == '0') {
	   bElements[i] = bElements[i].substr(1);
	 }
  }
  // Add the timespans
  var seconds = new Number(aElements[2]) + new Number(bElements[2]);
  var minutes = new Number(aElements[1]) + new Number(bElements[1]) + Math.floor(seconds / 60);
  var hours = new Number(aElements[0]) + new Number(bElements[0]) + Math.floor(minutes / 60);
  seconds = seconds - Math.floor(seconds / 60) * 60; 
  minutes = minutes % 60;

  seconds = seconds.toString();
  minutes = minutes.toString();
  hours = hours.toString();
  var p = seconds.indexOf(".");
  if (p != -1 && seconds.length > p + 2) seconds = seconds.substring(0, p + 3);	
  var result = ((hours.length < 2) ? "0" : "") + hours 
  + ((minutes.length < 2) ? ":0" : ":") + minutes 
  + ((p == -1 && seconds.length < 2 || p == 1) ? ":0" : ":") + seconds;
  
  return result;
}
/**
 * Adds a listener for the updateTOC() events after the TOC changes.
 * Does nothing if the listener is already registered.
 */
function LMSAPI_addTOCListener(key, listener) {
	//  this.tocListeners.put(key, listener);
}
/*
 * Removes a listener previously added with addTOCListener. 
 * Does nothing if the listener has not been registered.
 */
function LMSAPI_removeTOCListener(key) {
	 // this.tocListeners.remove(key);
}
/**
 * Sends updateTOC() to all registered TOCListeners.
 */
function LMSAPI_fireUpdateTOC() {
  if (window.lmsTOCFrame != null) window.lmsTOCFrame.location.reload();
  if (window.lmsNavFrame != null) window.lmsNavFrame.location.reload();
}
/**
 * Sends updateTOC() to all registered TOCListeners, after all pending events have been processed.
 */
function LMSAPI_fireUpdateTOCLater() {
   setTimeout("API.fireUpdateTOC()",1);
}

/**
 * Changes the behaviour of TinyLMS.
 * If TinyLMS is used as a SCORMAdapter, the parent LMS is used for persistence
 * storage. Otherwise, Cookies are used.
 * 
 * Note: Usign TinyLMS as a SCORMAdapter also requires inserting appropriate
 * API calls to the parent LMS on load and on unload of the Frameset of TinyLMS.
 */
function LMSAPI_setSCORMAdapter(aBoolean) {
    this.isSCORMAdapter = aBoolean;
    if (this.isSCORMAdapter) {
        this.loadDatabase = LMSAPI_loadDatabaseFromParentLMS;
        this.saveDatabase = LMSAPI_saveDatabaseToParentLMS;
    } else {
        this.loadDatabase = LMSAPI_loadDatabaseFromCookie;
        this.saveDatabase = LMSAPI_saveDatabaseToCookie;
    }
}

/**
 * Toggles logging on and off.
 */
function LMSAPI_toggleLogging() {
	if (logger.level > 0) {
		logger.level = 0;
		logger.close();
	} else {
		logger.level = this.loglevel;
		logger.open();
	}
}

/**
 * This is the constructor for a SCORM 1.2 Data Model Entry.
 *
 * Properties:
 *
 * key: The key of the data model entry.
 * accessiblity: A String containing the letters "g"=global, "r"=read, "w"=write
 * type: "CMIBlank", "CMIBoolean", "CMIDecimal", "CMIFeedback", "CMIIdentifier", "CMIInteger", "CMISInteger",
 *       "CMIString255", "CMIString4096", "CMITime", "CMITimespan", "CMIVocabulary"
 * vocabulary: Array containing all allowed members of a CMIVocabulary.
 *             For type CMIInteger, the vocabulary contains the minimal and maximal
 *             values.
 * defaultValue: The default value of the data model entry.
 * value: The value of the data model entry.
 */ 
function LMSDataModelEntry(key, accessibility, type, vocabulary, defaultValue) {
  this.key = key;
  this.accessibility = accessibility;
  this.type = type;
  this.vocabulary = vocabulary;
  this.defaultValue = defaultValue;
  this.value = defaultValue;
  
  this.isReadable = accessibility.indexOf("r") != -1;
  this.isWriteable = accessibility.indexOf("w") != -1;
  this.isGlobal = accessibility.indexOf("g") != -1; 
}


/**
 * This is the constructor for a SCORM 1.2 LMS API.
 */
function LMSAPI() {
    this.loglevel = 0;
	this.logger = logger; // we copy a reference to the logger here, for convenient access by the TOC scripts.
	this.showDebugButtons = false;
	this.showBugInfoButton = false;

	// The value of this property is set by the script imsmanifest.js.
	this.version = "unknown";
	
	// The CMI data model of the LMS
	this.dataModel = [
    new LMSDataModelEntry("cmi.core._children", "gr", "CMIString255", null, "student_id,student_name,lesson_location,credit,lesson_status,entry,score,total_time,exit,session_time"),
    new LMSDataModelEntry("cmi.core.student_id", "gr", "CMIIdentifier", null, null),
    new LMSDataModelEntry("cmi.core.student_name", "gr", "CMIString255", null, null),
    new LMSDataModelEntry("cmi.core.lesson_location", "rw", "CMIString255", null, ""),
    new LMSDataModelEntry("cmi.core.credit", "r", "CMIVocabulary", ["credit","no-credit"], "credit"),
	
	// Note: not attempted is not part of the vocabulary, although it is used as the initial value.
	// The reason for this is, that _only_ the LMS is allowed to set the value to not attempted.
	// If an SCO attempts to do this, it should get an error. 
    new LMSDataModelEntry("cmi.core.lesson_status", "rw", "CMIVocabulary", ["passed", "completed", "failed", "incomplete", "browsed" /*, "not attempted"*/], "not attempted"),
	
    new LMSDataModelEntry("cmi.core.entry", "r", "CMIVocabulary", ["ab-initio","resume",""], ""),
    new LMSDataModelEntry("cmi.core.score._children", "r", "CMIString255", null, "raw"),
    new LMSDataModelEntry("cmi.core.score.raw", "rw", "CMIDecimal", [0, 100], ""),
    new LMSDataModelEntry("cmi.core.total_time", "r", "CMITimespan", null, "0000:00:00.00"),
    new LMSDataModelEntry("cmi.core.exit", "w", "CMIVocabulary", ["time-out","suspend","logout",""], ""),
    new LMSDataModelEntry("cmi.core.session_time", "w", "CMITimespan", null, ""),
    new LMSDataModelEntry("cmi.suspend_data", "rw", "CMIString4096", null, ""),
    new LMSDataModelEntry("cmi.launch_data", "r", "CMIString4096", null, "")
  ];

	// If true: The LMS automatically navigates through the course
	// If false: The user does the navigation manually
	// The property is set by the imsmanifest.js script.
	this.isAutomaticSequencing = false;

	// The type of organization structure.
	this.HIERARCHICAL_STRUCTURE = 1;
	this.LAYERED_STRUCTURE = 2;
	this.organizationStructure = this.HIERARCHICAL_STRUCTURE;

	// The content aggregation model
	// These properties are set by the imsmanifest.js script.
	this.cam = null;
	this.camColumnNames = null;
	
	// The user map
	// The userMap property is set by the imsmanifest.js script.
	this.userMap = null; // this is an instance of collections.js Map.
	this.currentUser = null; // this is an instance of lmssecurity.js User.
	
	// The locale specific labels
	// The variable LMSLabels is set by one of the lmslabels_<languagecode>.js files.
	this.labels = null;
	
	// The current SCO and the current organization item
	this.currentSCO = null;
	this.currentItem = null;
	this.anticipatedItem = null;
	
	// Constants for Error handling
	this.ERROR_MATRIX_KEY = 0;
	this.ERROR_MATRIX_VALUE = 1;
	this.ERROR_MATRIX = [
		["", ""],
		["0", "No error"],
		["101", "General Exception"],
		["201", "Invalid argument error"],
		["202", "Element cannot have children"],
		["203", "Element not an array - Cannot have count"],
		["301", "Not initialized"],
		["401", "Not implemented error"],
		["402", "Invalid set value, element is a keyword"],
		["403", "Element is read only"],
		["404", "Element is write only"],
		["405", "Incorrect Data Type"]
	];
  
	// Define exception/error codes
	this.NO_ERROR = "0";
	this.ERROR_GENERAL = "101";
	this.ERROR_INVALID_ARGUMENT = "201";
	this.ERROR_ELEMENT_CANNOT_HAVE_CHILDREN = "202";
	this.ERROR_ELEMENT_IS_NOT_AN_ARRAY = "203";
	this.ERROR_NOT_INITIALIZED = "301";
	this.ERROR_NOT_IMPLEMENTED = "401";
	this.ERROR_INVALID_SET_VALUE = "402";
	this.ERROR_ELEMENT_IS_READ_ONLY = "403";
	this.ERROR_ELEMENT_IS_WRITE_ONLY = "404";
	this.ERROR_INCORRECT_DATA_TYPE = "405";

	// Local variable used to keep from bootstrapping more than once
	this.started = false;
	
	// The mode variable is set to MODE_COURSE if the LMS presents a course,
	// it is set to MODE_ADMIN if the LMS presents an administration page 
	this.MODE_COURSE = 0;
	this.MODE_ADMIN = 1;
	this.MODE_LOGGED_OUT = 2,
	this.mode = this.MODE_LOGGED_OUT;
	
	// The state of the current SCO
	this.STATE_NOT_INITIALIZED = 0; // The current SCO is not initialized
	this.STATE_INITIALIZED = 1; // The current SCO is initialized
	this.scoState = this.STATE_NOT_INITIALIZED;
  
	// The last error 
	this.lastError = this.NO_ERROR;
  
	// The URL of the top level frame that contains this lmsapi.js
	// (without the filename of the top level frame).
	this.topURL = "";
	this.tocListeners = new Map();
	
	// Relative path from topURL to the course directory.
	// Note: Trailing slash must be there. It helps to simplify the string concatenations a little bit.
	this.coursePath = "course/";
	
	// The name of the current layer or null if no layer is active.
	// This attribute is needed for layered organization structures only.
	this.currentColumnName = null;

	// Public API Operations
	this.LMSInitialize = LMSAPI_LMSInitialize;
	this.LMSFinish = LMSAPI_LMSFinish;
	this.LMSGetValue = LMSAPI_LMSGetValue;
	this.LMSSetValue = LMSAPI_LMSSetValue;
	this.LMSCommit = LMSAPI_LMSCommit;
	this.LMSGetLastError = LMSAPI_LMSGetLastError;
	this.LMSGetErrorString = LMSAPI_LMSGetErrorString;
	this.LMSGetDiagnostic = LMSAPI_LMSGetDiagnostic;
  
	// Protected API Operations
	this.start = LMSAPI_start;
	this.getFirstItem = LMSAPI_getFirstItem;
	this.getNextItemOf = LMSAPI_getNextItemOf;
	this.getNextRowOf = LMSAPI_getNextRowOf;
	this.getPreviousItemOf = LMSAPI_getPreviousItemOf;
	this.getPreviousRowOf = LMSAPI_getPreviousRowOf;
	this.gotoFirstItem = LMSAPI_gotoFirstItem;
	this.gotoResumeItem = LMSAPI_gotoResumeItem;
	this.gotoNextItem = LMSAPI_gotoNextItem;
	this.gotoPreviousItem = LMSAPI_gotoPreviousItem;
	this.gotoItemWithID = LMSAPI_gotoItemWithID;
	this.gotoItemBefore = LMSAPI_gotoItemBefore;
	this.gotoItemAfter = LMSAPI_gotoItemAfter;
	this.gotoItem = LMSAPI_gotoItem;
	this.gotoLogin = LMSAPI_gotoLogin;
	this.gotoMenu = LMSAPI_gotoMenu;
	this.getValue = LMSAPI_getValue;
	this.setValue = LMSAPI_setValue;
	this.setValueToDefault = LMSAPI_setValueToDefault;
	this.login = LMSAPI_login;
	this.logout = LMSAPI_logout;
	this.isLoggedIn = LMSAPI_isLoggedIn;
	this.readCMIdataModel = LMSAPI_readCMIdataModel;
	this.writeCMIdataModel = LMSAPI_writeCMIdataModel;
	this.toggleLogging = LMSAPI_toggleLogging;
	
	// If this.isSCORMAdapter == false, then cookies are used for persistence
	// storage of the database. 
	// If this.isSCORMAdapter == true, then the parent LMS is use for persistence
	// storage.
	// Changing this variable also requires changing this.loadDatabase and
	// this.saveDatabase. To change it, use setSCORMAdapter(boolean)
	this.isSCORMAdapter = false;
	this.loadDatabase = LMSAPI_loadDatabaseFromCookie;
	this.saveDatabase = LMSAPI_saveDatabaseToCookie;
	//this.loadDatabase = LMSAPI_loadDatabaseFromParentLMS;
	//this.saveDatabase = LMSAPI_saveDatabaseToParentLMS;
	this.setSCORMAdapter = LMSAPI_setSCORMAdapter;
	
	this.clearDatabase = LMSAPI_clearDatabase;
	this.resetDatabase = LMSAPI_resetDatabase;
	this.getCurrentItem = LMSAPI_getCurrentItem;
	this.getAnticipatedItem = LMSAPI_getAnticipatedItem;
	this.getCurrentOrganization = LMSAPI_getCurrentOrganization;
	this.getCurrentColumnName = LMSAPI_getCurrentColumnName;
	this.setCurrentColumnName = LMSAPI_setCurrentColumnName;
	
	this.addTOCListener = LMSAPI_addTOCListener;
	this.removeTOCListener = LMSAPI_removeTOCListener;
	this.fireUpdateTOC = LMSAPI_fireUpdateTOC;
	this.fireUpdateTOCLater = LMSAPI_fireUpdateTOCLater;
	this.addTimespan = LMSAPI_addTimespan;
	this.showBugInfo = LMSAPI_showBugInfo;

	// This is used to discern API objects of TinyLMS from API objects of a parent LMS 	
	this.isTinyLMS = true;
}

/**
 * The API variable is the only thing of the LMS that may be accessed from an SCO.
 */
var API = new LMSAPI();