/*
	ICUS_SCORM_API

	10/09/2003 - Created by Timothée Groleau
	(c) 2003 - ICUS Pte Ltd - All rights reserved.

	Create a class to build a SCORM API object

	Requires the scorm data definition object structure
*/

function SCORMAPI(data_definition, strict) {
	this.$data_definition = data_definition;
	this.setStrictMode(strict);
	this.reset();
}

o = SCORMAPI.prototype;

/* =============================================
	initialization function
============================================= */

o.reset = function() {
	this.$lastErrorCode = "0";
	this.$initialized = false;
	this.$data = {};
}


/* =============================================
	stores the error codes and descriptions
============================================= */

o.$errorStrings = {};
o.$errorStrings[0] = "No error";
o.$errorStrings[101] = "General Exception";
o.$errorStrings[201] = "Invalid argument error";
o.$errorStrings[202] = "Element cannot have children";
o.$errorStrings[203] = "Element not an array - Cannot have count";
o.$errorStrings[301] = "Not Initialized";
o.$errorStrings[401] = "Not implemented error";
o.$errorStrings[402] = "Invalid set value, element is a keyword";
o.$errorStrings[403] = "Element is read only";
o.$errorStrings[404] = "Element is write only";
o.$errorStrings[405] = "Incorrect data type";



/* =============================================
	methods to perform the data type checks
============================================= */

/* =============================================
	Methods below describes the SCORM standard data-types
============================================= */

o.isAlphaNum = function(arg) {
	var r = new RegExp("^[a-zA-Z0-9]+$");
	return (arg.search(r) > -1);
}

o.isCMIBlank = function(arg) {
	return (arg == "");
}

o.isCMIBoolean = function(arg) {
	if (!this.$strict)  arg = arg.toLowerCase();
	return (arg == "true" || arg == "false");
}

o.isCMIDecimal = function(arg) {
	var r = new RegExp("^([0-9]+|(-0*[1-9][0-9]*))([.][0-9]+)?$");
	return (arg.search(r) > -1);
}

o.isCMIFeedback = function(arg) {
	// not supported yet, requires more thinking
	return true;
}

o.isCMIIdentifier = function(arg) {
	return (this.isCMIString255(arg) && this.isAlphaNum(arg));
}

o.isCMIInteger = function(arg) {
	var r = new RegExp("^([0-9]+)$");
	return (arg.search(r) > -1);
}

o.isCMISInteger = function(arg) {
	var r = new RegExp("^([0-9]+|(-0*[1-9][0-9]*))$");
	return (arg.search(r) > -1);
}

o.isCMIString255 = function(arg) {
	return (arg.length <= 255);
}

o.isCMIString4096 = function(arg) {
	return (arg.length <= 4096);
}

o.isCMITime = function(arg) {
	var r = new RegExp("^([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,2})?$");
	return (arg.search(r) > -1);
}

o.isCMITimespan = function(arg) {
	var r = new RegExp("^[0-9]{2,4}:[0-9]{2}:[0-9]{2}([.][0-9]{1,2})?$");
	return (arg.search(r) > -1);
}

o.isCMIVocabulary = function(arg, dataDefinition) {
	if (!this.$strict) arg = arg.toLowerCase();

	for (var i in dataDefinition.vocabulary) {
		if (dataDefinition.vocabulary[i] == arg) return true;
	}

	return false;
}

o.isDataTypeCorrect = function(value, dataDefinition) {
	for (var i in dataDefinition.dataType) {
		if (this["is" + dataDefinition.dataType[i]](value, dataDefinition)) return true;
	}
	return false;
}



/* =============================================
	Methods below are ICUS-specific datatypes
	to match more closely the actual SCORM specification
============================================= */

// is used for student ID, CMIIdentifier with additional dash and underscore
o.isICUSStudentID = function(arg) {
	if (!this.isCMIString255(arg)) return;
	var r = new RegExp("^[a-zA-Z0-9_-]+$");
	return (arg.search(r) > -1);
}



/* =============================================
	methods to perform parameter check - PRIVATE - 
============================================= */

