// SmartSCOPlayer2004.js
/* ========== DESCRIPTION ===========
This player works in 2 modes:
- When m_trackID = -1 => Remote mode:
    The data is not committed to LMS (WebService) but are sent via an html proxy to the player from the LMS
    The methods that are passed to the LMS's player are:
		- Initialize
		- Terminate
		- SetValue
		- Commit
    ; other methods are local
- When m_trackID != -1 => LMS mode
    The data is committed to LMS (WebService) 
	The methods that commit to LMS are:
		- Commit
		- Terminate
	; other methods are local
================================== */
// ========= GENERIC UTILITY FUNCTIONS =========

function IsValidIeeeIdentifier(s)
{
	// False if any char < "!" that is not tab, CR or LF
	var c;
	for (var i=0;i<s.length;i++)
	{
		c = s.charCodeAt(i);
		if ((c < 33)&&((c!=9)&&(c!=10)&&(c!=13))) 
			return false;
	}
	return true;
}

// ========= TIME AND DURATION FUNCTIONS ========
// Helper functions for duration
function centisecsToISODuration(n) {
    // Note: SCORM and IEEE 1484.11.1 require centisec precision
    // Months calculated by approximation based on average number
  // of days over 4 years (365*4+1), not counting the extra day
  // every 1000 years. If a reference date was available,
  // the calculation could be more precise, but becomes complex,
    // since the exact result depends on where the reference date
    // falls within the period (e.g. beginning, end or ???)
  // 1 year ~ (365*4+1)/4*60*60*24*100 = 3155760000 centiseconds
  // 1 month ~ (365*4+1)/48*60*60*24*100 = 262980000 centiseconds
  // 1 day = 8640000 centiseconds
  // 1 hour = 360000 centiseconds
  // 1 minute = 6000 centiseconds
    n = Math.round(Math.max(n,0)); // there is no such thing as a negative duration
    var str = "P";
    var nCs = n;
    // Next set of operations uses whole seconds
    var nY = Math.floor(nCs / 3155760000);
    nCs -= nY * 3155760000;
    var nM = Math.floor(nCs / 262980000);
    nCs -= nM * 262980000;
    var nD = Math.floor(nCs / 8640000);
    nCs -= nD * 8640000;
    var nH = Math.floor(nCs / 360000);
    nCs -= nH * 360000;
    var nMin = Math.floor(nCs /6000);
    nCs -= nMin * 6000
    // Now we can construct string
    if (nY > 0) str += nY + "Y";
    if (nM > 0) str += nM + "M";
    if (nD > 0) str += nD + "D";
    if ((nH > 0) || (nMin > 0) || (nCs > 0)) {
        str += "T";
        if (nH > 0) str += nH + "H";
        if (nMin > 0) str += nMin + "M";
    if (nCs > 0) str += (nCs / 100) + "S";
    }
    if (str == "P") str = "PT0H0M0S";
      // technically PT0S should do but SCORM test suite assumes longer form.
    return str;
}

function ISODurationToCentisec(str) {
    // Only gross syntax check is performed here
    // Months calculated by approximation based on average number
  // of days over 4 years (365*4+1), not counting the extra day
  // every 1000 years. If a reference date was available,
  // the calculation could be more precise, but becomes complex,
    // since the exact result depends on where the reference date
    // falls within the period (e.g. beginning, end or ???)
  // 1 year ~ (365*4+1)/4*60*60*24*100 = 3155760000 centiseconds
  // 1 month ~ (365*4+1)/48*60*60*24*100 = 262980000 centiseconds
  // 1 day = 8640000 centiseconds
  // 1 hour = 360000 centiseconds
  // 1 minute = 6000 centiseconds
    var aV = new Array(0,0,0,0,0,0);
    var bErr = false;
    var bTFound = false;
    if (str.indexOf("P") != 0) bErr = true;
    if (!bErr){
        var aT = new Array("Y","M","D","H","M","S")
        var p=0;
        var i = 0;
        str = str.substr(1); //get past the P
        for (i = 0 ; i < aT.length; i++){
            if (str.indexOf("T") == 0){
                str = str.substr(1);
                i = Math.max(i,3);
        bTFound = true;
            }
            p = str.indexOf(aT[i]);
            //alert("Checking for " + aT[i] + "\nstr = " + str);
            if (p > -1){
                // Is this a M before or after T?
                if ((i == 1) && (str.indexOf("T") > -1) && (str.indexOf("T") < p)) continue;
                if (aT[i] == "S"){
                    aV[i] = parseFloat(str.substr(0,p))
                } else {
                    aV[i] = parseInt(str.substr(0,p))
                }
                if (isNaN(aV[i])){
                    bErr = true;
                    break;
                } else if ((i > 2) && (!bTFound)){
                    bErr = true;
                    break;
        }
                str = str.substr(p+1);
            }
        }
        if ((!bErr) && (str.length != 0)) bErr = true;
        //alert(aV.toString())
    }
    if (bErr){
        //alert("Bad format: " + str)
        m_lastError = 406; // TBD get correct error number here
        return 0
    }
    return aV[0]*3155760000 + aV[1]*262980000
      + aV[2]*8640000 + aV[3]*360000 + aV[4]*6000
      + Math.round(aV[5]*100)
}

// ========= PSEUDO RTE ===========
// Housekeeping
var m_lastError = 0;
var m_AfterTerminate = "_none_";

var m_BatchActions = new Array();  // records all actions for Commit
function LogBatchAction(nam, val)
{
	if (nam.substring(0, 4) == "cmi.")
		m_BatchActions.push(new NamedValue(nam,val));
}

// Comm session state management

var gnSessionTime = 0; // current session time
var m_status = 0;
  // 0 = not initialized; 1 = initialized; 3 = terminated

