/*  ----------------------------------------------------------------------------
 *  Development Notes
 *  ----------------------------------------------------------------------------
	
	Monday, 6 July 2009 15:23:52

 *
 *  ToDo:
 *      # Any time I write to suspend_data, I need to check the data will fit.
 *      # Change things so that version != 1.3 doesn't automatically imply 1.2
 *      # Test for and suspend non supported attributes individually.
 *      # Add asynchronous support to all methods
 *      # Make latency calculation routines the same for setTime and setInteraction
 *      # Implement the code to attach objective IDs to interactions
 *      # Implement description support for interactions
 *      # Implement correct response support for interactions
*/

// Initialise our namespace
if(Object.isUndefined(CADRE)) var CADRE = {};
CADRE.scorm = ('scorm' in CADRE) ? CADRE.scorm : {};

CADRE.scorm.API = function() {
  var Initialized = null, Terminated = null; Completed = null;

  var API = function(w,r) {w=w?w:window; r=r?r:100;
  	while(!('API' in w) && !('API_1484_11' in w) && ('parent' in w) && w.parent != w) {
  		if(r--) w = w.parent;
  		else {
  		  document.fire("scorm:error",{code:"-1",string:"API Not Found - Too Deep"});
  		  return {};
  		}
  	}
  	
  	if('API_1484_11' in w) return w.API_1484_11;
  	if('API' in w) return w.API;
  	if('opener' in w && w.opener != null) return arguments.callee(w.opener,r);
  	
	  document.fire("scorm:error",{code:"-1",string:"API Not Found"});
  	return {};
  }();
    
  var scormAPI = {
    OK: Object.keys(API).length ? true : false,
    INIT: 0,
    Initialize: function(p) { p=p?p:'';
      if(!this.OK) return 'false';
		  document.fire("scorm:notice",{string:"Initialize('"+p+"')"});
      if(Initialized != null) return Initialized;
      if('Initialize' in API) {
        Initialized = API.Initialize(p);
        if(this.INIT=Number(API.GetLastError()))
      		document.fire("scorm:error",{method: "Initialize", param: p, code: API.GetLastError(),string: API.GetErrorString(API.GetLastError()),diagnostic: API.GetDiagnostic('')});
        return Initialized;
      }
      if('LMSInitialize' in API) {
        Initialized = API.LMSInitialize(p);
        if(Number(API.LMSGetLastError()))
      		document.fire("scorm:error",{method: "LMSInitialize", param: p, code: API.LMSGetLastError(),string: API.LMSGetErrorString(API.LMSGetLastError()),diagnostic: API.LMSGetDiagnostic('')});
        return Initialized;
      }
  		document.fire("scorm:error",{string: "Initialize not found in API"});
      return Initialized = 'false';
    },
    Terminate: function(p) { p=p?p:'';
      if(!this.OK) return 'false';
		  document.fire("scorm:notice",{string:"Terminate('"+p+"')"});
      if(Terminated != null) return Terminated;
      if('Terminate' in API) {
        Terminated = API.Terminate(p);
        if(Number(API.GetLastError()))
      		document.fire("scorm:error",{method: "Terminate", param: p, code: API.GetLastError(),string: API.GetErrorString(API.GetLastError()),diagnostic: API.GetDiagnostic('')});
        return Terminated;
      }
      if('LMSFinish' in API) {
        Terminated = API.LMSFinish(p);
        if(Number(API.LMSGetLastError()))
      		document.fire("scorm:error",{method: "LMSFinish", param: p, code: API.LMSGetLastError(),string: API.LMSGetErrorString(API.LMSGetLastError()),diagnostic: API.LMSGetDiagnostic('')});
        return Terminated;
      }
  		document.fire("scorm:error",{string: "Terminate/Finish not found in API"});
      return Finished = 'false';

    },
    GetValue: function(p) {
      if(!this.OK || !p) return '';
		  document.fire("scorm:notice",{string:"GetValue('"+p+"')"});
      if('GetValue' in API) {
        var _ = API.GetValue(p);
        if(Number(API.GetLastError()))
      		document.fire("scorm:error",{method: "GetValue", param: p, code: API.GetLastError(),string: API.GetErrorString(API.GetLastError()),diagnostic: API.GetDiagnostic('')});
        return _;
      }
      if('LMSGetValue' in API) {
        var _ = API.LMSGetValue(p);
        if(Number(API.LMSGetLastError()))
      		document.fire("scorm:error",{method: "LMSGetValue", param: p, code: API.LMSGetLastError(),string: API.LMSGetErrorString(API.LMSGetLastError()),diagnostic: API.LMSGetDiagnostic('')});
        return _;
      }
  		document.fire("scorm:error",{string: "GetValue not found in API"});
      return '';
    },
    SetValue: function(p,v) {
      if(!this.OK || !p) return 'false';
      if(Completed == null) {                
        if(scormAPI.GetValue('cmi._version') == "1.0")
          Completed = /completed|passed|failed/.test(scormAPI.GetValue('cmi.completion_status')+scormAPI.GetValue('cmi.success_status'));
        if(scormAPI.GetValue('cmi._version') == "3.4")
          Completed = /completed|passed|failed/.test(scormAPI.GetValue('cmi.core.lesson_status'));
      }
      if(Completed) return 'false';
		  document.fire("scorm:notice",{string:"SetValue('"+p+"','"+v+"')"});
      if('SetValue' in API) {
        var _ = API.SetValue(p,v);
        if(Number(API.GetLastError()))
      		document.fire("scorm:error",{method: "SetValue", param: p, value: v, code: API.GetLastError(),string: API.GetErrorString(API.GetLastError()),diagnostic: API.GetDiagnostic('')});
        return _;
      }
      if('LMSSetValue' in API) {
        var _ = API.LMSSetValue(p,v);
        if(Number(API.LMSGetLastError()))
      		document.fire("scorm:error",{method: "LMSSetValue", param: p, value: v, code: API.LMSGetLastError(),string: API.LMSGetErrorString(API.LMSGetLastError()),diagnostic: API.LMSGetDiagnostic('')});
        return _;
      }
  		document.fire("scorm:error",{string: "SetValue not found in API"});
      return 'false';
    },
    Commit: function(p) { p=p?p:'';
      if(!this.OK) return 'false';
		  document.fire("scorm:notice",{string:"Commit('"+p+"')"});
      if('Commit' in API) {
        var _ = API.Commit(p);
        if(Number(API.GetLastError()))
      		document.fire("scorm:error",{method: "Commit", param: p, code: API.GetLastError(),string: API.GetErrorString(API.GetLastError()),diagnostic: API.GetDiagnostic('')});
        return _;
      }
      if('LMSCommit' in API) {
        var _ = API.LMSCommit(p);
        if(Number(API.LMSGetLastError()))
      		document.fire("scorm:error",{method: "LMSCommit", param: p, code: API.LMSGetLastError(),string: API.LMSGetErrorString(API.LMSGetLastError()),diagnostic: API.LMSGetDiagnostic('')});
        return _;
      }
  		document.fire("scorm:error",{string: "Commit not found in API"});
      return 'false';
    },
    GetLastError: function() {
      if(!this.OK) return '101';
		  document.fire("scorm:notice",{string:"GetLastError()"});
      if('GetLastError' in API)
        return String(API.GetLastError());
      if('LMSGetLastError' in API)
        return String(API.LMSGetLastError());
    	document.fire("scorm:error",{string: "GetLastError not found in API"});
      return null;
    },
    GetErrorString: function(n) {
      if(!this.OK) return 'General Exception';
      if(!n)       return '';
		  document.fire("scorm:notice",{string:"GetErrorString('"+n+"')"});
      if('GetErrorString' in API)
        return API.GetErrorString(n);
      if('LMSGetErrorString' in API)
        return API.LMSGetErrorString(n);
      document.fire("scorm:error",{string: "GetErrorString not found in API"});
      return null;
    },
    GetDiagnostic: function(p)	{ p=p?p:'';
      if(!this.OK) return "API Not Found";
		  document.fire("scorm:notice",{string:"GetDiagnostic('"+p+"')"});
      if('GetGetDiagnostic' in API)
        return API.GetDiagnostic(p);
      if('LMSGetDiagnostic' in API)
        return API.LMSGetDiagnostic(p);
      document.fire("scorm:error",{string: "GetDiagnostic not found in API"});
      return null;
    }
  };
  
  // SCORM 1.2 Mappings
  scormAPI.LMSInitialize      = scormAPI.Initialize;
  scormAPI.LMSFinish          = scormAPI.Terminate;
  scormAPI.LMSGetValue        = scormAPI.GetValue;
  scormAPI.LMSSetValue        = scormAPI.SetValue;
  scormAPI.LMSCommit          = scormAPI.Commit;
  scormAPI.LMSGetLastError    = scormAPI.GetLastError;
  scormAPI.LMSGetErrorString  = scormAPI.GetErrorString;
  scormAPI.LMSGetDiagnostic   = scormAPI.GetDiagnostic;
  
  return scormAPI;
}();

