function ScormAdapter() {
  function bind(s, w) {
    return scan(w=(w||window),s) || scan(w.opener, s) };
  function scan(w, s) {
    var i = 0; if (!w) return;
    while (w[s] == null && w.parent != w && i++ < 50) w = w.parent;
    return w[s];
  };
  this.constructor = ScormAdapter;
  this.api = function() {
    return !this.start ? 0 : bind("API");
  };
};

ScormAdapter.prototype = {

  api: function() {
    return !this.start ? 0 : bind("API");
  },

  bookmark: function(href) {
    return (typeof href != "undefined") ?
      this.set("cmi.core.lesson_location", href) :
      this.get("cmi.core.lesson_location");
  },

  children: function(key) {
    var s = this.get(key + "._children");
    return s ? s.split(/, ?/) : [];
  },

  collection: function(key) {
    var a = [], n = this.count(key), c = this.children(key), i, j;
    for (i = 0; i < n; i++) {
      var o = {index:i};
      for (j = 0; j < c.length; j++) {
        o[c[j]] = this.get(key + "." + i + "." + c[j]);
      }
      a[o.id] = o;
      a.push(o);
    }
    return a;
  },

  commit: function() {
    return !this.api() ? false : this.api().LMSCommit("");
  },

  complete: function(success) {
    this.set("cmi.core.lesson_status", (
    this.get("cmi.core.credit") == "credit") ?
      (success ? "passed" : "failed") :
      (success ? "complete" : "incomplete")
    );
  },

  count: function(key) {
    var n = parseFloat(this.get(key + "._count"));
    return isNaN(n) ? 0 : n;
  },

  data: function(object) {
    return (typeof object != "undefined") ?
      this.set("cmi.suspend_data", object) :
      this.get("cmi.suspend_data");
  },

  each: function(keys, block, context) {
    if (typeof keys == "string") {
      var c = this.children(keys), i = 0;
      for (i; i < c.length; i++) c[i] = keys + "." + c[i];
      keys = c;
    }
    for (var i = 0; i < keys.length; i++) {
      block.call(context,
        this.get(keys[i]), // value
        keys[i],           // key
        keys,              // keys
        this.error()       // error #
      );
    }
  },

  error: function() {
    return !this.api() ? -1 : parseFloat(this.api().LMSGetLastError(""));
  },

  "get": function(key) {
    return !this.api() ? "" : this.api().LMSGetValue(key);
  },

  "set": function(key, value) {
    if (typeof value == "object") {
      var o = {}, p; for (p in value)
          o[p] = this.set(key + "." + p, value[p]);
      return o;
    }
    return !this.api() ? false : this.api().LMSSetValue(key, value);
  },

  initialize: function() {
    this.start = new Date();
    if (this.api()) {
      this.api().LMSInitialize("");
      if (this.get("cmi.core.lesson_status") == "not attempted")
        this.set("cmi.core.lesson_status", "browsed");
      // may not be appropriate for all scenarios
      this.set("cmi.core.exit", "suspend");
      return true;
    }
    return false;
  },

  interactions: function(s) {
    var c = this.collection("cmi.core.interactions");
    return /^(s|n)/.test(typeof s) ? c[s] : c;
  },

  session: function() {
    return this.toTime(((new Date) - this.start) / 1000);
  },

  terminate: function(status) {
    if (this.api()) {
      if (/^(passed|completed|failed|incomplete|browsed|not attempted)$/.test(status))
      this.set("cmi.core.lesson_status", status);
      this.set("cmi.core.session_time", this.session());
      var r = this.api().LMSFinish("");
      delete this.start;
      return r;
    }
    return false;
  },

  toSeconds: function(timespan) {
    var a = (timespan || "0:0:0").split(":"), n = parseFloat;
    return (n(a[0]) * 3600) + (n(a[1]) * 60) + (n(a[2]));
  },

  toTime: function(seconds) { // CMITimespan HHHH:MM::SS.SS
    var d = function(n) {return n < 10 ? "0" + n : n;};
    var n = parseFloat(seconds); if (isNaN(n)) n = 0;
    var h = Math.max(Math.floor(n/3600), 0); n-=(h*3600);
    var m = Math.max(Math.floor(n/60), 0); n-=(m*60);
    return d(h) +":"+ d(m) +":"+ d(String(n).substring(0, 4));
  },

  errorCodes: {
   '-1': 'No API Adapter Available.',
      0: 'No Error',
    101: 'General exception',
    201: 'Invalid argument error',
    202: 'Element cannot have children',
    203: 'Element not an array - cannot have count',
    301: 'Not initialized',
    401: 'Not implemented error',
    402: 'Invalid set value, element is a keyword',
    403: 'Element is read only',
    404: 'Element is write only',
    405: 'Incorrect Data Type'
  }

};