function Score() // Constructor for a new score
{
  this.scaled = "";
  this.raw = "";
  this.min = "";
  this.max = "";
}

var gaScore = new Score();
var gaNamedValues = new Array();
var gaInteractions = new Array();
var gaObjectives = new Array();
var gaCommentsFromLearner = new Array();
var gaCommentsFromLms = new Array();
var gsPrimarySuccessStatus = "unknown";
var gsPrimaryCompletionStatus = "not attempted"


// These are not reinitialized on reset attempt
var gLearnPrefAudioCaptioning = "0";
var gLearnPrefAudioLevel = "1";
var gLearnPrefDeliverySpeed = "1";
var gLearnPrefLanguage = "fr-BE";

var gSuspendData = "";
var gSuspendLocation = "";

function NamedValue(nam,val) // Constructor for a name-value pair
{
  this.nam = nam;
  this.val = val
}

function SetValueByName(nam,val)
{
  var n = -1;
  for (i=0;i<gaNamedValues.length;i++)  if (gaNamedValues[i].nam == nam) n = i;
  if (n == -1) gaNamedValues[gaNamedValues.length] = new NamedValue(nam,val);
  else gaNamedValues[n].val = val;
   return 0;
}

function GetValueByName(nam)
{
  var n = -1;
  for (i=0;i<gaNamedValues.length;i++)  if (gaNamedValues[i].nam == nam) n = i;
  if (n == -1) return null;
  else return gaNamedValues[n].val;
}

function StripLeadingDots(s, n)
{
  var p = 0;
  // strip n dotted elements in dot notation
  var a = s.split(".");
  for (i = 0; i< n; i++) p += a[i].length + 1;
  return s.substr(p);
}

function SetValueScore(aScore, what, val)
{
	var n = parseFloat(val);
	if (isNaN(n)) 
		return 406;
	switch(what)
	{
		case "scaled":
			if ((n < -1.0) || (n > 1.0)) 
				return 407;
			aScore.scaled = n;
			break;
		case "raw":
			aScore.raw = n;
			break;
		case "min":
			aScore.min = n;
			break;
		case "max":
			aScore.max = n;
			break;
		default:
			return 401;
	}
	return 0;
}

function GetValueScore(aScore, what)
{
	var r = "";
	var nErr = 0;
	switch(what)
	{
		case "_children":
			r = "scaled,raw,min,max";
			break;
		case "scaled":
			r = aScore.scaled;
			break;
		case "raw":
			r = aScore.raw;
			break;
		case "min":
			r = aScore.min;
			break;
		case "max":
			r = aScore.max;
			break;
		default:
			m_lastError = 351;
	}
	return r + "";
}

function Interaction(id) // Constructor for a new interaction record
{
  this.id = id;
  this.type= null;
  this.description = "";
  this.latency = "";
  this.timestamp = "";
  this.result = "";
  this.weigthing = "";
  this.correctResponses = new Array();
  this.learnerResponse = "";
  this.objectives = new Array();
}

function isValidInteractionType(s)
{
  var a = new Array("true-false","choice","fill-in","long-fill-in","likert","matching","performance","sequencing","numeric","other");
  for (i=0;i<a.length;i++)
  {
    if (s == a[i]) 
		return true;
  }
  return false;
}

function isPositiveInt(n)
{
  return ((!isNaN(n)) && (Math.round(n) == n) && (n >= 0))
}

function SetCorrectResponses(nInteraction, n2, nam, val)
{
  //alert("Set CorrectResponses\n" + nInteraction + "\n" + n2 + "\n" + nam + "\n" + val)
  var nErr = 0;
  var typ = gaInteractions[nInteraction].type;
  if (typ == null) return 301; // tbd
  var i = parseInt(n2);
  if (!isPositiveInt(i)) return 351; // tbd
  if (nam != "pattern") return 351; // tbd
  if ((i > 0) && ((typ == "true-false") || (typ == likert))) return 351;
  if (i > gaInteractions[nInteraction].correctResponses.length) return 351;
  gaInteractions[nInteraction].correctResponses[i] = val.toString();
  return 0;
}

function GetCorrectResponses(nInteraction, n2, nam)
{
  //alert("Get CorrectResponses\n" + nInteraction + "\n" + n2 + "\n" + nam)
  var nErr = 0;
  var r = "";
  var typ = gaInteractions[nInteraction].type;
  if (typ == null) nErr = 301; // tbd
  else
  {
    var i = parseInt(n2);
    if (!isPositiveInt(i)) nErr = 351; // tbd
    else if (nam != "pattern") nErr = 351; // tbd
    else if ((i > 0) && ((typ == "true-false") || (typ == likert))) nErr = 351;
    else if (i > gaInteractions[nInteraction].correctResponses.length - 1) nErr = 351;
    else r = gaInteractions[nInteraction].correctResponses[i].toString();
  }
  if (nErr != 0) m_lastError = nErr;
  return r
}