// ----------------------------------------------------------------------------
// -------------------------- SCORM Helper Functions --------------------------
// ----------------------------------------------------------------------------

// ------------------------- Configuration Parameters -------------------------
CADRE.scorm.config = {
  autosuspend: true
};
// ----------------------------------------------------------------------------


/* ---------------------------- Initialise the API ----------------------------
 * config: A configuration object.  The following parameters are currently
 * supported:
 * {
 *   // By default the API auto suspends fields not supported in the current LMS
 *   // to suspend_data.  Setting autosuspend to false disables this feature.
 *   autosuspend: false
 * }
 * callback: callback should be a string that evaluates to a function or an
 * actual function.  If you pass a callback, this function will always return
 * true.
*/

CADRE.scorm.initialize = function(config,callback) {
  if(Object.keys(config).length && 'autosuspend' in config)
    CADRE.scorm.config.autosuspend = config.autosuspend;
  
  var api = CADRE.scorm.API;
  if(!api.OK) return null; // No API

  // If callback is a string evaluate it
  if(Object.isString(callback))  {
    try {eval("callback = "+callback);}
    catch (e) {document.fire("scorm:error",{method: "initialize", param: "{callback:"+callback+"}", code: "201",string: "Invalid Argument",diagnostic: "Error in Callback"});}
  }

  // Wrap the initialization code in a function so it can be deferred
  function initialize() {
    var initialize = CADRE.scorm.API.Initialize('');
    //  First Call              Subsequent Call SCORM 1.3  Subsequent Call SCORM 1.2
    if(/true/.test(initialize) || (/103/.test(api.INIT)) || (/101/.test(api.INIT)))  initialize = 'true';  

    if(Object.isFunction(callback)) callback(initialize);
    return initialize;
  }

  // if callback is a function then call initialize asynchronously and return true
  if(Object.isFunction(callback)) {initialize.defer();return true;}

  // otherwise, call initialize synchronously
  return initialize();
};
// ----------------------------------------------------------------------------


