/*  ----------------------------------------------------------------------------
 *  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
 *      # Fix handling of autosuspend==false
*/

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


// ----------------------------------------------------------------------------
// ----------------------------- Utility Functions ----------------------------
// ----------------------------------------------------------------------------
CADRE.tools = ('tools' in CADRE) ? CADRE.tools : {};
CADRE.tools.getType = function(obj) {
    return (obj === null) ? "null" : typeof(obj) == "undefined" ? "undefined" :Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
};

CADRE.tools.isType = function(obj,type) {
  if(type===CADRE.tools.getType(obj))
    return true;
  return false;
};

CADRE.tools.isEmpty = function(obj) {
  if(CADRE.tools.isType(obj,"undefined")) return true;
  if(CADRE.tools.isType(obj,"string") && obj=="") return true;
  if(CADRE.tools.isType(obj,"array") && obj.length==0) return true;    
  if(CADRE.tools.isType(obj,"object")) {
    for (var name in obj)
      if(!(obj.hasOwnProperty(name))) continue;
      else return false;
    return true;
  }
  return false;
};
// ----------------------------------------------------------------------------


// ----------------------------------------------------------------------------
// ----------------------------- CADRE SCORM API ------------------------------
// ----------------------------------------------------------------------------

CADRE.scorm = function() {
  
  // ----------------------------------------------------------------------------
  // ------------------------------ Low level API -----------------------------
  // ----------------------------------------------------------------------------  
  var 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 --------------------------
  // ----------------------------------------------------------------------------
  
  /* Converts dotted property names into objects.
   * For example cmi.interactions.0.objectives.0.id becomes
   * {cmi: 
   *   {interactions:
   *     [
   *       {objectives:
   *         [
   *          {id:v}
   *         ]
   *       }
   *     ]
   *   }
   * }
  */
  function objectify(obj,p,v) {
    if(CADRE.tools.isEmpty(p)) return v;
    var bits = p.split('.');
    while(bits.length && CADRE.tools.isEmpty(bits[0]))
      bits = bits.slice(1);
    if(!bits.length) return v;

    if(!CADRE.tools.isType(obj,"object")) obj = {};

    bits.inject(obj,function(e,bit,index) {
      if(bits.length==index+1) {
        var _ = v;
        e[bit] = _;
      } else if(/^\d+$/.test(bit)) {
        if(typeof(e[Number(bit)]) == "undefined") {
          var _ = {};
          e[Number(bit)] = _;
        } else var _ = e[Number(bit)];
      } else if(!(bit in e)) {
        if(/^\d+$/.test(bits[index+1])) var _ = [];
        else var _ = {};
        e[bit] = _;
      } else _ = e[bit];
      return _;
    });
    return obj;
  };

  /* Converts objects into dotted property names.
   * For example
   * {cmi: 
   *   {interactions:
   *     [
   *       {objectives:
   *         [
   *          {id:v}
   *         ]
   *       }
   *     ]
   *   }
   * }
   * becomes  {"cmi.interactions.0.objectives.0.id":v}
   *
  */
  function dotify(obj,str) {
    if(!CADRE.tools.isType(obj,"object") && !CADRE.tools.isType(obj,"array"))
      return obj;

    str =  CADRE.tools.isType(str,"string") ? str : "";

    var dots={};
    function dotify(obj,str) {
      if(CADRE.tools.isType(obj,"string") || CADRE.tools.isType(obj,"number"))
        dots[str]=obj;
      if(CADRE.tools.isType(obj,"object")) {
        for(o in obj)
          if(obj.hasOwnProperty(o))
            dotify(obj[o],CADRE.tools.isEmpty(str) ? o : str+'.'+o);
      }
      if(CADRE.tools.isType(obj,"array")) {
        for(var i=0;i<obj.length;i++)
          dotify(obj[i],CADRE.tools.isEmpty(str) ? i : str+'.'+i);
      }
    }
    dotify(obj,str);
    return dots;
  };
  
  /* List of supported scorm elements */
  scormproperties = {
    "1.2": ["cmi.interactions.n.id","cmi.interactions.n.type","cmi.interactions.n.result",
    "cmi.interactions.n.student_response","cmi.interactions.n.latency",
    "cmi.interactions.n.time","cmi.interactions.n.weighting",
    "cmi.interactions.n.correct_responses.m.pattern","cmi.interactions.n.objectives.m.id",
    "cmi.objectives.n.id","cmi.objectives.n.status","cmi.objectives.n.score.max",
    "cmi.objectives.n.score.min","cmi.objectives.n.score.raw","cmi.core.credit",
    "cmi.core.entry","cmi.core.exit","cmi.core.lesson_location","cmi.core.lesson_mode",
    "cmi.core.lesson_status","cmi.core.score.max","cmi.core.score.min","cmi.core.score.raw",
    "cmi.core.session_time","cmi.core.student_id","cmi.core.student_name",
    "cmi.core.total_time","cmi.comments","cmi.comments_from_lms","cmi.launch_data",
    "cmi.student_data.mastery_score","cmi.student_data.max_time_allowed",
    "cmi.student_data.time_limit_action","cmi.student_preference.audio",
    "cmi.student_preference.language","cmi.student_preference.speed",
    "cmi.student_preference.text","cmi.suspend_data"],
    "1.3": ["cmi.interactions.n.id","cmi.interactions.n.type",
    "cmi.interactions.n.objectives.m.id","cmi.interactions.n.timestamp",
    "cmi.interactions.n.correct_responses.m.pattern","cmi.interactions.n.weighting",
    "cmi.interactions.n.learner_response","cmi.interactions.n.result",
    "cmi.interactions.n.latency","cmi.interactions.n.description","cmi.objectives.n.id",
    "cmi.objectives.n.success_status","cmi.objectives.n.completion_status",
    "cmi.objectives.n.score.scaled","cmi.objectives.n.score.raw",
    "cmi.objectives.n.score.min","cmi.objectives.n.score.max",
    "cmi.objectives.n.progress_measure","cmi.objectives.n.description",
    "cmi.completion_status","cmi.completion_threshold","cmi.credit","cmi.entry","cmi.exit",
    "cmi.launch_data","cmi.learner_id","cmi.learner_name","cmi.location",
    "cmi.max_time_allowed","cmi.mode","cmi.progress_measure","cmi.scaled_passing_score",
    "cmi.score.scaled","cmi.score.raw","cmi.score.min","cmi.score.max","cmi.session_time",
    "cmi.success_status","cmi.suspend_data",
    "cmi.time_limit_action","cmi.total_time","cmi.comments_from_learner.n.comment",
    "cmi.comments_from_learner.n.location","cmi.comments_from_learner.n.timestamp",
    "cmi.comments_from_lms.n.comment","cmi.comments_from_lms.n.location",
    "cmi.comments_from_lms.n.timestamp","cmi.learner_preference.audio_level",
    "cmi.learner_preference.language","cmi.learner_preference.delivery_speed",
    "cmi.learner_preference.audio_captioning"]
  };

  /* Utility functions for working with suspended data */
  var suspenddata = function() {
    var cache=null;

    return {
      set: function data_set(context,id,data) {
        if(!API.OK) return null; // No API
        if(cache==null) this.init();

        if(CADRE.tools.isEmpty(context) || CADRE.tools.isEmpty(id)) return false;
        if(!(context in cache)) cache[context]={};

        if(CADRE.tools.isEmpty(data)) delete cache[context][id];
        else if(CADRE.tools.isType(data,"object"))
          cache[context][id] = Object.toJSON(data);
        else
          cache[context][id] = data;

        return true;
      },
      get: function data_get(context,id) {
        if(!API.OK) return null; // No API
        if(cache==null) this.init();

        if(CADRE.tools.isEmpty(context)) return false;
        if(CADRE.tools.isEmpty(id)) {
          var sd = {};
          for(id in cache[context])
            if(cache[context][id].isJSON()) sd[id] = cache[context][id].evalJSON();
            else sd[id] = cache[context][id];
          return sd;
        }
        return (context in cache && id in cache[context]) ? CADRE.tools.isType(cache[context][id],'string') && cache[context][id].isJSON() ? cache[context][id].evalJSON() : cache[context][id] : null;
      },
      find: function data_find(context,pattern) {
        var props={},_ = $H(cache[context]).grep(new RegExp(pattern));
        for(var i=0;i<_.length;i++) props[_[i][0]] = _[i][1];
        return props;
      },
      save: function data_save() {
        if(!API.OK) return null; // No API
        if(cache==null) this.init();


        // compress the cmi suspend_data
        if(scormconfig.autosuspend) {
          if("cmi" in cache) {
            var _,__,csd={},v = getVersion();
            $H(cache.cmi).inject(csd,function(e,p,index) {
              var c=0;
              var ptrn = p.key.replace(/\.([0-9]+)/g,function(str, p1, offset, s) {
                return "."+String.fromCharCode("n".charCodeAt()-c++);
              });

              if((_=scormproperties[v].indexOf(ptrn))>=0) {
                if(__ = p.key.match(/\d+(?=\.)/g)) csd[_+"."+__.join(".")]=p.value;
                else csd[_]=p.value;
              } else csd[p.key] = p.value;

              return csd;
            });
            cache.cmi = csd;
          }
        } else delete cache["cmi"];

        var sd = Object.toJSON(cache);
        if(sd == "{}") return true;
        if(getVersion() == "1.2")
          if(sd.length<4096) {
            API.SetValue("cmi.suspend_data",sd);
            return true;
          } else
            document.fire("scorm:error",{method: "data.save", param: Object.toJSON(sd), code: "405",string: "Incorrect Data Type",diagnostic: "Insufficient space to store data ("+sd.length+")"});
        if(getVersion() == "1.3")
          if(sd.length<64000) {
            API.SetValue("cmi.suspend_data",sd);
            return true;
          } else
            document.fire("scorm:error",{method: "data.save", param: Object.toJSON(sd), code: "406",string: "Data Model Element Type Mismatch",diagnostic: "Insufficient space to store data ("+sd.length+")"});      
        return false;
      },
      init: function data_init() {
        var sd = API.GetValue("cmi.suspend_data");
        if(sd.isJSON()) {cache = sd.evalJSON();}
        else {cache = {};}

        if("cmi" in cache)  {
          var sd={},v = getVersion();
          $H(cache.cmi).inject(sd,function(e,p,index) {
            var idx = p.key.match(/\d+/g);
            var ptrn = scormproperties[v][Number(idx.shift())];
            var property = ptrn.replace(/\.[a-z]\./g,function(str, p1, offset, s) {
              return "."+idx.shift()+".";
            });

            sd[property] = p.value;
            return sd;
          });
          cache.cmi = sd;
        }
      }
    };
  }();

  /* Utility functions for working with scorm values */
  var scormvalue = function() {
    var cache={},index={};

    function get(p) {
      if(p in cache) return cache[p];
      var _ = suspenddata.get("cmi",p);
      if( _ == null || _ == false ) {
        // A Moodle bug means that bogus objectives
        // don't generate an error.
        // e.g. cmi.objectives.andrew.id -> GetLastError()==="0"
        // e.g. cmi.objectives.123456789.id -> GetLastError()==="0"
        if(/\.[0-9]+\./.test(p)) {
          var pc = p.replace(/(.*)\.[0-9]+\..*/,"$1._count");
          var c = Number(API.GetValue(pc));
          if(API.GetLastError()==="0") {
            if(c>Number(p.replace(/.*\.([0-9])+\..*/,"$1"))) {
              _ = API.GetValue(p);
            } else _=null;
          } else {
            _ = API.GetValue(p);
          }
        } else _ = API.GetValue(p);
        if(API.GetLastError()!=="0") _ = null;
      }
      return cache[p]=_;
    }

    return {
      get: function value_get(p) {
        if(!API.OK || !p) return null;
        return (_=get(p)) ? _ : '';
      },
      set: function value_set(p,v) {
        if(!API.OK || !p) return null;
        var __;
        var _=((__=API.SetValue(p,v))=='true');
        if(!_ || String(API.GetValue(p)) !== String(v))
          _=suspenddata.set("cmi",p,v);
        if(_) {cache[p]=v;return true;}
        return false;
      },
      suspend: function value_suspend(p,v) {
        if(!API.OK || !p) return null;
        _=suspenddata.set("cmi",p,v);
        if(_) {cache[p]=v;return true;}
        return false;
      },
      find: function value_find(pattern) {
        var ctx={},c=0,data = {},version=getVersion();
        ptrn = pattern.replace(/\.([0-9]+)/g,function(str, p1, offset, s) {
          ctx[String.fromCharCode("n".charCodeAt()-c)] = p1;
          return "."+String.fromCharCode("n".charCodeAt()-c++);
        });

        // If pattern exactly matches a valid property return it
        if(scormproperties[version].indexOf(ptrn)>=0) {
          if(!(/\.[a-z]\./.test(pattern)) && (_ = get(pattern)) != null) {
            data[pattern]=_;return data;
          } else props = [pattern];
        } else
          var props = scormproperties[version].grep(new RegExp(ptrn));

        // otherwise fetch all the matching properties
        for(var i=0;i<props.length;i++) {
          var idx={},idxc=0,tplItems={},lastItem=null,value=null;
          props[i].scan(/\.([a-z])\./,function(match){idx[match[1]]=0;idxc++;lastItem=match[1];});

          while(true) {
            for(var j in idx)
              if(idx.hasOwnProperty(j)) 
                if(j in ctx) tplItems[j]="."+ctx[j]+".";
                else tplItems[j]="."+idx[j]+".";

            var elementStr = props[i].interpolate(tplItems,/(^|.|\r|\n)(\.([a-z])\.)/);

            if((value = get(elementStr)) != null) {
              data[elementStr] = value;
              if((lastItem in ctx) || lastItem==null) break;
              idx[lastItem]++;
            } else {
              var j = lastItem;
              while(j in idx) {
                var s = elementStr.replace(/^(.+)\d+\.\w+$/,"$1"+(idx[j]+1));
                if($H(data).find(function(v){return v.key.match(s);})) {idx[j]++;break;}
                if(idx[j]==0) {j= String.fromCharCode(j.charCodeAt(0)+1);continue;}
                idx[j]=0;j= String.fromCharCode(j.charCodeAt(0)+1);
                if(j in ctx) {j=String.fromCharCode(j.charCodeAt(0)+1);continue;}
                if(j in idx) {idx[j]++;break;}
              }
              if(!(j in idx)) break;
            }
          }
        }
        var sd = suspenddata.find("cmi",pattern);
        for(var p in sd) if(sd.hasOwnProperty(p)) data[p] = sd[p];
        return data;
      },
      getIndex: function value_getIndex(pattern,id) {
        var idx,newidx=0;
        var ids = this.find(pattern);

        for(var i in ids)
          if(ids.hasOwnProperty(i)) {
            if(ids[i] == id && i.match(/.*\.(\d+)\..*/)) {
              idx = Number(i.replace(/.*\.(\d+)\..*/,"$1")); break;
            }
            newidx++;
          }
        if(!CADRE.tools.isType(idx,"number")) idx=newidx;

        // Double check zero indexes.
        // This safety net is intended to catch SCORM 1.2 LMS
        // when we don't have autosuspend enabled
        if(idx==0)  {
          var pc = pattern.replace(/(.*)\.[a-z]\..*/,"$1._count");
          var c = Number(API.GetValue(pc));
          if(API.GetLastError()==="0" && c>idx) idx=c;
        }
        return idx;
      }
    };
  }();

  // ----------------------------------------------------------------------------


  // ------------------------- Configuration Parameters -------------------------
  scormconfig = {autosuspend: false};
  // ----------------------------------------------------------------------------


  /* ---------------------------- Initialise the API ----------------------------
   * config: A configuration object.  The following parameters are currently
   * supported -
   * {
   *   // Enabling autosuspend will allow you to read back values between sessions
   *   // that aren't normally read/write.  For example SCORM 1.2 doesn't allow
   *   // you to read back interaction data.  Turning on autosuspend caches in
   *   // suspend_data any values that the LMS can't store and return natively
   *
   *   // Default is false
   *   autosuspend: true
   * }
   * 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.
  */

  function initialize(config,callback) {
    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() {
      if(CADRE.tools.isType(config,'object'))
        if('autosuspend' in config)
          scormconfig.autosuspend = config.autosuspend;

      var initialize = 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=='true') ? true : false;
    }

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


  // ------------------- Simple Getters for Read Only properties ----------------
  var getVersion = function getVersion() {
    if(!API.OK) return null; // No API
    var v=null;
    return function() {
      return v!==null ? v :
        API.GetValue('cmi._version') == "1.0" ? v="1.3" : (API.GetValue('cmi._version') == "3.4" ? v="1.2" : v="");
    };
  }();
  function getLearnerId() {
    if(!API.OK) return null; // No API
    switch(getVersion()) {
      case "1.2": return scormvalue.get('cmi.core.student_id');break;
      case "1.3": return scormvalue.get('cmi.learner_id');break;
      default: return '';
    }
  };
  function getLearnerName() {
    if(!API.OK) return null; // No API
    switch(getVersion()) {
      case "1.2": return scormvalue.get('cmi.core.student_name');break;
      case "1.3": return scormvalue.get('cmi.learner_name');break;
      default: return '';
    }
  };
  function getCredit() {
    if(!API.OK) return null; // No API
    switch(getVersion()) {
      case "1.2": return scormvalue.get('cmi.core.credit');break;
      case "1.3": return scormvalue.get('cmi.credit');break;
      default: return '';
    }
  };
  function getEntry() {
    if(!API.OK) return null; // No API
    switch(getVersion()) {
      case "1.2": return scormvalue.get('cmi.core.entry');break;
      case "1.3": return scormvalue.get('cmi.entry');break;
      default: return '';
    }
  };
  function getMode() {
    if(!API.OK) return null; // No API
    switch(getVersion()) {
      case "1.2": return scormvalue.get('cmi.core.lesson_mode');break;
      case "1.3": return scormvalue.get('cmi.mode');break;
      default: return '';
    }
  };
  function getLaunchData() {if(!API.OK) return null; return (/[^\s]/.test(API.GetValue('cmi.launch_data'))) ? (API.GetValue('cmi.launch_data').isJSON() ? API.GetValue('cmi.launch_data').evalJSON(true) : (""+API.GetValue('cmi.launch_data').gsub(/\\\"/,"\"").isJSON() ? API.GetValue('cmi.launch_data').gsub(/\\\"/,"\"").evalJSON(true) : API.GetValue('cmi.launch_data'))) : "";};
  // ----------------------------------------------------------------------------

  /* ------------------------- Set the Lesson Location --------------------------
     location: string(255) // String representing the students location in the sco
  */
  function setLocation() {
    if(!API.OK) return null; // No API
    switch(getVersion()) {
      case "1.2": return scormvalue.set("cmi.location",location);
      case "1.3": return scormvalue.set("cmi.core.lesson_location",location);
      default: return false;
    }
  };
  // ----------------------------------------------------------------------------
  /* ------------------------- Get the Lesson Location ----------------------- */
  function getLocation() {
    if(!API.OK) return null; // No API
    switch(getVersion()) {
      case "1.2": return scormvalue.get("cmi.core.lesson_location");
      case "1.3": return scormvalue.get("cmi.location");
      default: return '';
    }
  };
  // ----------------------------------------------------------------------------

  /* ---------------------------- Set the Lesson Status -------------------------
   * status: string["passed","failed","completed","incomplete","not attempted"]
  */
  function setStatus(status) {
    if(!API.OK) return null; // No API

    var e = true;
    switch(getVersion()) {
      case "1.2": return scormvalue.set("cmi.core.lesson_status",status);
      case "1.3": 
        if(/passed|failed/.test(status))  {
          e &= scormvalue.set("cmi.success_status",status);
          e &= scormvalue.set("cmi.completion_status","completed");
          return e;
        }
        if(/completed|incomplete|not attempted/.test(status))
          return scormvalue.set("cmi.completion_status",status);
    }
    return false;
  };
  // ----------------------------------------------------------------------------

  /* ------------------------ Get the Lesson Status -------------------------- */
  function getStatus() {
    if(!API.OK) return null; // No API

    switch(getVersion()) {
      case "1.2": return scormvalue.get("cmi.core.lesson_status");
      case "1.3": 
        var success = scormvalue.get("cmi.success_status");
        if(success && success != 'unknown') return success;
        var completion = scormvalue.get("cmi.completion_status");
        if(completion && completion != 'unknown') return completion;
        if(success == 'unknown' && completion == 'unknown') return "not attempted";
    }
    return '';
  };
  // ----------------------------------------------------------------------------

  /* ------------------------ Set the Overall Score --------------------------
   * data: An object containing the following:
   * {
   *   score_raw:     real(10,7) range (0..100),
   *   score_max:     real(10,7) range (0..100),
   *   score_min:     real(10,7) range (0..100),
   *   score_scaled:  real(10,7) range(-1..1) 
   * }
  */
  function setScore(data) {
    if(!API.OK) return null; // No API

    var e=true;

    switch(getVersion()) {
      case "1.2": context = "cmi.core.score";break;
      case "1.3": context = "cmi.score";break;
      default: return false;
    }

    for(var i in data)
      if(data.hasOwnProperty(i))
        e&=scormvalue.set(context+"."+i.replace(/\w+?_/,""),String(data[i]).replace(/(\d+\.\d{7}).+/,"$1"));

    return e;
  };
  // ----------------------------------------------------------------------------

  /* ------------------------ Get the Overall Score -------------------------- */
  function getScore() {
    if(!API.OK) return null; // No API

    switch(getVersion()) {
      case "1.2": context = "cmi.core.score";break;
      case "1.3": context = "cmi.score";break;
      default: return {};
    }

    // Assemble our score object
    var score = {};
    props = scormvalue.find(context);
    for(var p in props)
      if(props.hasOwnProperty(p)) {
        if(CADRE.tools.isType(props[p],"string") && props[p].match(/^[0-9.]+$/)) props[p] = String(props[p]);
        score["score"+p.substr(context.length).replace(/\./,"_")] = String(props[p]);
      }
    return score ? score : 0;
  };
  // ----------------------------------------------------------------------------

  /* ------------------------ 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)
   *
   * It's your responsibility to fetch the time before setting it.  You can't set
   * the time to a value less than what has previously been stored in the LMS. You
   * can however call this routine multiple times in a single session.  Only the
   * time specified in the last call will be used.  See the SCORM 1.2 documentation
   * to get a better idea of how this works
   *
   * time: REAL(8.2)         // Total time on activity in seconds;
  */
  function setTime(time) {
    if(!API.OK) return null; // No API

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

    switch(getVersion()) {
      case "1.2": var session_time = "cmi.core.session_time"; break;
      case "1.3": var session_time = "cmi.session_time"; break;
      default: return false;
    }

    // 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) {
      switch(getVersion()) {
        // We are not using scormvalue.set because this is a write only value
        case "1.2": return API.SetValue(session_time,"00:00:00.00")=='true';
        case "1.3": return API.SetValue(session_time,"PT00H00M00.00S")=='true';
        default: return false;
      }
    }

    var t = time-totalTime;
    var h = String(Math.floor(Math.floor(t)/3600)).replace(/^(\d)(?=$)/,"0$1");
    var m = String(Math.floor((Math.floor(t)-(h*3600))/60)).replace(/^(\d)(?=$)/,"0$1");
    var s = String(Math.floor(t)-(h*3600)-(m*60)).replace(/(\d+\.\d\d).+/,"$1").replace(/^(\d)(?=\.)/,"0$1");

    if(getVersion() == "1.2") sessionTime = h+":"+m+":"+s;
    if(getVersion() == "1.3") sessionTime = "PT"+h+"H"+m+"M"+s+"S";

    // We are not using scormvalue.set because this is a write only value
    return API.SetValue(session_time,sessionTime)=='true';
  };
  // ----------------------------------------------------------------------------

  /* ------------------------------ Get the Time -------------------------------- */
  function getTime() {
    if(!API.OK) return null; // No API

    switch(getVersion()) {
      case "1.2":
        var totalTime = scormvalue.get("cmi.core.total_time");
        var _ = totalTime.replace(/(\d*\.{0,1}\d+)/g);
        var __ = 0;
        __+= Number(_.replace(/^(\d+):.*/,"$1"))*Math.pow(60,2);
        __+= Number(_.replace(/.*?:(\d+):.*/,"$1"))*60;
        __+= Number(_.replace(/(.*:(\d+\.\d+)$)|(.*?(\d+)$)/,"$2$4"));
        totalTime = __;
        break;
      case "1.3":
        var totalTime = scormvalue.get("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 = __;
        break;
    }

    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
    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
  */
  function setData(id,data) {
    if(!API.OK) return null; // No API
    return suspenddata.set("user",id,data);
  };
  // ----------------------------------------------------------------------------


  /* ------------------------ 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
  */
  function getData(id) {
    if(!API.OK) return null; // No API
    var _ = suspenddata.get("user",id);
    return (CADRE.tools.isType(_,'string') && _.isJSON()) ? _.evalJSON() : _;
  };
  // ----------------------------------------------------------------------------


  /*  -------------------------- 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.

      Please note that score_scaled, progress_measure and description are only
      supported in scorm 1.3.  In SCORM 1.2 if auto_suspend is enabled these items
      will be saved in suspend_data, however be careful as large numbers of
      objectives will soon fill up your suspend_data.
      
      SCORM 1.3 recommends that ids be URNs. All URNs are required to have the
      following syntax (phrases in quotes are required): 
      
                        <URN> ::= "urn:"<NID>":"<NSS> 
      
      Where <NID> is the Namespace Identifier and <NSS> is the Namespace
      Specific String 

      Example:  urn:SimSkill:Stakeholders-Client_Satisfaction
      
      While it's not required, it would be good if you used this nomenclature.
      When running in SCORM 1.2 I automatically strip non-alphanumeric
      characters from your ids.
      
      Finally id can optionally be an integer.  Calls to setObjective will
      return boolean false on failure or the objective index on success.
      Subsequent calls to setObjective can either send the string id or
      the index.  Either way your objective will be updated.  This feature is
      more important for interactions when you are running in SCORM 1.2
      without autosuspend, but has been built into setObjective and
      getObjective for consistency.

      id:   The string ID of this objective or the objective index
      data: OPTIONAL { 
        score_raw: real (10,7) range (0..100),
        score_min: real (10,7) range (0..100),
        score_max: real (10,7) range (0..100),
        score_scaled: real (10,7) range (-1..1),
        status: ["passed","failed","completed","incomplete", "not attempted"],
        progress_measure: real (10,7) range (0..1),
        description: string (250)
      }
  */
  function setObjective(id,data) {
    if(!API.OK) return null; // No API

    // SCORM 1.2 limits ids to alphanumeric
    id = (getVersion() == "1.2") ? String(id).replace(/[^0-9a-zA-Z]/g,"").replace(/^urn/,"") : id;

    var e=true;  

    // If the Id is numeric, assume it's an index
    if(/^[0-9]+$/.test(id)) var idx = Number(id);
    else {
      var idx = scormvalue.getIndex("cmi.objectives.n.id",id);
      if(!scormvalue.get("cmi.objectives."+idx+".id"))
        e&=scormvalue.set("cmi.objectives."+idx+".id",id);
    }

    for(var i in data) {
      if(!(data.hasOwnProperty(i))) continue;
      if(/^score_/.test(i)) {
        var _ = i.split('_');
        e&=scormvalue.set("cmi.objectives."+idx+"."+_[0]+"."+_[1],String(data[i]).replace(/(\d+\.\d{7}).+/,"$1"));
      } else if(i=="status" && getVersion() == "1.3") {
        if(/passed|failed/.test(data[i]))  {
          /* 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&=scormvalue.set("cmi.objectives."+idx+".success_status",data[i]);
        }
        if(/completed|incomplete|not attempted/.test(data[i]))  {
          /* 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&=scormvalue.set("cmi.objectives."+idx+".completion_status",data[i]);
        }
      } else {
        e&=scormvalue.set("cmi.objectives."+idx+"."+i,data[i]);
      }
    }

    if(e) return idx;
    return e;
  };
  // ----------------------------------------------------------------------------


  /* ------------------------------ Get Objective -------------------------------
     id:   The string ID or index of this objective
  */
  function getObjective(id) {
    if(!API.OK) return null; // No API

    // SCORM 1.2 limits ids to alphanumeric
    id = (getVersion() == "1.2") ? String(id).replace(/[^0-9a-zA-Z]/g,"").replace(/^urn/,"") : id;

    // If the Id is numeric, assume it's an index
    if(/^[0-9]+$/.test(id)) var idx = Number(id);
    // Otherwise fetch the index for the specified id
    else var idx = scormvalue.getIndex("cmi.objectives.n.id",id);

    var data={};
    if(!scormvalue.get("cmi.objectives."+idx+".id")) return {};
    if(CADRE.tools.isType(idx,'number'))  {
      d=scormvalue.find("cmi.objectives."+idx);
      for(var p in d) {
        if(d.hasOwnProperty(p))
          objectify(data,p.replace(/cmi\.objectives\.\d+\./,""),d[p]);
      }
    }
    if('score' in data) {
      for(var s in score)
        if(score.hasOwnProperty(s))
          data["score_"+s] = score[s];
      delete data['score'];
    }

    if("success_status" in data) {
      if(data.success_status != 'unknown')
        data.status = data.success_status;
      delete data['success_status'];
    }
    if("completion_status" in data) {
      if(data.completion_status != 'unknown')
        data.status = data.status ? data.status+='|'+data.completion_status : data.completion_status;
      delete data['completion_status'];
    }


    return data;
  };
  // ----------------------------------------------------------------------------


  /* --------------------- 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 should be 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'})

    Note:
    * SCORM 1.2 doesn't allow us to fetch existing interactions, but *some* 1.3
    * LMS do. For portability (if you enable autosuspend), each data item which
    * is unsupported either for reading or writing will be suspended using
    * suspend_data.  Be careful, if you track a lot of long data items which
    * can't be written or read back in your target LMS/SCORM version, your
    * suspend_data will blow out. You can stop the suspending uf unsupported
    * attributes by initializing with a config of {autosuspend:false}
    * 
    * While you should be careful about doing this, it also means you can track
    * additional, non-SCORM items with your interactions.  They won't appear
    * in your LMS reports, but they will be suspended and returned to you
    * when you fetch your interaction data back.  For example, I could track
    * an interaction such as:
    * {
    *   type:'fill-in',
    *   response:'I am',
    *   result:'correct',
    *   widgetStatus: 'deactivated'
    * }
     
    The id can either be the string id for this interaction or the interaction
    index number.  If you are using SCORM 1.2, interactions are read only.  If
    you have auto_suspend enabled, then you can effectively read and write them
    however if you have a lot of interactions that your tracking, you'll soon
    fill up your suspend data.  On a project by project basis you could then
    decide, ok we need to track lots of items, but we don't need to be able
    to read previously tracked items back in later sessions.  We do however
    need to make sure that if a user re-visits a previous interaction that
    they can redo it and have the data resubmitted.  So that you don't end up
    with multiple tracks for the same interaction, you could write your own
    code to keep track of the interaction indexes for each item you track.
    
    For example, let's say I'm in SCORM 1.2 with autosuspend disabled and I
    want to track the results from a quiz.  The quiz can be sat multiple times
    a session and the sco can be revisited/suspended multiple times.  I can
    keep track of the interaction like this:
    
    // First time
    var idx = setInteraction('Quiz One',{result:80});
    setData('Q1',idx);
    
    // Subsequent time
    var idx = getData('Q1');
    setInteraction(idx,{result:100})
    
    This way, I can track hundreds of interactions without running out of
    suspend data.  I just don't have access to previously set data in later
    sessions.  In many cases though, this is not important.  It is important
    however that I don't end up with hundreds of interaction records in the
    LMS for the same quiz.  Using the above code, you update the existing
    record.
    
    If you have only a few dozen interactions your tracking, or your using
    scorm 1.3, you don't need to worry, just use the string identifier
    

    id:   The string ID or index 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
     objectives:        OPTIONAL ARRAY(data) // Array of data objects of the form {id:"myObjectiveId"}
     correct_responses: OPTIONAL ARRAY(data) // Array of data objects of the form {pattern:"formatDeterminedByType"}
     weighting:         OPTIONAL REAL(10.2)
    }
  */
  function setInteraction(id,data) {
    if(!API.OK) return null; // No API

    // SCORM 1.2 limits ids to alphanumeric
    id = (getVersion() == "1.2") ? String(id).replace(/[^0-9a-zA-Z]/g,"").replace(/^urn/,"") : id;

    // Some items need to be suspended independently of being stored. For
    // example, timestamps are truncated in scorm 1.2, but for cross platform
    // consistency we need to suspend the full timestamp so the app can fetch
    // it in the future
    var sdata={};

    // Convert latency to scorm format
    if('latency' in data && data['latency'] != '')  {
      var h = String(Math.floor(Math.floor(data['latency'])/3600)).replace(/^(\d)(?=$)/,"0$1");
      var m = String(Math.floor((Math.floor(data['latency'])-(h*3600))/60)).replace(/^(\d)(?=$)/,"0$1");
      var s = String(data['latency']-(h*3600)-(m*60)).replace(/(\d+\.\d\d).+/,"$1").replace(/^(\d)(?!\d)/,"0$1").replace(/(\d+\.\d)$/,"$10");

      if(getVersion() == "1.2")
        data['latency'] = h+":"+m+":"+s;
      if(getVersion() == "1.3")
        data['latency'] = "PT"+h+"H"+m+"M"+s+"S";
    }
    
    // Remap SCORM 1.2 properties
    if(getVersion() == "1.2") {
      if("timestamp" in data) {
        data.time = data.timestamp.replace(/^.*T(\d\d):(\d\d):(\d\d).*/,"$1:$2:$3");
        sdata.time = data.timestamp;
        delete data['timestamp'];
      }
      if("result" in data && Number(data.result))
        data.result = String(data.result).replace(/(\d+\.\d{7}).+/,"$1");
      if("result" in data && data.result=="incorrect")
        data.result="wrong";
      if("response" in data) {
        data.student_response=data.response;
        delete data["response"];
      }
    } else if(getVersion() == "1.2") {
      if("response" in data) {
        data.learner_response=data.response;
        delete data["response"];
      }
    }

    var e=true;  
    // If the Id is numeric, assume it's an index
    if(/^[0-9]+$/.test(id))
      var idx = Number(id);
    // Otherwise fetch the index for the specified id
    else {
      var idx = scormvalue.getIndex("cmi.interactions.n.id",id);
      if(!scormvalue.get("cmi.interactions."+idx+".id"))
        e&=scormvalue.set("cmi.interactions."+idx+".id",id);
    }

    // Set scorm data
    var properties = dotify(data);
    for(var p in properties) {
      if(properties.hasOwnProperty(p)) {
        if(p.match(/.*\.\d+\..*idx$/)) continue;
        if(p.match(/.*\.\d+\..*id$/) || p.match(/.*\.\d+\..*pattern$/)) {
          if(p.match(/.*\.\d+\..*id$/) && getVersion() == "1.2")
            properties[p] = properties[p].replace(/[^0-9a-zA-Z]/g,"").replace(/^urn/,"");
          
          if(p.replace(/(.*\.\d+\..*?)\w+$/,"$1idx") in properties) var _ = properties[p.replace(/(.*\.\d+\..*?)\w+$/,"$1idx")];
          else var _ = scormvalue.getIndex("cmi.interactions."+idx+"."+p.replace(/(.*?)\.(\d+)\.(.*?)/,"$1.m.$3"),properties[p]);
          p = p.replace(/(.*?)\.(\d+)\.(.*?)/,"$1."+_+".$3");
        }
        e&=scormvalue.set("cmi.interactions."+idx+"."+p,properties[p]);
      }
    }

    // Set suspend only data
    var properties = dotify(sdata);
    for(var p in properties) {
      if(properties.hasOwnProperty(p)) {
        if(p.match(/.*\.\d+\..*id$/) || p.match(/.*\.\d+\..*pattern$/)) {
          var _ = scormvalue.getIndex("cmi.interactions."+idx+"."+p.replace(/(.*?)\.(\d+)\.(.*?)/,"$1.m.$3"),properties[p]);
          p = p.replace(/(.*?)\.(\d+)\.(.*?)/,"$1."+_+".$3");
        }
        e&=scormvalue.suspend("cmi.interactions."+idx+"."+p,properties[p]);
      }
    }

    if(e) return idx;
    return e;
  };
  // ----------------------------------------------------------------------------


  /* -------------------------- Get interaction data ----------------------------
     id:  The string ID or index of the requested interaction.
  */
  function getInteraction(id) {
    if(!API.OK) return null; // No API

    // SCORM 1.2 limits ids to alphanumeric
    id = (getVersion() == "1.2") ? String(id).replace(/[^0-9a-zA-Z]/g,"").replace(/^urn/,"") : id;

    // If the Id is numeric, assume it's an index
    if(/^[0-9]+$/.test(id)) var idx = Number(id);
    // Otherwise fetch the index for the specified id
    else var idx = scormvalue.getIndex("cmi.interactions.n.id",id);

    var data={};
    if(!scormvalue.get("cmi.interactions."+idx+".id")) return data;

    d=scormvalue.find("cmi.interactions."+idx);
    for(var p in d) {
      if(d.hasOwnProperty(p))
        data = objectify(data,p.replace(/cmi\.interactions\.\d+\./,""),d[p]);
    }

    if(getVersion() == "1.2") {
      if("time" in data) {
        data.timestamp = data.time;
        delete data['time'];
      }
      if("result" in data && data.result=="wrong")
        data.result="incorrect";
      if("student_response" in data) {
        data.learner_response=data.student_response;
        delete data["student_response"];
      }
    }

    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','']
  */
  function finalize(type,callback) {
    if(!API.OK) return null; // No API

    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 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(getStatus())?'':'suspend') : type;

      // Flush suspended data cache
      suspenddata.save();

      switch(getVersion()) {
        case "1.2": e.push(API.SetValue("cmi.core.exit",(type==="")?"" : type));break;
        case "1.3": e.push(API.SetValue("cmi.exit",(type==="")?"normal": type));break;
        default: return false;
      }
      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
  */
  function commit() {
    // Flush suspended data cache
    suspenddata.save();
    return (API.Commit('')=='true') ? true : false;
  };
  // ----------------------------------------------------------------------------
  
  // Return the public methods
  return {
    "API":            API,
    "initialize":     initialize,
    "getVersion":     getVersion,
    "getLearnerId":   getLearnerId,
    "getLearnerName": getLearnerName,
    "getCredit":      getCredit,
    "getEntry":       getEntry,
    "getMode":        getMode,
    "getLaunchData":  getLaunchData,
    "setLocation":    setLocation,
    "getLocation":    getLocation,
    "setStatus":      setStatus,
    "getStatus":      getStatus,
    "setScore":       setScore,
    "getScore":       getScore,
    "setTime":        setTime,
    "getTime":        getTime,
    "setData":        setData,
    "getData":        getData,
    "setObjective":   setObjective,
    "getObjective":   getObjective,
    "setInteraction": setInteraction,
    "getInteraction": getInteraction,
    "finalize":       finalize,
    "commit":         commit
  };
}();