function SetValueInteractions(what, val)
{
  // what = the part of the element name after "interactions."
  var nErr = 0;
  var a = what.split(".");
  var n = parseInt(a[0]);
  if (!isPositiveInt(n)) return 406;
  if (n > gaInteractions.length) return 407;
  if (a[1] == "id")
  {
    if ((!val) || (val == "") || (!IsValidIeeeIdentifier(val))) return 406;
    if (n == gaInteractions.length)
    {
      gaInteractions[n] = new Interaction(val);
    }
    else
    {
      gaInteractions[n].id = val;
    }
  }
  else
  {
    if (n > gaInteractions.length - 1) return 407; // doesn't exist
    if (gaInteractions[n].id == null) return 408; // id not set yet
    switch(a[1])
    {
      case "type":
        if (gaInteractions[n].type != null)
        {
          if (gaInteractions[n].type == val) return 0; // nothing to do
          return 351; // can't redefine
        }
        if (isValidInteractionType(val)) gaInteractions[n].type = val;
        else nErr = 351; // invalid type token
        break;
      case "latency":
        gaInteractions[n].latency = val;
        break;
      case "timestamp":
        gaInteractions[n].timestamp = val;
        break;
      case "result":
        gaInteractions[n].result = val;
        break;
      case "weighting":
        gaInteractions[n].weighting = val;
        break;
      case "description":
        gaInteractions[n].description = val;
        break;
      case "correct_responses":
        nErr = SetCorrectResponses(n, a[2], a[3], val);
        break;
      case "learner_response":
        gaInteractions[n].learnerResponse = val;
        break;
      case "objectives":
        if ((isPositiveInt(a[2]))
            && (a[2] <= gaInteractions[n].objectives.length)
            && (a[3] == "id"))
        {
          gaInteractions[n].objectives[a[2]] = val;
        }
        else nErr = 351;
        break;
      default:
        nErr = 351;
        break;
    }
  }
  return nErr
}

function GetValueInteractions(what)
{
  // what = the part of the element name after "cmi.interactions."
  var nErr = 0;
  var r = "";
  if (what == "_count")
  {
    if (!gaInteractions) return "0";
    else return gaInteractions.length + "";
  }
  if (what == "_children") return "id,type,latency,timestamp,result,weighting,description,correct_responses,learner_response";
  var a = what.split(".");
  var n = a[0];
  if (!isPositiveInt(n)) nErr = 406;
  else if (!gaInteractions) nErr == 407;
  else if (n > gaInteractions.length - 1) nErr = 407;
  else switch(a[1])
    {
      case "id":
        r = gaInteractions[n].id;
        break;
      case "type":
        r = gaInteractions[n].type;
        break;
      case "latency":
        r = gaInteractions[n].latency;
        break;
      case "timestamp":
        r = gaInteractions[n].timestamp;
        break;
      case "result":
        r = gaInteractions[n].result;
        break;
      case "weighting":
        r = gaInteractions[n].weigting;
        break;
      case "description":
        r = gaInteractions[n].description;
        break;
      case "correct_responses":
      	if (a[2] == "_count")
      	{
      		r = gaInteractions[n].correctResponses.length + "";
      	}
      	else
      	{
        	var a = GetCorrectResponses(n, a[2], a[3]);
        	if (a[0] != 0) nErr = a[0]; else r = a[1];
        }
        break;
      case "learner_response":
        r = gaInteractions[n].learnerResponse ;
        break;
      case "objectives":
        if (a[2] == "_count")
        {
        	r = gaInteractions[n].objectives.length +"";
        }
        else if ((isPositiveInt(a[2]))
            && (a[2] < gaInteractions[n].objectives.length)
            && (a[3] == "id"))
        {
          r = gaInteractions[n].objectives[a[2]];
        }
        else nErr = 351;
        break;
      default:
        nErr = 351;
        break;
    }
  if (nErr != 0) m_lastError = nErr;
  return r
}

function SetValueLearnerPreference(what,val)
{
  var nErr = 0;
  var r = "true";
  switch (what)
  {
    case "audio_captioning":
      if ((val != "-1") || (val != "0") || (val != 1))
      {
        nErr = 406; // TBD correct
      }
      else
      {
        gLearnPrefAudioCaptioning = val;
      }
      return "false";
      break;
    case "audio_level":
      var n = parseFloat(val);
      if ((isNaN(n)) || (n < 0))
      {
        nErr == 406; // TBD correct error
      }
      else
      {
        gLearnPrefAudioLevel = val
      }
      break;
    case "delivery_speed":
      var n = parseFloat(val);
      if ((isNaN(n)) || (n < 0))
      {
        nErr == 406; // TBD correct error
      }
      else
      {
        gLearnPrefDeliverySpeed = val;
      }
      break;
    case "language":
      gLearnPrefLanguage = val;
      break;
    default:
      nErr = 401; // TBD correct err value
      break;
  }
  return nErr;
}

function GetValueLearnerPreference(what)
{
  var nErr = 0;
  var r = "";
  switch (what)
  {
    case "_children":
      r = "audio_captioning,audio_level,delivery_speed,language";
      break;
    case "audio_captioning":
      r = gLearnPrefAudioCaptioning;
      break;
    case "audio_level":
      r = gLearnPrefAudioLevel;
      break;
    case "delivery_speed":
      r = gLearnPrefDeliverySpeed; break;
    case "language":
      r = gLearnPrefLanguage;
      break;
    default:
      nErr = 401; // TBD correct err value
      break;
  }
  if (nErr != 0) m_lastError = nErr;
  return r;
}

function Objective(id) // Constructor for a new objective record
{
	this.id = id;
	this.description = "";
	this.score = new Score();
	this.success_status = "unknown";
	this.completion_status = "unknown";
	this.progress_measure = "0";
	return this;
}

function IsValidSuccessStatus(val)
{
	var a = new Array("passed","failed","unknown");
	//alert(a.toString() + '\n"' + val + '"')
	for (var i=0;i<a.length;i++)
	{
		if (val==a[i]) 
			return true;
	}
	return false;
}

function IsValidCompletionStatus(val)
{
  var a = new Array("completed","incomplete","not attempted","unknown");
  for (var i=0;i<a.length;i++) if (val==a[i]) return true;
  return false;
}