// ------------------- Simple Getters for Read Only properties ----------------
CADRE.scorm.getVersion     = function() {return CADRE.scorm.API.GetValue('cmi._version') == "1.0" ? "1.3" : (CADRE.scorm.API.GetValue('cmi._version') == "3.4" ? "1.2" : "");};
CADRE.scorm.getLaunchData  = function() {return (/[^\s]/.test(CADRE.scorm.API.GetValue('cmi.launch_data'))) ? (CADRE.scorm.API.GetValue('cmi.launch_data').isJSON() ? CADRE.scorm.API.GetValue('cmi.launch_data').evalJSON(true) : (""+CADRE.scorm.API.GetValue('cmi.launch_data').gsub(/\\\"/,"\"").isJSON() ? CADRE.scorm.API.GetValue('cmi.launch_data').gsub(/\\\"/,"\"").evalJSON(true) : CADRE.scorm.API.GetValue('cmi.launch_data'))) : "";};
CADRE.scorm.getLearnerId   = function() {return CADRE.scorm.API.GetValue('cmi._version') == "1.0" ? CADRE.scorm.API.GetValue('cmi.learner_id')    : (CADRE.scorm.API.GetValue('cmi._version') == "3.4" ? CADRE.scorm.API.GetValue('cmi.core.student_id')  : "");};
CADRE.scorm.getLearnerName = function() {return CADRE.scorm.API.GetValue('cmi._version') == "1.0" ? CADRE.scorm.API.GetValue('cmi.learner_name')  : (CADRE.scorm.API.GetValue('cmi._version') == "3.4" ? CADRE.scorm.API.GetValue('cmi.core.student_name'): "");};
CADRE.scorm.getCredit      = function() {return CADRE.scorm.API.GetValue('cmi._version') == "1.0" ? CADRE.scorm.API.GetValue('cmi.credit')        : (CADRE.scorm.API.GetValue('cmi._version') == "3.4" ? CADRE.scorm.API.GetValue('cmi.core.credit')      : "");};
CADRE.scorm.getEntry       = function() {return CADRE.scorm.API.GetValue('cmi._version') == "1.0" ? CADRE.scorm.API.GetValue('cmi.entry')         : (CADRE.scorm.API.GetValue('cmi._version') == "3.4" ? CADRE.scorm.API.GetValue('cmi.core.entry')       : "");};
CADRE.scorm.getMode        = function() {return CADRE.scorm.API.GetValue('cmi._version') == "1.0" ? CADRE.scorm.API.GetValue('cmi.mode')          : (CADRE.scorm.API.GetValue('cmi._version') == "3.4" ? CADRE.scorm.API.GetValue('cmi.core.lesson_mode') : "");};
// ----------------------------------------------------------------------------


/* ------------------------ Set the Overall Score --------------------------
 * data: An object containing the following:
 * {
 *   score_raw: real(10,7),
 *   score_max: real(10,7),
 *   score_min: real(10,7),
 * }
*/
CADRE.scorm.setScore = function(data) {
  var api=CADRE.scorm.API;
  if(!api.OK) return null; // No API

  var e=[];  
  var scoreAttribute = (CADRE.scorm.getVersion() == "1.3") ? "cmi.score" : "cmi.core.score";
  var scoreChildren = api.GetValue(scoreAttribute+"._children");

  // Fetch any previously set suspend data  
  var sd = {};
  if(CADRE.scorm.config.autosuspend) {
    sd = api.GetValue("cmi.suspend_data");
    if(sd.isJSON()) {sd = sd.evalJSON();}
    else sd = {};
  }
  if(!('score' in sd)) sd.score = {};

  // Save the score attributes that the LMS supports
  $H(data).each(function(att)  {
    if(scoreChildren.indexOf(att.key.replace(/^.+_/,""))!=-1)  {
      if(/(^-{0,1}[0-9]{1,10}$)|(^-{0,1}[0-9]{1,10}\.[0-9]{1,7}$)/.test(String(att.value)))  {
        api.SetValue(scoreAttribute+"."+att.key.replace(/^.+_/,""),String(att.value));
        if(api.GetValue(scoreAttribute+"."+att.key.replace(/^.+_/,"")) !== String(att.value))
          if(CADRE.scorm.config.autosuspend) sd.score[att.key.replace(/^.+_/,"")] = att.value;
          else e.push('false');
      } else {
        if(CADRE.scorm.getVersion() == "1.3")
          document.fire("scorm:error",{method: "setScore", param: "{"+att.key+":"+att.value+"}", code: "406",string: "Data Model Element Type Mismatch",diagnostic: "Value should be real(10,7)"});
        else
          document.fire("scorm:error",{method: "setScore", param: "{"+att.key+":"+att.value+"}", code: "405",string: "Incorrect Data Type",diagnostic: "Value should be real(10,7)"});
        e.push('false');
      }
    } else if(CADRE.scorm.config.autosuspend)  {
      if(/(^-{0,1}[0-9]{1,10}$)|(^-{0,1}[0-9]{1,10}\.[0-9]{1,7}$)/.test(String(att.value)))
        sd.score[att.key.replace(/^.+_/,"")] = att.value;
      else {
        if(CADRE.scorm.getVersion() == "1.3")
          document.fire("scorm:error",{method: "setScore", param: "{"+att.key+":"+att.value+"}", code: "406",string: "Data Model Element Type Mismatch",diagnostic: "Value should be real(10,7)"});
        else
          document.fire("scorm:error",{method: "setScore", param: "{"+att.key+":"+att.value+"}", code: "405",string: "Incorrect Data Type",diagnostic: "Value should be real(10,7)"});
        e.push('false');
      }
    }
  });
  
  // Suspend any score attributes the LMS doesn't support
  if(CADRE.scorm.config.autosuspend && $H(sd.score).keys().length)  {
    e.push(api.SetValue("cmi.suspend_data",Object.toJSON(sd)));
  }

  return (e.indexOf('false')<0) ? true : false;
};
// ----------------------------------------------------------------------------