o.getData = function(arg) {
	arg = (this.$strict ? arg : arg.toLowerCase()).split(".");

	var result = {last: null, supported: true};
	var lastNonZeroIndex = -1;
	var idx = -1;

	// must handle special attributes separately
	var last  = arg[arg.length-1];
	if (last == "_children" || last == "_count" || last == "_version") {
		result.last = last;
		delete arg[arg.length-1];
		arg.length--;
	}
	var original = result.path = arg;

	// in the data definition, '.xxx.', where 'xxx' are digits, must be turned to '.n.'
	var formatted = arg.concat();
	for (idx=0; idx < formatted.length; idx++) {
		if (!isNaN(formatted[idx]) && parseInt(formatted[idx], 10) >= 0) {
			original[idx] = parseInt(formatted[idx], 10);
			formatted[idx] = "n";
			if (original[idx] > 0) lastNonZeroIndex = idx;
		}
	}

	// get data definition element:
	var elmt = this.$data_definition;
	idx = 0;
	while(idx<formatted.length && (elmt = elmt[formatted[idx]]) != null) {
		if (elmt["supported"] == false) result.supported = false;
		idx++;
	}
	result.dataDefinition = elmt;

	// check that eventual indexes are accessed in order, otherwise, dataDefinition is invalid
	if (lastNonZeroIndex > -1) {
		var upToLastIndex = original.concat();
		for (idx = upToLastIndex.length - 1; idx>lastNonZeroIndex; idx--) {
			delete upToLastIndex[idx];
			upToLastIndex.length--;
		}
		for (idx=0, elmt=this.$data; idx<upToLastIndex.length; idx++) {
			if (formatted[idx] == "n" && upToLastIndex[idx] > 0) {
				if (elmt[upToLastIndex[idx]-1] != null) {
					elmt = elmt[upToLastIndex[idx]];
				} else {
					result.dataDefinition = null;
					break;
				}
			} else if (elmt[upToLastIndex[idx]] != null) {
				elmt = elmt[upToLastIndex[idx]];
			} else {
				result.dataDefinition = null;
				break;
			}
		}
	}

	// get actual element (if any)
	elmt = this.$data;
	idx = 0;
	while(idx<original.length && (elmt = elmt[original[idx]]) != null) idx++;
	result.actualData = elmt;

	return result;
}

o.getChildren = function(dataObject) {
	var elmts = [];
	var node = dataObject.dataDefinition;
	if (node.n != null) node = node.n;
	
	for (var name in node) {
		if (node[name]["supported"] == false) continue;
		elmts[elmts.length] = name;
	}

	// returns the coma-separated formatted children list
	return elmts.join(",");
};

o.getCount = function(actualData) {
	// returns highest index + 1
	// does NOT return the actual number of elements
	var max = -1;
	for (var idx in actualData) {
		if (parseInt(idx) > max) max = parseInt(idx);
	}
	return (max + 1);
};



/* =============================================
	SCORM API public methods
============================================= */

o.LMSInitialize = function(arg) {
	if (arg != "") {
		this.$lastErrorCode = "201";
		return "false";
	}

	if (this.$initialized) {
		this.$lastErrorCode = "101";
		return "false";
	}
	
	this.$lastErrorCode = "0";
	this.$initialized = true;
	return "true";
};

o.LMSFinish = function(arg) {
	if (arg != "") {
		this.$lastErrorCode = "201";
		return "false";
	}

	if (!this.$initialized) {
		this.$lastErrorCode = "301";
		return "false";
	}

	// commit data
	this.LMSCommit("", true);
	
	this.$lastErrorCode = "0";
	this.$initialized = false;
	return "true";
};

o.LMSGetValue = function(element, force) {
	
	if (!this.$initialized && (force != true)) {
		this.$lastErrorCode = "301";
		return "";
	}

	var data = this.getData(element);

	if (data.dataDefinition == null) {
		// element is invalid
		this.$lastErrorCode = "201";
		return "";
	}
	if (!data.supported) {
		// element is not supported
		this.$lastErrorCode = "401";
		return "";
	}
	if (data.last != null) {
		if (data.last == "_children") {
			if (data.dataDefinition["dataType"] != null) {
				// data doesn't support _children
				this.$lastErrorCode = "202";
				return "";
			} else {
				// query OK
				this.$lastErrorCode = "0";
				return this.getChildren(data);
			}
		}
		if (data.last == "_count") {
			if (data.dataDefinition["n"] == null) {
				// data doesn't support _count
				this.$lastErrorCode = "203";
				return "";
			} else {
				// query OK
				this.$lastErrorCode = "0";
				return this.getCount(data.actualData);
			}
		}
		if (data.last == "_version") {
			// if all tests are passed so far , the _version is at the root of the data definition
			this.$lastErrorCode = "0";
			return this.$data_definition.cmi._version;
		}
	}
	// for optional elements, if force is true, allow getting of default value from our system,
	// otherwise, return not implemented error
	if ((data.dataDefinition.mandatory === false) && (data.actualData == null) && (force != true)) {
		// element is not implemented
		this.$lastErrorCode = "401";
		return "";
	}
	if (data.dataDefinition.accessibility == "wo" && (force != true)) {
		// element is write only
		this.$lastErrorCode = "404";
		return "";
	}

	this.$lastErrorCode = "0";
	if (data.actualData == null) {
		return data.dataDefinition.defaultValue;
		
	} else {
		return data.actualData;
	}
};