function SetValueObjectives(what, val)
{
  // what = the part of the element name after "objectives."
  var nErr = 0;
  var a = what.split(".");
  var n = parseInt(a[0]);
  if (!isPositiveInt(n)) return 406;
  if (n > gaObjectives.length) return 407;
  if (a[1] == "id")
  {
    if ((!val) || (val == "") || (!IsValidIeeeIdentifier(val))) return 406;
    if (n == gaObjectives.length)
    {
      // tbd add uniqueness check here
      var bOK = true;
      for (var i=0;i<gaObjectives.length;i++)
      {
        if (gaObjectives[i].id == val) bOK = false;
      }
      if (bOK)  gaObjectives[n] = new Objective(val);
      else nErr = 351; // duplicate; err code TBD on final Rel 3.
    }
    else
    {
      if (gaObjectives[n].id != val)
      {
        nErr = 404; // Cannot change ID; err code TBD on final Rel 3.
      }
    }
  }
  else
  {
    if (n > gaObjectives.length - 1) return 407; // doesn't exist
    if (gaObjectives[n].id == null) return 408; // id not set yet
    switch(a[1])
    {
      case "description":
        gaObjectives[n].description = val;
        break;
      case "success_status":
        if (IsValidSuccessStatus(val)) gaObjectives[n].success_status = val;
        else nErr = 406;
        break;
      case "completion_status":
        if (IsValidCompletionStatus(val)) gaObjectives[n].completion_status = val;
        else nErr = 406;
        break;
      case "progress_measure":
        if ((!isNaN(n)) && (n >= 0.0) && (n <= 1.0)) gaObjectives[n].progress_measure = val;
        else nErr = 406;
        break;
      case "score":
        // a2 = "scaled" | "min" | "max" | "raw"
        nErr = SetValueScore(gaObjectives[n].score, a[2], val);
        break;
      default:
        nErr = 351;
        break;
    }
  }
  return nErr
}

function GetValueObjectives(what)
{
  // what = the part of the element name after "objectives."
  if (what == "_children")
  {
  	return "id,score,success_status,completion_status,progress_measure,description";
  }
  else if (what == "_count")
  {
  	return gaObjectives.length + "";
  }
  var nErr = 0;
  var r = "";
  var a = what.split(".");
  var n = parseInt(a[0]);
  if (!isPositiveInt(n)) nErr = 406;
  else if (n > gaObjectives.length - 1) nErr = 407;
  else switch(a[1])
    {
      case "completion_status":
        r = gaObjectives[n].completion_status;
        break;
      case "description":
        r = gaObjectives[n].description;
        break;
      case "id":
        r = gaObjectives[n].id;
        break;
      case "progress_measure":
        r = gaObjectives[n].progress_measure;
        break;
      case "score":
        // a2 = "scaled" | "min" | "max" | "raw"
        r = GetValueScore(gaObjectives[n].score, a[2]);
        break;
      case "success_status":
        r = gaObjectives[n].success_status;
        break;
      default:
        nErr = 351;
        break;
    }
  if (nErr != 0) m_lastError = nErr;
  return r
}

function SetValueSuspendData(val)
{
  if (val.length <= 64000)
  {
    gSuspendData = val;
    if (val.length > 4000)
    {
      // Maybe add a compatibility warning here
    }
    return 0;
  }
  else
  {
    return 351;
  }
}

function SetValueSuspendLocation(val)
{
  if (val.length <= 1000)
  {
    gSuspendLocation = val;
    return 0;
  }
  else
  {
    return 351;
  }
}

function CmiComment()
{
  this.comment = "";
  this.location = "";
  this.timestamp = "";
}

function SetValueCommentFromLearner(what, val)
{
	// what = n.element
	var nErr = 0;
	var aTemp = null
	var a = what.split(".");
	var n = parseInt(a[0]);
	if (!isPositiveInt(n)) 
		return 406;
	if (n > gaCommentsFromLearner.length) 
		return 407;
	if (n == gaCommentsFromLearner.length) 
		aTemp = new CmiComment();
	else 
		aTemp = gaCommentsFromLearner[n];
	switch(a[1])
	{
		case "comment":
			aTemp.comment = val;
			break;
		case "location":
			aTemp.location = val;
			break;
		case "timestamp":
			aTemp.timestamp = val;
			break;
		default:
			nErr = 403; // TBD adjust this error
	}
	if (nErr == 0) 
		gaCommentsFromLearner[n] = aTemp;
	return nErr;
}

function GetValueCommentFromLearner(what)
{
	// what = n.element
	var nErr = 0;
	var r = null;
	if (what == "_children") 
		return "comment,location,timestamp";
	if (what == "_count") 
		return gaCommentsFromLearner.length.toString();
	var a = what.split(".");
	var n = parseInt(a[0]);
	if (!isPositiveInt(n)) 
		return 406;
	else if (n > gaCommentsFromLearner.length - 1)
	{
		nErr = 407 // TBD Adjust
	}
	else
	{
		switch(a[1])
		{
			case "comment":
				r = gaCommentsFromLearner[n].comment;
				break;
			case "location":
				r = gaCommentsFromLearner[n].location;
				break;
			case "timestamp":
				r = gaCommentsFromLearner[n].timestamp;
				break;
			default:
				nErr = 403; // TBD adjust this error
		}
	}
	if (nErr != 0) 
		m_lastError = nErr;
	return r;
}

function GetValueCommentFromLms(what)
{
  // what = n.element
  var nErr = 0;
  var r = null;
  if (what == "_children") return "comment,location,timestamp";
  if (what == "_count") return gaCommentsFromLms.length;
  var a = what.split(".");
  var n = parseInt(a[0]);
  if (!isPositiveInt(n)) return 406;
  else if (n > gaCommentsFromLms.length - 1)
  {
    nErr = 407 // TBD Adjust
  }
  else
  {
    switch(a[1])
    {
      case "comment":
        r = gaCommentsFromLms[n].comment;
        break;
      case "location":
        r = gaCommentsFromLms[n].location;
        break;
      case "timestamp":
        r = gaCommentsFromLms[n].timestamp;
        break;
      default:
        nErr = 403; // TBD adjust this error
    }
  }
  if (nErr != 0) m_lastError = nErr;
  return r
}