/* ------------------------ Get the Overall Score -------------------------- */
CADRE.scorm.getScore = function() {
  var api = CADRE.scorm.API;
  if(!api.OK) return null; // No API
  
  var _, scoreAttribute = (CADRE.scorm.getVersion() == "1.3") ? "cmi.score" : "cmi.core.score";
  
  // Grab any suspended score data
  var sd = {};
  if(CADRE.scorm.config.autosuspend) {
    sd = api.GetValue("cmi.suspend_data");
    if(sd.isJSON()) {sd = sd.evalJSON();}
    else sd = {};
  }
  if(!('score' in sd))  sd.score = {};
  
  // Assemble our score object
  var score = {score_raw:'',score_min:'',score_max:''};
  if(/^[0-9]+$/.test(_ = api.GetValue(scoreAttribute+".raw"))) score.score_raw = Number(_);
  else if('raw' in sd.score) score.score_raw = sd.score.raw;
  if(/^[0-9]+$/.test(_ = api.GetValue(scoreAttribute+".min"))) score.score_min = Number(_);
  else if('min' in sd.score) score.score_min = sd.score.min;
  if(/^[0-9]+$/.test(_ = api.GetValue(scoreAttribute+".max"))) score.score_max = Number(_);
  else if('max' in sd.score) score.score_max = sd.score.max;
  return score;
};
// ----------------------------------------------------------------------------

/* ------------------------ Set the Time On Activity --------------------------
 * Scorm has 2 attributes to do with time, session_time and total_time.
 * session_time is write only and as the name implies, is the time the user has
 * spent on the activity this session.  total_time is read only and is the total
 * time the user has spent on the activity across all sessions.
 *
 * To normaliz/simplify this, scorm.setTime and scorm.getTime set and return
 * total seconds. Maximum Time supported is 35999999.99 (9999 Hours,59 Minutes,59.99 Seconds)
 *
 * time: REAL(8.2)         // Total time on activity in seconds;
*/
CADRE.scorm.setTime = function(time) {
  var api=CADRE.scorm.API;
  if(!api.OK) return null; // No API

  time = Object.isNumber(time) ? time : 0;

  var session_time = (CADRE.scorm.getVersion() == "1.3") ? "cmi.session_time" : "cmi.core.session_time";
  
  // Fetch total_time and convert to seconds
  if(CADRE.scorm.getVersion() == "1.2")  {
    var totalTime = api.GetValue("cmi.core.total_time");
    var _ = totalTime.match(/(\d*\.{0,1}\d+)/g);
    var __ = 0;
    if(Object.isArray(_)) while(_.length<3) _.unshift('00');
    else _ = ["0000","00","00.00"];
    for(var i=0;i<_.length;i++)
      __+= (Math.pow(60,i))*Number(_[_.length-(i+1)]);
    totalTime = __;
  }

  if(CADRE.scorm.getVersion() == "1.3")  {
    var totalTime = api.GetValue("cmi.total_time");
    var __ = 0;
    var _ = totalTime.replace(/.+T(.+)/,"$1");    
    if(/\dH/.test(_)) __+= Number(_.replace(/.*?(\d+)H.*/,"$1"))*Math.pow(60,2);
    if(/\dM/.test(_)) __+= Number(_.replace(/.*?(\d+)M.*/,"$1"))*60;
    if(/\dS/.test(_)) __+= Number(_.replace(/(.*?(\d+\.\d+)S.*)|(.*?(\d+)S.*)/,"$2$4"));
    totalTime = __;
  }
  
  // Croak if the specified time is less than the current total_time
  if(time < totalTime)  {
    document.fire("scorm:error",{method: "setTime", param: time, code: "406",string: "Data Model Element Type Mismatch",diagnostic: "time ("+time+") is less than total_time("+totalTime+")"});
    return false;
  }
  
  // Set the session_time to zero if asked to
  if(totalTime===time) {
    if(CADRE.scorm.getVersion() == "1.2")    
      return api.SetValue(session_time,"00:00:00.00");
    if(CADRE.scorm.getVersion() == "1.3")    
      return api.SetValue(session_time,"PT00H00M00.00S");
  }
  
  // Pads Single characters with a leading 0
  function zeropad(s) {return String(s).length<2 ? "0"+String(s) : s;}
    
  // Convert seconds to hours, minutes, seconds
  time = time-totalTime;
  if(CADRE.scorm.getVersion() == "1.3") var sessionTime = "PT";
  if(CADRE.scorm.getVersion() == "1.2") var sessionTime = "";

  // Hours
  if(time>=Math.pow(60,2)) {
    sessionTime += zeropad(Math.floor(time/Math.pow(60,2)));
    time = time-(Math.floor(time/Math.pow(60,2))*Math.pow(60,2));
  } else sessionTime += "00";
  if(CADRE.scorm.getVersion() == "1.3") sessionTime += "H";

  // Minutes
  if(CADRE.scorm.getVersion() == "1.2") sessionTime += ":";
  if(time>=60) {
    sessionTime += zeropad(Math.floor(time/60));
    time = time-(Math.floor(time/60)*60);
  } else sessionTime += "00";
  if(CADRE.scorm.getVersion() == "1.3") sessionTime += "M";

  // Seconds
  if(CADRE.scorm.getVersion() == "1.2") sessionTime += ":";
  sessionTime+=zeropad(Math.floor(time));

  // Hundredths
  sessionTime+= "."+zeropad(Math.round((time-Math.floor(time))*100));
  if(CADRE.scorm.getVersion() == "1.3") sessionTime += "S";
  
      
  return api.SetValue(session_time,sessionTime) == 'true' ? true: false;
};
// ----------------------------------------------------------------------------