o.LMSSetValue = function(element, value, force) {
	if (!this.$initialized && (force != true)) {
		this.$lastErrorCode = "301";
		return "false";
	}
	
	var data = this.getData(element);
	value = value.toString();
	
	if (data.dataDefinition == null) {
		// element is invalid
		this.$lastErrorCode = data.supported ? "201" : "401";
		return "false";
	}
	if (!data.supported) {
		// element is not supported
		this.$lastErrorCode = "401";
		return "false";
	}
	if ((data.last != null) || (data.dataDefinition["dataType"] == null)) {
		// element is a keyword
		this.$lastErrorCode = "402";
		return "false";
	}
	if ((data.dataDefinition.accessibility == "ro")  && (force != true)) {
		// element is read only
		this.$lastErrorCode = "403";
		return "false";
	}
	if (!this.isDataTypeCorrect(value, data.dataDefinition)) {
		// incorrect data type
		this.$lastErrorCode = "405";
		return "false";
	} else {
		// data type is correct, check range as well if needed
		if (data.dataDefinition.range != null && this.isCMIDecimal(value)) {
			var n = Number(value);
			if (n < data.dataDefinition.range[0] || n > data.dataDefinition.range[1]) {
				// out of range, no specific error in scorm 1.2, use 405
				this.$lastErrorCode = "405";
				return "false";
			}
		}
	}
	
	// manually checks exceptions
	if (!force) {
		if (!this.$strict) element = element.toLowerCase();
	
		// "not attempted" is not a legal value for cmi.core.lesson_status when set from sco
		if (element == "cmi.core.lesson_status" && value == "not attempted") {
			this.$lastErrorCode = "405"; // Incorrect data type
			return "false";
		}
		// comments must be concatenated instead of overwritten
		if (element == "cmi.comments") {
			value = this.LMSGetValue(element, true) + value;
		}
	}

	// all tests passed successfully, assign value (create path if necessary)
	var o = this.$data;
	for (var i=0; i<data.path.length-1; i++) {
		if (o[data.path[i]] == null) o[data.path[i]] = {};
		o = o[data.path[i]];
	}
	o[data.path[data.path.length-1]] = value;

	this.$lastErrorCode = "0";
	return "true";
};

o.LMSCommit = function(arg, force) {
	if (arg != "") {
		this.$lastErrorCode = "201";
		return "false";
	}

	if (!this.$initialized) {
		this.$lastErrorCode = "301";
		return "false";
	}

	this.$lastErrorCode = "0";
	return "true"
};

o.LMSGetLastError = function(force) {
	return this.$lastErrorCode;
};

o.LMSGetErrorString = function(errorNum, force) {
	return ((errorNum == "") || (errorNum == null) || (this.$errorStrings[errorNum] == null)) ? "" : this.$errorStrings[errorNum];
};

o.LMSGetDiagnostic = function(errorNum, force) {
	return this.LMSGetErrorString(((errorNum == null) || (errorNum == "")) ? this.LMSGetLastError() : errorNum);
};



/* =============================================
	Additional, non-standard API methods
	(to be used internally or for testing, NOT in actual courses)
============================================= */

o.setStrictMode = function(strict) {
	this.$strict = (strict === true);
}

o.getStrictMode = function() {
	return this.$strict;
}

o.removeElement = function(name) {
	var path = name.split(".");

	var o = this.$data;
	for (var i=0; i<path.length-1; i++) {
		if (o[path[i]] == null) return false;
		o = o[path[i]];
	}
	delete o[path[path.length-1]];
	return true;
}

o.getDataDefinition = function() {
	return this.$data_definition;
}

/* =============================================
	cleanup used variable
============================================= */
delete o;