function SetValueSessionTime(val)
{
  m_lastError = 0;
  n = ISODurationToCentisec(val);
  if (m_lastError == 0) gnSessionTime = n;
  return m_lastError;
}

function SetValueSuccessStatus(val)
{
  if (IsValidSuccessStatus(val))
  {
    gsPrimarySuccessStatus = val;
    m_lastError = 0;
  }
  else
  {
    m_lastError = 406;
  }
  return m_lastError;
}

function GetValueSuccessStatus(val)
{
	var n = parseFloat(GetValueScore(gaScore, "scaled"));
	if (!isNaN(n))
	{
		if (m_cmi['cmi.scaled_passing_score'] != "")
		{
			if (n >= m_cmi['cmi.scaled_passing_score']) 
				return "passed";
			else 
				return "failed";
		}
	}
	return gsPrimarySuccessStatus;
}

function SetValueCompletionStatus(val)
{
	if (IsValidCompletionStatus(val))
	{
		gsPrimaryCompletionStatus = val;
		m_lastError = 0;
	}
	else
	{
		m_lastError = 406;
	}
	return m_lastError;
}

function GetValueCompletionStatus(val)
{
	var n = parseFloat(GetValueByName("progress_measure"));
	if (!isNaN(n))
		{
		if (n >= m_cmi['cmi.completion_threshold']) 
			return "completed";
		else 
			return "incomplete";
	}
	return gsPrimaryCompletionStatus;
}

function _InitCMIValueD(what, val)
{
	what = cmiDecode(what);
	_InitCMIValue(what, val);
}

function _InitCMIValue(what, val)
{
	var a = what.split(".");
	switch (a[0])
	{
		case "cmi":
			switch(a[1])
			{
				case "comments_from_learner":
					SetValueCommentFromLearner(StripLeadingDots(what,2),val);
					break;
				case "completion_status":
					SetValueCompletionStatus(val);
					break;
				case "interactions":
					SetValueInteractions(StripLeadingDots(what,2),val);
					break;
				case "learner_preference":
					SetValueLearnerPreference(StripLeadingDots(what,2),val);
					break;
				case "location":
					SetValueSuspendLocation(val);
					break;
				case "objectives":
					SetValueObjectives(StripLeadingDots(what,2),val);
					break;
				case "progress_measure":
					SetValueByName(StripLeadingDots(what,1), val);
					break;
				case "score":
					SetValueScore(gaScore,StripLeadingDots(what,2),val);
					break;
				case "session_time":
					SetValueSessionTime(val);
					break;
				case "success_status":
					SetValueSuccessStatus(val);
					break;
				case "suspend_data":
					SetValueSuspendData(val);
					break;
				case "completion_threshold":
				case "entry":
				case "total_time":
				case "scaled_passing_score":				
					m_cmi[what] = val;
					break;
				case "comments_from_lms":
				case "launch_data":
				case "max_time_allowed":
				case "mode":
				case "time_limit_action":
					break;
				default:
					break;
			}
			break;
	}
}


function _SetValue(what, val)
{
	var nErr = 0;
	var r = "false";
	m_lastError = 0;
	if (m_status < 1)
		nErr = 132; // tbd
	else if (m_status > 2)
		nErr = 133; // tbd
	
	if (nErr == 0)
	{
		var a = what.split(".");
		switch (a[0])
		{
			case "cmi":
				switch(a[1])
				{
					case "comments_from_learner":
						nErr = SetValueCommentFromLearner(StripLeadingDots(what,2),val);
						break;
					case "comments_from_lms":
					case "completion_threshold":
					case "entry":
					case "launch_data":
					case "max_time_allowed":
					case "mode":
					case "scaled_passing_score":
					case "time_limit_action":
					case "total_time":
						nErr = 404;
						break;
					case "completion_status":
						nErr = SetValueCompletionStatus(val);
						break;
					case "exit":
						switch(val)
						{
							case "suspend":
							case "":
							case "logout":
							case "normal":
							case "time-out":
								// Just let it persist...
								break;
							default:
								nErr = 404;
								break;
						}
						break;
					case "interactions":
						nErr = SetValueInteractions(StripLeadingDots(what,2),val);
						break;
					case "learner_preference":
						nErr = SetValueLearnerPreference(StripLeadingDots(what,2),val);
						break;
					case "location":
						nErr = SetValueSuspendLocation(val);
						break;
					case "objectives":
						nErr = SetValueObjectives(StripLeadingDots(what,2),val);
						break;
					case "progress_measure":
						// TBD add validation
						nErr = SetValueByName(StripLeadingDots(what,1), val);
						break;
					case "score":
						nErr = SetValueScore(gaScore,StripLeadingDots(what,2),val);
						break;
					case "session_time":
						nErr = SetValueSessionTime(val);
						break;
					case "success_status":
						nErr = SetValueSuccessStatus(val);
						break;
					case "suspend_data":
						nErr = SetValueSuspendData(val);
						break;
					default:
						nErr = 401; // SetValueByName(StripLeadingDots(what,1), val);
						break;
				}
				break;
			case "adl":
				if (a[1] == "nav")
				{
					if (a[2] == "request")
					{
						m_AfterTerminate = val;
						switch(val)
						{
							case "continue":
							case "previous":
							case "exit":
							case "exitAll":
							case "abandon":
							case "abandonAll":
							case "_none_":
								nErr = SetValueByName(what, val);
								break;
							default:
								if ((val.indexOf("choice") < 8) || (val.indexOf("{target=") != 0) || (val.indexOf("}") != val.indexOf("choice") - 1))
								{
									nErr = 406;
								}
								break;
						}
					}
					else
					{
						nErr = 406;
					}
				}
				else
				{
					nErr = 401;
				}
				break;
			default:
				if (what == "")
				{
					nErr = 406;
				}
				else
				{
					nErr = 401;
				}
		}
	}
	if ((nErr == 0) && (m_lastError != 0)) 
		nErr = 0;
	m_lastError = nErr;
	r = ((m_lastError == 0).toString());
	if (m_logAPI) traceAPI("SetValue", what, val, r);
	if (r == "true")
	{
		if (m_trackId == -1)
			RemoteCall("SetValue", what, val);
		else
			LogBatchAction(what, val);
	}
	return r;
}