/* ------------------------------ Get the Time -------------------------------- */
CADRE.scorm.getTime = function() {
  var api=CADRE.scorm.API;
  if(!api.OK) return null; // No API
  
  // Fetch total_time and convert to seconds
  if(CADRE.scorm.getVersion() == "1.2")  {
    var totalTime = api.GetValue("cmi.core.total_time");
    var _ = totalTime.match(/(\d*\.{0,1}\d+)/g);
    var __ = 0;
    if(Object.isArray(_)) while(_.length<3) _.unshift('00');
    else _ = ["0000","00","00.00"];
    for(var i=0;i<_.length;i++)
      __+= (Math.pow(60,i))*Number(_[_.length-(i+1)]);
    totalTime = __;
  }

  if(CADRE.scorm.getVersion() == "1.3")  {
    var totalTime = api.GetValue("cmi.total_time");
    var __ = 0;
    var _ = totalTime.replace(/.+T(.+)/,"$1");    
    if(/\dH/.test(_)) __+= Number(_.replace(/.*?(\d+)H.*/,"$1"))*Math.pow(60,2);
    if(/\dM/.test(_)) __+= Number(_.replace(/.*?(\d+)M.*/,"$1"))*60;
    if(/\dS/.test(_)) __+= Number(_.replace(/(.*?(\d+\.\d+)S.*)|(.*?(\d+)S.*)/,"$2$4"));
    totalTime = __;
  }
  
  return totalTime ? totalTime : 0;
};
// ----------------------------------------------------------------------------


/* ---------------------------- Set Suspended Data -------------------------
  Using this method you can store and retrieve arbitrary data.  The data parameter
  can be any data type you want as long as any string data within your object
  comprises is not outside the Universal Character Set (ISO/IEC 10646).

  This means you can't rely on binary data being stored safely.  If in doubt Base64
  encode your data first.
  
  if data is an empty string or undefined.  The data item pointed to by ID will be
  removed.
  
  id:   The string ID of this data object
  data: OPTIONAL
*/
CADRE.scorm.setData = function(id,data) {
  var api = CADRE.scorm.API;
  if(!api.OK) return null; // No API

  var e = 'false';
  
  if(typeof id==='undefined' || id==='') return false;
  
  // Grab existing suspended data
  var sd = api.GetValue("cmi.suspend_data");
  if(sd.isJSON()) {sd = sd.evalJSON();}
  else {sd = {};}
  if(!('data' in sd)) sd.data = {};

  // If id exists and data is undefined or '', remove it
  if(typeof data === 'undefined' || data==='') delete sd.data[id];
  else sd.data[id] = data;
  sd = Object.toJSON(sd);
  
  // If we have sufficient space, store the result
  if(CADRE.scorm.getVersion() == "1.2")
    if(sd.length<4096)
      e = api.SetValue("cmi.suspend_data",sd);
    else
      document.fire("scorm:error",{method: "setData", param: Object.toJSON(data), code: "405",string: "Incorrect Data Type",diagnostic: "Insufficient space to store data ("+sd.length+")"});
    
  if(CADRE.scorm.getVersion() == "1.3")
    if(sd.length<64000)
      e = api.SetValue("cmi.suspend_data",sd);
    else
      document.fire("scorm:error",{method: "setData", param: Object.toJSON(data), code: "406",string: "Data Model Element Type Mismatch",diagnostic: "Insufficient space to store data ("+sd.length+")"});
  
  return (e=='true') ? true : false;
};
// ----------------------------------------------------------------------------


/* ------------------------ Get Suspended Data --------------------------
   If you specify an ID that data item will be returned.  If you don't specify
   an ID, All data items will be returned
   
   id: OPTIONAL
*/
CADRE.scorm.getData = function(id) {
  api = CADRE.scorm.API;
  
  // Grab suspended data
  var sd = api.GetValue("cmi.suspend_data");
  if(sd.isJSON()) {sd = sd.evalJSON();}
  else {sd = {};}
  if(!('data' in sd)) sd.data = {};
  if(typeof id === 'undefined' || id === '') return sd.data;
  return typeof sd.data[id] !== 'undefined' ? sd.data[id] : '';

};
// ----------------------------------------------------------------------------



/* ---------------------------- Set the Lesson Status -------------------------
 * status: ["passed","failed","completed","incomplete","not attempted"]
*/
CADRE.scorm.setStatus = function(status) {
  var e = 'false', api = CADRE.scorm.API;
  
  if(CADRE.scorm.getVersion() == "1.3")  {
    if(/passed|failed/.test(status))
      e = api.SetValue("cmi.success_status",status);
    if(/completed|incomplete|not attempted/.test(status))
      e = api.SetValue("cmi.completion_status",status);
  }
  if(CADRE.scorm.getVersion() == "1.2")
    e = api.SetValue("cmi.core.lesson_status",status);

  return (e=='true') ? true : false;
};
// ----------------------------------------------------------------------------


/* ------------------------ Get the Lesson Status -------------------------- */
CADRE.scorm.getStatus = function() {
  api = CADRE.scorm.API;
  
  if(CADRE.scorm.getVersion() == "1.2")
    return api.GetValue("cmi.core.lesson_status");

  if(CADRE.scorm.getVersion() == "1.3")  {
    var success = api.GetValue("cmi.success_status");
    if(success && success != 'unknown') return success;
    var completion = api.GetValue("cmi.completion_status");
    if(completion && completion != 'unknown') return completion;
    if(success == 'unknown' && completion == 'unknown') return "not attempted";
  }
  
  return '';
};
// ----------------------------------------------------------------------------


/*  -------------------------- Set Objective ----------------------------------
    For consistant behaviour across implementations use

                             passed and failed
                                    OR
                completed and incomplete and not attempted

    Don't mix your nomenclature. A bug in Moodle (Build: 20081210) prevents me
    setting success_status or completion_status to 'unknown' in SCORM 1.3. So
    since I can't unset a status, if you send me a completion and a success
    status, both will remain in the LMS.
    
    Users who do use both nomenclature will find that in scorm 1.2 the system
    will retain and return the most recent value set, but in scorm 1.3, once
    set, the system will retain the most recent value from each nomenclature.
    
    To allow 1.3 users to maintain both nomenclature, getStatus will return one
    value if only one nomenclature has been used and 2 values separated by a
    bar (|) if both nomenclature have been used.
    
    When the Moodle bug is fix or you use a different LMS. Uncomment the lines
    below and the system will maintain a single status for 1.3 users just as it
    does for 1.2 users.

    id:   The string ID of this objective
    data: OPTIONAL { 
      score_raw: [0-100],
      score_max: [0-100],
      score_min: [0-100],
      status: ["passed","failed","completed","incomplete", "not attempted"]
    }
*/
CADRE.scorm.setObjective = function(id,data) {
  var api=CADRE.scorm.API;
  if(!api.OK) return null; // No API
  
  var c=0, o=[], e=[];
  
  while(api.GetValue("cmi.objectives."+c+".id") != '')
    o.push(api.GetValue("cmi.objectives."+(c++)+".id"));
  
  var idx = (o.indexOf(id)>=0) ? o.indexOf(id) : o.length;
  if(o.indexOf(id)<0) e.push(api.SetValue("cmi.objectives."+idx+".id",id));
    
  data = $H(data);
  data.each(function(d) {
    switch(d.key) {
      case 'score_raw': e.push(api.SetValue("cmi.objectives."+idx+".score.raw",d.value)); break;
      case 'score_max': e.push(api.SetValue("cmi.objectives."+idx+".score.max",d.value)); break;
      case 'score_min': e.push(api.SetValue("cmi.objectives."+idx+".score.min",d.value)); break;
    }
    if(CADRE.scorm.getVersion() == "1.3" && d.key=="status")  {
      if(/passed|failed/.test(d.value))  {
        /* Not supported in Moodle
        var _ = api.GetValue("cmi.objectives."+idx+".completion_status");
        if(_ && _ != 'unknown')
          e.push(api.SetValue("cmi.objectives."+idx+".completion_status",'unknown'));
        */
        e.push(api.SetValue("cmi.objectives."+idx+".success_status",d.value));
      }
      if(/completed|incomplete|not attempted/.test(d.value))  {
        /* Not supported in Moodle
        var _ = api.GetValue("cmi.objectives."+idx+".success_status");
        if(_ && _ != 'unknown')
          e.push(api.SetValue("cmi.objectives."+idx+".success_status",'unknown'));
        */
        e.push(api.SetValue("cmi.objectives."+idx+".completion_status",d.value));
      }
    }
    if(CADRE.scorm.getVersion() == "1.2" && d.key=="status")  {
      e.push(api.SetValue("cmi.objectives."+idx+".status",d.value));
    }
  });
  
  return (e.indexOf('false')<0) ? true : false;
};
// ----------------------------------------------------------------------------


/* ------------------------------ Get Objective -------------------------------
   id:   The string ID of this objective
*/
CADRE.scorm.getObjective = function(id) {
  var api=CADRE.scorm.API;
  if(!api.OK) return null; // No API
  
  var c=0, o=[];
  
  while(api.GetValue("cmi.objectives."+c+".id") != '')
    o.push(api.GetValue("cmi.objectives."+(c++)+".id"));
  
  var data = {score_raw:"",score_max:"",score_min:"",status:""};
  
  if(o.indexOf(id)>=0)  {
    var idx = o.indexOf(id);
    data.score_raw = api.GetValue("cmi.objectives."+idx+".score.raw");
    data.score_max = api.GetValue("cmi.objectives."+idx+".score.max");
    data.score_min = api.GetValue("cmi.objectives."+idx+".score.min");

    if(CADRE.scorm.getVersion() == "1.2")
      data.status = api.GetValue("cmi.objectives."+idx+".status");

    if(CADRE.scorm.getVersion() == "1.3")  {
      var _ = api.GetValue("cmi.objectives."+idx+".success_status");
      if(_ && _ != 'unknown') data.status = _;
      var _ = api.GetValue("cmi.objectives."+idx+".completion_status");
      if(_ && _ != 'unknown') data.status  = data.status ? data.status+='|'+_ : _;
    }
  }
  return data;
};
// ----------------------------------------------------------------------------


/* ------------------------- Set the Lesson Location --------------------------
   location: String representing the students location in the sco
             [255 characters Max]
*/
CADRE.scorm.setLocation = function(location) {
  var e = 'true', api = CADRE.scorm.API;

  if(CADRE.scorm.getVersion() == "1.3")
    e = api.SetValue("cmi.location",location);
  if(CADRE.scorm.getVersion() == "1.2")
    e = api.SetValue("cmi.core.lesson_location",location);
  
  return (e=='true') ? true : false;
};
// ----------------------------------------------------------------------------


/* ------------------------- Get the Lesson Location ----------------------- */
CADRE.scorm.getLocation = function() {
  api = CADRE.scorm.API;
  
  if(CADRE.scorm.getVersion() == "1.2")
    return api.GetValue("cmi.core.lesson_location");
  if(CADRE.scorm.getVersion() == "1.3")
    return api.GetValue("cmi.location");
  
  return '';
};
// ----------------------------------------------------------------------------