function _GetValue(what)
{
	var r = "";
	var nErr = 0;
	if (m_status < 1)
		nErr = 122;
	else if (m_status > 2)
		nErr = 123; // tbd

	if (nErr == 0)
	{
		var a = what.split(".")
		m_lastError = 0;
		switch (a[0])
		{
			case "cmi":
				switch(a[1])
				{
					case "_version":
						r = "1.0."
						break;
					case "comments_from_learner":
						r = GetValueCommentFromLearner(StripLeadingDots(what,2));
						break;
					case "comments_from_lms":
						r = GetValueCommentFromLms(StripLeadingDots(what,2));
						break;
					case "completion_status":
						r = GetValueCompletionStatus();
						break;
					case "completion_threshold":
						r = m_cmi['cmi.completion_threshold'];
						break;
					case "credit":
						r = m_cmi['cmi.credit'];
						break;
					case "entry":
						r = m_cmi['cmi.entry'];
						break;
					case "exit":
						r = "";
						nErr = 405;
						break;
					case "interactions":
						r = GetValueInteractions(StripLeadingDots(what,2));
						break;
					case "launch_data":
						r = m_cmi['cmi.launch_data'];
						break;
					case "learner_id":
						r = m_cmi['cmi.learner_id'];
						break;
					case "learner_name":
						r = m_cmi['cmi.learner_name'];
						break;
					case "learner_preference":
						r = GetValueLearnerPreference(StripLeadingDots(what,2));
						break;
					case "location":
						r = gSuspendLocation;
						break;
					case "max_time_allowed":
						r = m_cmi['cmi.max_time_allowed'];
						break;
					case "mode":
						r = m_cmi['cmi.mode'];
						break;
					case "objectives":
						r = GetValueObjectives(StripLeadingDots(what,2));
						break;
					case "progress_measure":
						r = GetValueByName(StripLeadingDots(what,1));
						break;
					case "scaled_passing_score":
						if (m_cmi['cmi.scaled_passing_score'] != "")
						{
							r = m_cmi['cmi.scaled_passing_score'].toString();
						}
						else
						{
							r = "";
							nErr = 403;
						}
						break;
					case "score":
						r = GetValueScore(gaScore,StripLeadingDots(what,2))
						break;
					case "session_time":
						r = "";
						nErr = 405;
						break;
					case "success_status":
						r = GetValueSuccessStatus();
						break;
					case "suspend_data":
						r = gSuspendData;
						break;
					case "time_limit_action":
						r = m_cmi['cmi.time_limit_action'];
						break;
					case "total_time":
						r = m_cmi['cmi.total_time'];
						break;
					default:
						r = ""; // GetValueByName(StripLeadingDots(what,1));
						nErr = 401;
						break;
				}
				break;
			case "adl":
				if (a[1] == "nav")
				{
					if (a[2] == "request")
					{
						r = GetValueByName(what);
						if (r == null)
						{
							r = "_none_";
						}
					}
					else if (a[2] == "request_valid")
					{
						switch(a[3])
						{
							case "continue":
								r = m_adl['adl.nav.request_valid.continue'];
								break;
							case "previous":
								r = m_adl['adl.nav.request_valid.previous'];
								break;
							case "choice":
								if (a[4].indexOf("{target=") == 0) 
									r = "unknown";
								else 
									nErr = 406;
								break;
							default:
								nErr = 406;
								break;
						}
					}
					else
					{
						nErr = 406;
					}
				}
				break;
			default:
				if (what == "")
				{
					nErr = 406;
				}
				else
				{
					nErr = 201;
				}
				break;
		}
	}
	if ((nErr == 0) && (m_lastError != 0)) 
		nErr = 0;
	m_lastError = nErr;
	if (m_logAPI) traceAPI("GetValue", what, null, r+"");
	return r + "";
}

function _Initialize(parm)
{
	m_lastError = 0;
	var r = "true";
	
	if (m_status == 0)
	{
		//if (m_trackId == -1)
		//	RemoteCall("Initialize", "", "");
			
		r = "true";
		m_status = 1;
	}
	else if (m_status < 3)
	{
		r = "false";
		m_lastError = 103;
	}
	else if (m_status > 2)
	{
		r = "false";
		m_lastError = 104;
	}
	if (m_logAPI) traceAPI("Initialize", parm, null, r+"");
	
	return r + "";
}