/* --------------------- Set the data for an interaction ----------------------
   Suggested usage is to use an id that describes the question, use fill-in as
   the type, response should contain the users chosen answer and result should
   contain the determined result. For portability IDs are limited to 255
   characters and Fill-in responses to 250 characters. Commas are reserved in
   responses for separating multiple answers.
   
   e.g. : setInteraction('Who_is_James_Boag',{type:'fill-in',response:'I am',result:'correct'})
   
  * SCORM 1.2 doesn't allow us to fetch existing interactions, but 1.3 does.
  * For consistancy, interactions in players (1.2 or 1.3) that don't support
  * getting interaction data will be suspended using suspend_data.  You can
  * stop this initializing with a config of {suspend:false}

  id:   The string ID[a-zA-Z0-9_-] of this objective. Max length 255 characters
  data: An object containing the following:
  {
   type:      [true-false,choice,fill-in,likert,matching,performance,sequencing,numeric],
   response:  {format determined by type}, // Max length 250 characters
   result:    [correct,incorrect,unanticipated,neutral,REAL(10.7)],
   timestamp: OPTIONAL YYYY-MM-DDTHH:MM:SS,  // In scorm 1.2 this will be truncated to HH:MM:SS in the reports
   latency:   OPTIONAL REAL(10.2)  // In seconds
  }
*/
CADRE.scorm.setInteraction = function(id,data) {
  var api=CADRE.scorm.API;
  if(!api.OK) return null; // No API

  var c=0, i=[], e=[], canGet=false, idx=-1;
  
  // See if we can fetch existing interactions
  // QUOTE: SCORM 3rd Ed. Run-Time Documentation
  // If the SCO invokes a GetValue() request where the index (n) is a number
  // larger than what the LMS is currently maintaining (e.g., the request
  // indicated an n value of 5 when there are only 3 interactions in the
  // array), then the LMS shall set the error code to 301 – General Get Failure
  // and return an empty characterstring.
  if(CADRE.scorm.getVersion() == "1.3")  {
    canGet=true;
    api.GetValue("cmi.interactions.0.id");
    canGet = canGet && (/^0$|^301$/.test(api.GetLastError())) ? true : false;
    api.GetValue("cmi.interactions.0.type");
    canGet = canGet && (/^0$|^301$/.test(api.GetLastError())) ? true : false;
    api.GetValue("cmi.interactions.0.learner_response");
    canGet = canGet && (/^0$|^301$/.test(api.GetLastError())) ? true : false;
    api.GetValue("cmi.interactions.0.result");
    canGet = canGet && (/^0$|^301$/.test(api.GetLastError())) ? true : false;
    if('timestamp' in data && data.timestamp !=='')  {
      if(CADRE.scorm.getVersion() == "1.2")
        api.GetValue("cmi.interactions.0.time");
      if(CADRE.scorm.getVersion() == "1.3")
        api.GetValue("cmi.interactions.0.timestamp");
      canGet = canGet && (/^0$|^301|^403$/.test(api.GetLastError())) ? true : false;
    }
    if('latency' in data && data.latency !=='')  {
      api.GetValue("cmi.interactions.0.latency");
      canGet = canGet && (/^0$|^301|^403$/.test(api.GetLastError())) ? true : false;
    }
  }

  if(canGet)  {
    // If we can get interaction data, lookup the interaction ID or
    // set it if it hasn't been set before 
    while(api.GetValue("cmi.interactions."+c+".id") != '')
      i.push(api.GetValue("cmi.interactions."+(c++)+".id"));
    idx = (i.indexOf(id)>=0) ? i.indexOf(id) : i.length;
    if(i.indexOf(id)<0) e.push(api.SetValue("cmi.interactions."+idx+".id",id));
  } else if(CADRE.scorm.config.autosuspend)  {
    // Otherwise if suspend is enabled look for the interaction in the suspend data
    var sd = api.GetValue("cmi.suspend_data");
    if(sd.isJSON()) {
      sd = sd.evalJSON();
      if('interactions' in sd) {
        sd.interactions.each(function(rec) {if(rec.id == id) idx = rec.n;});
        if(idx<0) e.push(api.SetValue("cmi.interactions."+sd.interactions.length+".id",id));
        idx = (idx<0) ? sd.interactions.length : idx;
      } else {idx=0; e.push(api.SetValue("cmi.interactions."+idx+".id",id));}
    } else {idx=0; e.push(api.SetValue("cmi.interactions."+idx+".id",id));}
  } else {
    // If suspend is disabled the best we can do is append a new interaction
    c = api.GetValue("cmi.interactions._count");
    if(Object.isNumber(c) && !Number(api.GetLastError())) idx=c;
    // If all else fails set the index to 0 and hope for the best
    idx = (idx<0) ? 0 : idx;
    e.push(api.SetValue("cmi.interactions."+idx+".id",id));
  }

  // SCORM 1.2 uses wrong instead of incorrect
  data.result = (CADRE.scorm.getVersion() == "1.2" && data.result == "incorrect") ? "wrong" : data.result;
    
  // Set the interaction data
  e.push(api.SetValue("cmi.interactions."+idx+".type",data.type));
  e.push(api.SetValue("cmi.interactions."+idx+".result",data.result));
  if(CADRE.scorm.getVersion() == "1.2")
    e.push(api.SetValue("cmi.interactions."+idx+".student_response",data.response));
  if(CADRE.scorm.getVersion() == "1.3")
    e.push(api.SetValue("cmi.interactions."+idx+".learner_response",data.response));

  if('timestamp' in data && data.timestamp != "")  {
    if(CADRE.scorm.getVersion() == "1.2")
      e.push(api.SetValue("cmi.interactions."+idx+".time",data.timestamp.replace(/^.*T(\d\d):(\d\d):(\d\d).*/,"$1:$2:$3")));
    if(CADRE.scorm.getVersion() == "1.3")  {
      e.push(api.SetValue("cmi.interactions."+idx+".timestamp",data.timestamp));
    }
  }

  if('latency' in data && data.latency != "")  {
    var h = Math.floor(Math.floor(data.latency)/3600);
    var m = Math.floor((Math.floor(data.latency)-(h*3600))/60);
    var s = Math.floor((Math.floor(data.latency)-(h*3600)-(m*60)));

    if(CADRE.scorm.getVersion() == "1.2") {      
      latency = (h<10)  ? "0"+h+":" : ""+h+":";
      latency += (m<10) ? "0"+m+":" : ""+m+":";
      latency += (s<10) ? "0"+s : s;
      latency += String(data.latency).indexOf('.')>-1 ? String(data.latency).replace(/\d+(\.\d{1,2})/,"$1") : '';
      e.push(api.SetValue("cmi.interactions."+idx+".latency",latency));
    }
    if(CADRE.scorm.getVersion() == "1.3")  {
      latency = "PT"+h+"H"+m+"M"+s+(String(data.latency).indexOf('.')>-1 ? String(data.latency).replace(/^\d+(\.\d{1,2})/,"$1") : '')+"S";
      e.push(api.SetValue("cmi.interactions."+idx+".latency",latency));
    }
  }

  // Suspend the interaction data if we need to
  if(!canGet && CADRE.scorm.config.autosuspend)  {
    var sidx = -1;
    var sd = api.GetValue("cmi.suspend_data");
    if(sd.isJSON()) {sd = sd.evalJSON();}
    else {sd = {};}
    if(!('interactions' in sd)) sd.interactions = [];
    sd.interactions.each(function(rec,index) {if(rec.id == id) sidx = index;});
    sidx = (sidx<0) ? sd.interactions.length : sidx;
    sd.interactions[sidx] = $H({id:id, n:idx}).merge(data);
    e.push(api.SetValue("cmi.suspend_data",Object.toJSON(sd)));
  }

  return (e.indexOf('false')<0) ? true : false;
};
// ----------------------------------------------------------------------------