function _Terminate(parm)
{
	m_lastError = 0;
	var r = null;
	if (m_status == 0)
	{
		r = "false";
		m_lastError = 112;
	}
	else if (m_status == 1)
	{
		if (m_trackId > 0)
		{
			var http = InvokeWebMethod("Terminate");
		
			if (http.status == 200)
			{
				r = "true";
				m_status = 3;
			
				switch(m_AfterTerminate)
				{
					case "continue":
						goNext();
						break;
					case "previous":
						goPrevious();
						break;
					case "exit":
					case "exitAll":
					case "abandon":
					case "abandonAll":
						document.location.href = "about:blank";
						break;
				}
			}
			else
			{
				SetLastError(http);
				r = "false";
			}
		}
		else if (m_trackId == -1)
		{
			RemoteCall("Terminate", "", "");
			r = "true";
			m_status = 3;
		}
		else
		{
			r = "true";
			m_status = 3;
		}
	}
	else
	{
		r = "false";
		m_lastError = 113;
	}
	if (m_logAPI) traceAPI("Terminate", parm, null, r+"");
	return r + "";
}

function _Commit(parm)
{
	m_lastError = 0;
	var r = "true";
	if (m_status < 1)
	{
		m_lastError = 142;
		r = "false";
	}
	else if (m_status > 2)
	{
		m_lastError = 143;
		r = "false";
	}
	else
	{
		if (m_trackId > 0)
		{
			var http = InvokeWebMethod("SetValues");
			if (http.status == 200)
			{
				m_BatchActions = new Array();
				m_lastError = 0;
				r = "true";
			}
			else
			{
				SetLastError(http);
				r = "false";
			}
		}
		else if (m_trackId == -1)
		{
			RemoteCall("Commit", "", "");
			m_lastError = 0;
			r = "true";
		}
		else
		{
			m_lastError = 0;
			r = "true";
		}
	}
	if (m_logAPI) traceAPI("Commit", parm, null, r);
	return r;
}

function _GetLastError(parm)
{
	if (m_logAPI) traceAPI("GetLastError", parm, null, m_lastError + "");
  return m_lastError + "";
}

function _GetErrorString(parm)
{
	var r = null;
	var n = parseInt(parm);
	if (isNaN(n))
	{
		m_lastError = 406;
		if (m_logAPI) traceAPI("GetErrorString", parm, null, "");
		return "";
	}
	switch (n)
	{
		case 101: r = "General exception"; break;
		case 102: r = "General initialization failure"; break;
		case 103: r = "Already initialized"; break;
		case 104: r = "Content instance terminated"; break;
		case 111: r = "General termination failure"; break;
		case 112: r = "Termination before initialization"; break;
		case 113: r = "Termination after termination"; break;
		case 122: r = "Retrieve data before initialization"; break;
		case 123: r = "Retrieve data after termination"; break;
		case 132: r = "Store data before initialization"; break;
		case 133: r = "Store data after termination"; break;
		case 142: r = "Commit before initialization"; break;
		case 143: r = "Commit after termination"; break;
		case 201: r = "General argument error"; break;
		case 302: r = "General get failure"; break;
		case 351: r = "General set failure"; break;
		case 391: r = "General commit failure"; break;
		case 401: r = "Undefined data model element"; break;
		case 402: r = "Unimplemented data model element"; break;
		case 403: r = "Data model element value not initialized"; break;
		case 404: r = "Data model element is read only"; break;
		case 405: r = "Data model element is write only"; break;
		case 406: r = "Data model element type mismatch"; break;
		case 407: r = "Data model element value out of range"; break;
		case 408: r = "Data model element dependency not established"; break;
		default: r = "General exception"; break;
	}
	if (m_logAPI) traceAPI("GetErrorString", parm, null, r + "");
	return r + "";
}

function _GetDiagnostic(parm)
{
	if (m_logAPI) traceAPI("GetDiagnostic", parm, null, "Adopted SCORM 2004 SCO player from Claude Ostyn");

	return "Adopted SCORM 2004 SCO player from Claude Ostyn";
}

var API_1484_11 = null;

function InitAPI()
{
	API_1484_11 = new Object();
	API_1484_11.SetValue = _SetValue;
	API_1484_11.GetValue = _GetValue;
	API_1484_11.Initialize = _Initialize;
	API_1484_11.Terminate = _Terminate;
	API_1484_11.Commit = _Commit;
	API_1484_11.GetLastError = _GetLastError;
	API_1484_11.GetErrorString = _GetErrorString;
	API_1484_11.GetDiagnostic = _GetDiagnostic;
}

function InvokeWebMethod(method)
{
	var http = CreateHttpRequest();
	var url = m_trackService + "/" + method;
	var body = "trackId=" + m_trackId;
	
	if ((method == "Terminate") || (method == "SetValues"))
	{
		if (m_BatchActions.length > 0)
		{
			for(var i=0; i<m_BatchActions.length; i++)
			{
				body += "&names=" + encodeURI(m_BatchActions[i].nam) + "&values=" + encodeURI(m_BatchActions[i].val);
			}
		}
		else
		{
			body += "&names=&values=";
		}
	}

	http.open("POST", url , false);
	http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
	http.send(body);

	return http;
}

function CreateHttpRequest()
{
	var xmlHttp = null;
		
	try { xmlHttp = new XMLHttpRequest(); }
	catch(eout)
	{
		try { xmlHttp = new ActiveXObject("MSXML2.XMLHTTP.3.0"); }
		catch(ein){ xmlHttp = new ActiveXObject("MSXML2.XMLHTTP"); }
	}
	return xmlHttp;
}

function SetLastError(http)
{
	var error = http.getResponseHeader("SCOError");
	if (error == "")
		error = "101";		// set general exception
		
	m_lastError = parseInt(error);
}

var m_waitingUris = new Array();
var m_frameUsage = 0;
var m_unloadingState = -1; /* unloadingState: -1: Not unloadind, 0: Unloading, still no calls, 1: Unloading, there were calls while unloading, -2: I've asked to click cancel, don't ask again */
var m_currentUri = "";
var m_maxUriLength = 1800;