/* -------------------------- Get interaction data ----------------------------
   id:   The string ID of this interaction
*/
CADRE.scorm.getInteraction = function(id) {
  var api=CADRE.scorm.API;
  if(!api.OK) return null; // No API

  var c=0, i=[], e=[], gotThem=false, data= {type:"",response:"",result:""};
    
  // Try to fetch the data from interactions if possible
  if(CADRE.scorm.getVersion() == "1.3")  {
    gotThem=true;
    while(api.GetValue("cmi.interactions."+c+".id") != '')
      i.push(api.GetValue("cmi.interactions."+(c++)+".id"));

    var idx = i.indexOf(id);
    if(idx>=0) {
      data.type = api.GetValue("cmi.interactions."+idx+".type");
      gotThem = gotThem && (/^0$/.test(api.GetLastError())) ? true : false;
      data.response = api.GetValue("cmi.interactions."+idx+".learner_response");
      gotThem = gotThem && (/^0$/.test(api.GetLastError())) ? true : false;
      data.result = api.GetValue("cmi.interactions."+idx+".result");
      gotThem = gotThem && (/^0$/.test(api.GetLastError())) ? true : false;
      data.timestamp = api.GetValue("cmi.interactions."+idx+".timestamp");
      gotThem = gotThem && (/^0$|^403$/.test(api.GetLastError())) ? true : false;
      data.latency = api.GetValue("cmi.interactions."+idx+".latency");
      gotThem = gotThem && (/^0$|^403$/.test(api.GetLastError())) ? true : false;
    }
    if(gotThem) {
      // Transform the latency into seconds
      if('latency' in data && data.latency != "")  {
        var l = data.latency.match(/PT(\d+)H(\d+)M([\d\.]+)S/);
        data.latency = Number(l[1])*3600+Number(l[2])*60+Number(l[3]);
      }
      return data;
    }
    
    // Reinitialise data in case we populated it with incomplete data
    data= {type:"",response:"",result:""};
  }
  
  // Try to get the data from suspend_data
  var sd = api.GetValue("cmi.suspend_data");
  if(sd.isJSON()) {sd = sd.evalJSON();}
  else {sd = {};}
  if(!('interactions' in sd)) sd.interactions = [];
  sd.interactions.each(function(rec,index) {
    if(rec.id == id)
      $H(rec).each(function(r) {
        if(r.key!='n')
          data[r.key] = r.value;
      });
  });
  
  // SCORM 1.2 uses wrong instead of incorrect
  data.result = (CADRE.scorm.getVersion() == "1.2" && data.result == "wrong") ? "incorrect" : data.result;

  // Return the data if we found any, otherwise an empty object
  return data;
};
// ----------------------------------------------------------------------------


/* --- The finalize routine sets our exit type, commits changes and calls Finish --
   If called without a parameter, exit looks at the sco status and sets an
   exit value of suspend if the course is not set completed, passed or failed.
   
   If the sco is set completed, passed or failed a normal exit is performed.
   
   It is not recommended to use logout, since it will be deprecated in the next
   version of scorm.
   
   Type: ['logout','suspend','']
*/
CADRE.scorm.finalize = function(type,callback) {
  var api = CADRE.scorm.API;
  if(!api.OK) return null; // No API

  // If type isn't set and the SCO isn't complete set the exit to suspend
  // otherwise set a normal exit ('')
  type = !(/logout|suspend|^$/.test(type)) ? (/completed|passed|failed/.test(CADRE.scorm.getStatus())?'':'suspend') : type;

  if(Object.isString(callback))  {
    try {eval("callback = "+callback);}
    catch (e) {document.fire("scorm:error",{method: "initialize", param: "{callback:"+callback+"}", code: "201",string: "Invalid Argument",diagnostic: "Error in Callback"});}
  }

  function finalize() {
    var e=[];

    if(CADRE.scorm.getVersion() == "1.2")
      e.push(api.SetValue("cmi.core.exit", (type==="") ? "" : type));
    if(CADRE.scorm.getVersion() == "1.3")
      e.push(api.SetValue("cmi.exit",(type==="") ? "normal" : type));

    e.push(api.Terminate(''));

    var finalize = (e.indexOf('false')<0) ? true : false;

    if(Object.isFunction(callback)) callback(finalize);
    return finalize;
  }

  if(Object.isFunction(callback)) {finalize.defer();return true;}
  return finalize();
};
// ----------------------------------------------------------------------------

/* ------------------------- Commit outstanding saves -------------------------
   Not normally necessary unless you've expressly told functions to set data
   without committing
*/
CADRE.scorm.commit = function() {
  return CADRE.scorm.API.Commit('');
};
// ----------------------------------------------------------------------------