function RemoteCall(method, par1, par2)
{
	var uri;
	var newArgs = 'm=' + metEncode(method) + '&p1=' + cmiEncode(par1) + '&p2=' + encodeURIComponent(par2);
	
	if (method != "SetValue")
	{
		if (m_unloadingState == 0)
			m_unloadingState = 1;
		if (m_currentUri.length == 0)
			uri = m_trackService + '#' + newArgs;
		else
			uri = m_currentUri + '&' + newArgs;
		m_currentUri = "";
	}
	else
	{
		if (m_currentUri.length == 0)
		{
			m_currentUri = m_trackService + '#' + newArgs;
		}
		else
		{
			// IE has url limit of ~2000
			if (m_browserInfo.isIE && ((m_currentUri.length + newArgs.length + 1) > 1800) && (m_unloadingState == -1))
			{
				uri = m_currentUri;
				m_currentUri = m_trackService + '#' + newArgs;
			}
			else
			{
				m_currentUri = m_currentUri + '&' + newArgs;
			}
		}
	}
	
	if (uri)
	{
		SendUri(uri);
		if (m_browserInfo.showAlert && (method == "Commit") && (m_unloadingState == 1))
			alert(m_browserInfo.alertMessage);
	}
}

function SendUri(uri)
{
	if (m_frameUsage != 0)
	{
		m_waitingUris.push(uri);
	}
	else
	{
		m_frameUsage = 1;
		frames['stub'].location.href = uri;
	}
}

function frame_OnLoad()
{
	if (m_frameUsage == 1)
	{
		m_frameUsage = 2;
		frames['stub'].location.href = "about:blank";
	}
	else if (m_frameUsage == 2)
	{
		if (m_waitingUris.length > 0)
		{
			m_frameUsage = 1;
			frames['stub'].location.href = m_waitingUris.splice(0,1);
		}
		else
		{
			m_frameUsage = 0;
		}
	}
}

function frameset_OnBeforeUnload()
{
	if (m_browserInfo.isIE)
	{
		if (m_unloadingState != -2)
		{
			m_unloadingState = -2;
			window.frames['content'].location.href = "about:blank";
			if (m_browserInfo.showAlert)
				return m_browserInfo.alertMessage;
		}
	}
	else
	{
		m_unloadingState = 0;
	}
}

// Encoding-decoding section. To minimize uri size, cmi values are encoded...

function metEncode(met)
{
	if (met == "Initialize")
		return "I";
	if (met == "Terminate")
		return "F";
	if (met == "Commit")
		return "C";
	if (met == "SetValue")
		return "S";
	return met;
}

function cmiEncode(name)
{
	var retVal = "";
	prepareEncode();
	var a = name.split(".");
	for (var i=0; i<a.length; i++)
	{
		if (retVal.length > 0)
			retVal += ".";
			
		if (nm[a[i]])
			retVal += nm[a[i]];
		else
			retVal += a[i];
	}
	return retVal;
}

function cmiDecode(name)
{
	var retVal = "";
	prepareDecode();
	var a = name.split(".");
	for (var i=0; i<a.length; i++)
	{
		if (retVal.length > 0)
			retVal += ".";
			
		if (nmr[a[i]])
			retVal += nmr[a[i]];
		else
			retVal += a[i];
	}
	return retVal;
}

var nm = new Array;
var nmr = new Array;

function prepareDecode()
{
	if (nmr['D'])
		return;
	prepareEncode();
	for (var k in nm)
		nmr[nm[k]] = k;
}

function prepareEncode()
{
	if (nm['adl'])
		return;
	nm['adl'] 	='D';
	nm['audio_captioning'] 	='E';
	nm['audio_level'] 	='F';
	nm['choice'] 	='G';
	nm['cmi'] 	='C';
	nm['comment'] 	='I';
	nm['comments_from_learner'] 	='J';
	nm['comments_from_lms'] 	='K';
	nm['completion_status'] 	='L';
	nm['completion_threshold'] 	='M';
	nm['continue'] 	='N';
	nm['correct_responses'] 	='O';
	nm['credit'] 	='P';
	nm['delivery_speed'] 	='Q';
	nm['description'] 	='R';
	nm['entry'] 	='S';
	nm['exit'] 	='T';
	nm['id'] 	='U';
	nm['interactions'] 	='V';
	nm['language'] 	='W';
	nm['latency'] 	='X0';
	nm['launch_data'] 	='X1';
	nm['learner_id'] 	='X2';
	nm['learner_name'] 	='X3';
	nm['learner_preference'] 	='X4';
	nm['learner_response'] 	='X5';
	nm['location'] 	='X6';
	nm['max'] 	='X7';
	nm['max_time_allowed'] 	='X8';
	nm['min'] 	='X9';
	nm['mode'] 	='Y0';
	nm['nav'] 	='Y1';
	nm['objectives'] 	='Y2';
	nm['pattern'] 	='Y3';
	nm['previous'] 	='Y4';
	nm['progress_measure'] 	='Y5';
	nm['raw'] 	='Y6';
	nm['request'] 	='Y7';
	nm['request_valid'] 	='Y8';
	nm['result'] 	='Y9';
	nm['scaled'] 	='Z0';
	nm['scaled_passing_score'] 	='Z1';
	nm['score'] 	='Z2';
	nm['session_time'] 	='Z3';
	nm['success_status'] 	='Z4';
	nm['suspend_data'] 	='Z5';
	nm['time_limit_action'] 	='Z6';
	nm['timestamp'] 	='Z7';
	nm['total_time'] 	='Z8';
	nm['type'] 	='Z9';
	nm['weighting'] 	='A';
}
