
var VERSION = "7.7.0";

var PREFERENCE_DEFAULT = 0;
var PREFERENCE_OFF     = -1;
var PREFERENCE_ON      = 1;

var LESSON_STATUS_PASSED        = 1;    //the user completed the content with a score sufficient to pass
var LESSON_STATUS_COMPLETED     = 2;    //the user completed the content
var LESSON_STATUS_FAILED        = 3;    //the user completed the content but his/her score was not sufficient to pass
var LESSON_STATUS_INCOMPLETE    = 4;    //the user began the content but did not complete it
var LESSON_STATUS_BROWSED       = 5;    //the user looked at the content but was not making a recorded attempt at it
var LESSON_STATUS_NOT_ATTEMPTED = 6;    //the user has not started the content

var ENTRY_REVIEW     = 1;
var ENTRY_FIRST_TIME = 2;
var ENTRY_RESUME     = 3;

var MODE_NORMAL = 1;
var MODE_BROWSE = 2;
var MODE_REVIEW = 3;

var MAX_CMI_TIME = 36002439990; //max CMI Timespan 9999:99:99.99

var NO_ERROR = 0;
var ERROR_LMS = 1;
var ERROR_INVALID_PREFERENCE = 2;
var ERROR_INVALID_NUMBER = 3;
var ERROR_INVALID_ID = 4;
var ERROR_INVALID_STATUS = 5;
var ERROR_INVALID_RESPONSE = 6;
var ERROR_NOT_LOADED = 7;
var ERROR_INVALID_INTERACTION_RESPONSE = 8;

var EXIT_TYPE_SUSPEND = "SUSPEND";
var EXIT_TYPE_FINISH  = "FINISH";
var EXIT_TYPE_TIMEOUT = "TIMEOUT";
var EXIT_TYPE_UNLOAD  = "UNLOAD";

var INTERACTION_RESULT_CORRECT  = "CORRECT";
var INTERACTION_RESULT_WRONG  = "WRONG";
var INTERACTION_RESULT_UNANTICIPATED  = "UNANTICIPATED";
var INTERACTION_RESULT_NEUTRAL = "NEUTRAL";

var INTERACTION_TYPE_TRUE_FALSE = "true-false";
var INTERACTION_TYPE_CHOICE = "choice";
var INTERACTION_TYPE_FILL_IN = "fill-in";
var INTERACTION_TYPE_LONG_FILL_IN = "long-fill-in";
var INTERACTION_TYPE_MATCHING = "matching";
var INTERACTION_TYPE_PERFORMANCE = "performance";
var INTERACTION_TYPE_SEQUENCING = "sequencing";
var INTERACTION_TYPE_LIKERT = "likert";
var INTERACTION_TYPE_NUMERIC = "numeric";

var DATA_CHUNK_PAIR_SEPARATOR = '###';
var DATA_CHUNK_VALUE_SEPARATOR = '$$';

//Configuration Parameters
var APPID = "__APPID__";                            //this is used for SCORM Cloud. Set to "" to disable.
var CLOUDURL = "__CLOUDURL__";

var blnDebug = true;                        //set this to false if you don't want the overhead of recording debug information

var strLMSStandard = "SCORM2004";    //used in versions that support multiple standards, set to "NONE" to default
                                            //to StandAlone mode. Possible values = "AUTO", "NONE", "SCORM", "AICC", "SCORM2004", "TCAPI", "CMI5"
                                            //AUTO mode will automatically determine the best standard to use
                                            //(it first tries AICC based on "AICC_URL" query param, then TCAPI and CMI5 based on "endpoint" and other
                                            //query parameters, then SCORM 2004, then SCORM 1.2/1.1 then NONE

var DEFAULT_EXIT_TYPE = EXIT_TYPE_SUSPEND;  //When the content is unloaded without an API function indicating the type of exit,
                                            //what default behavior do you want to assume.  Use EXIT_TYPE_SUSPEND if you plan to
                                            //call Finish when the content is complete.  Use EXIT_TYPE_FINISH if you do not plan
                                            //to call Finish.


var AICC_LESSON_ID = "1";                   //if recording question answers in AICC in an LMS that supports interactions,
                                            //this field need to match the system_id on line in the .DES file that describes
                                            //this course, the default is 1. Be sure that this value does not contain double quote characters (")

var EXIT_BEHAVIOR = "SCORM_RECOMMENDED";        //used to control window closing behavior on call of ConcedeControl
                                                //Possible Values: SCORM_RECOMMENDED, ALWAYS_CLOSE, ALWAYS_CLOSE_TOP, ALWAYS_CLOSE_PARENT,
                                                //                 NOTHING, REDIR_CONTENT_FRAME, LMS_SPECIFIED_REDIRECT
                                                //
                                                // Not used for cmi5

var EXIT_TARGET = "lms/goodbye.html";       //Used in conjunction with EXIT_BEHAVIOR, only with REDIR_CONTENT_FRAME. This should be a neutral page that is displayed
                                            //after the course has exited, but before it has been taked away by the LMS

var AICC_COMM_DISABLE_XMLHTTP = false;      //false is the preferred value, true can be required in certain cross domain situations
var AICC_COMM_DISABLE_IFRAME = false;       //false is the preferred value, true can be required in certain cross domain situations

var AICC_COMM_PREPEND_HTTP_IF_MISSING = true;       //Some AICC LMS's will omit the "http://" from the AICC_URL value. If this is the case,
                                                    //set this setting to true to have the API prepend the "http://" value

var AICC_REPORT_MIN_MAX_SCORE = true;       //Some AICC LMS's have trouble processing a score which contains a min and max value. Setting this
                                            //value to false allows you to turn off that reporting to accommodate those LMS's.


var SHOW_DEBUG_ON_LAUNCH = false;       //set this to true when debugging to force the debug window to launch immediately

var DO_NOT_REPORT_INTERACTIONS = false;     //set this to true to disable reporting of question results to the LMS as some LMS's
                                            //particularly those supporting only AICC can have problems with interactions results


var SCORE_CAN_ONLY_IMPROVE = false;         //set this to true to ensure that on subsequent attempts, a learner's score can only go up

var REVIEW_MODE_IS_READ_ONLY = true;        //set this to true if no new data should be saved when a course is launched in review mode (normally this is the LMS's responsibility)


var TCAPI_DONT_USE_BROKEN_URN_IDS = true;       //Prior to 7.0, there was a regression that caused some activity IDs to be invalid URIs. With this flag set to true,
                                                //the IDs will be set to valid URIs. However, this will cause a difference of IDs between interactions from before
                                                //7.0 and after. Therefore, set this to false when running any courses that were recorded using a version of Driver earlier than 7.0.


/*
These variables control how long the API should wait on an AICC form submission before timing out
AICC_RE_CHECK_LOADED_INTERVAL = Number of milliseconds the API waits between checks to see if the form is loaded
AICC_RE_CHECK_ATTEMPTS_BEFORE_TIMEOUT = Number of times the API checks to see if the form is loaded
AICC_RE_CHECK_LOADED_INTERVAL * AICC_RE_CHECK_ATTEMPTS_BEFORE_TIMEOUT = Desired time out in milliseconds
*/
var AICC_RE_CHECK_LOADED_INTERVAL = 250;
var AICC_RE_CHECK_ATTEMPTS_BEFORE_TIMEOUT = 240;

var USE_AICC_KILL_TIME = true;              //set this to false to disable the explicit extra waiting between AICC requests

//This controls the entry default when it is not set by the LMS
//Possible options are
//ENTRY_REVIEW (normal default) - ENTRY_FIRST_TIME - ENTRY_RESUME
var AICC_ENTRY_FLAG_DEFAULT = ENTRY_REVIEW;

var AICC_USE_CUSTOM_COMMS = false; //Used to force the AICC communications through a custom routine (defined in indexAPI.html) instead of other methods

var FORCED_COMMIT_TIME = "60000"; //Used to force CommitData back to the LMS at the desired interval (in milliseconds). Set to 0 (zero) to not force a commit time.

var ALLOW_NONE_STANDARD = true; //Set this to false to show an error message if no LMS API can be found - Standalone will still work fine.

var USE_2004_SUSPENDALL_NAVREQ = false;
var USE_STRICT_SUSPEND_DATA_LIMITS = false; //determines if we check the suspend_data length in Driver
var EXIT_SUSPEND_IF_COMPLETED = false; //determines if Suspend is sent for exit when SetReachedEnd is called - legacy driver defaulted to false
var EXIT_NORMAL_IF_PASSED = false; //determines if exit is Normal when Passed - legacy driver defaulted to false and always sends suspend
var AICC_ENCODE_PARAMETER_VALUES = true; //default is true;

var PASS_FAIL_SETS_COMPLETION_FOR_2004 = true;  //determines if SetPassed and SetFailed also set completion for SCORM 2004
var ALLOW_INTERACTION_NULL_LEARNER_RESPONSE = false;  //determines whether learner response is required when recording interactions
var PREVENT_STATUS_CHANGE_DURING_INIT = false;  //when true prevents initialization process from setting complete/success status from LMS result
var USE_LEGACY_IDENTIFIERS_FOR_2004 = true; //set to `true` to disable conversion of identifiers to proper URIs (may make content non-compliant with SCORM spec)
var URI_IDENTIFIER_PREFIX = "urn:scormdriver:"; //used to make proper URI identifiers out of identifiers that are detected as non-absolute URIs

window.scormdriver_content = window;

function LoadContent() {}

function GetQueryStringValue(strElement, strQueryString){

    var aryPairs;
    var foundValue;

    //WriteToDebug("In GetQueryStringValue strElement=" + strElement + ", strQueryString=" + strQueryString);

    //get rid of the leading "?"
    strQueryString = strQueryString.substring(1);

    //split into name/value pairs
    aryPairs = strQueryString.split("&");

    foundValue = SearchQueryStringPairs(aryPairs, strElement);

    if (foundValue === null){
        //sometimes we wind up with a querystring that looks like "indexAPI.html?ShowDebug=true?AICC_URL=http://www.blah.com/blahblah&AICC_SID=12345"
        //try to handle this case by splitting on both ? and &

        aryPairs = strQueryString.split(/[\?\&]/);
        foundValue = SearchQueryStringPairs(aryPairs, strElement);
    }

    if (foundValue === null){
        //if we didn't find a match, return an empty string
        WriteToDebug("GetQueryStringValue Element '" + strElement + "' Not Found, Returning: empty string");
        return "";
    }
    else{
        WriteToDebug("GetQueryStringValue for '" + strElement + "' Returning: " + foundValue);
        return foundValue;
    }
}

function SearchQueryStringPairs(aryPairs, strElement){

    var i;
    var intEqualPos;
    var strArg = "";
    var strValue = "";

    strElement = strElement.toLowerCase();

    //search each querystring value and return the first match
    for (i=0; i < aryPairs.length; i++){

        //WriteToDebug("Looking at element-" + aryPairs[i]);

        intEqualPos = aryPairs[i].indexOf('=');

        if (intEqualPos != -1){

            strArg = aryPairs[i].substring(0,intEqualPos);

            //WriteToDebug("Arg=" + strArg);

            if (EqualsIgnoreCase(strArg, strElement)){

                //WriteToDebug("Found Match");
                strValue = aryPairs[i].substring(intEqualPos+1);

                //WriteToDebug("Decoding value-" + strValue);

                strValue = new String(strValue)

                strValue = strValue.replace(/\+/g, "%20")
                strValue = unescape(strValue);

                //WriteToDebug("Returning value-" + strValue);

                return new String(strValue);
            }
        }
    }

    return null;
}

function ConvertStringToBoolean(str){

    var intTemp;

    //WriteToDebug("In ConvertStringToBoolean");

    if (EqualsIgnoreCase(str, "true") || EqualsIgnoreCase(str, "t") || str.toLowerCase().indexOf("t") == 0){
        //WriteToDebug("Found alpha true");
        return true;
    }
    else{
        intTemp = parseInt(str, 10);
        if (intTemp == 1 || intTemp==-1){
            //WriteToDebug("Found numeric true");
             return true;
        }
        else{
            //WriteToDebug("Returning false");
            return false;
        }
    }
}



function EqualsIgnoreCase(str1, str2){

    //WriteToDebug("In EqualsIgnoreCase str1=" + str1 + ", str2=" + str2);

    var blnReturn;

    str1 = new String(str1);
    str2 = new String(str2);

    blnReturn = (str1.toLowerCase() == str2.toLowerCase())

    //WriteToDebug("Returning " + blnReturn);

    return blnReturn;
}


function ValidInteger(intNum){

    WriteToDebug("In ValidInteger intNum=" + intNum);

    var str = new String(intNum);

    if (str.indexOf("-", 0) == 0){
        str = str.substring(1, str.length - 1);
    }

    var regValidChars= new RegExp("[^0-9]");

    if (str.search(regValidChars) == -1){
        WriteToDebug("Returning true");
        return true;
    }

    WriteToDebug("Returning false");
    return false;
}


//---------------------------------------------------------------------------------
//Time Manipulation Functions

function ConvertDateToIso8601TimeStamp(dtm){

    var strTimeStamp;

    dtm = new Date(dtm);

    var Year   = dtm.getFullYear();
    var Month  = dtm.getMonth() + 1;
    var Day    = dtm.getDate();
    var Hour   = dtm.getHours();
    var Minute = dtm.getMinutes();
    var Second = dtm.getSeconds();

    Month  = ZeroPad(Month, 2);
    Day    = ZeroPad(Day, 2);
    Hour   = ZeroPad(Hour, 2);
    Minute = ZeroPad(Minute, 2);
    Second = ZeroPad(Second, 2);

    strTimeStamp = Year + "-" + Month + "-" + Day + "T" + Hour + ":" + Minute + ":" + Second;


    //JBR 11-9-2011 - adding timezone offset
    // should only be called for SCORM 2004+
    var tzoffset = -(dtm.getTimezoneOffset()/60);
    if(tzoffset!=0){
        strTimeStamp += '.0'; // this seconds fraction is required if you have a TZOffset
        if(tzoffset>0){
            // handle the fraction offest in minutes
            if((''+tzoffset).indexOf('.')!=-1){
                var fraction = '0'+(''+tzoffset).substr((''+tzoffset).indexOf('.'),(''+tzoffset).length);
                var base = (''+tzoffset).substr(0,(''+tzoffset).indexOf('.'));
                fraction = (fraction*60);
                strTimeStamp += '+'+ZeroPad(base+'.'+fraction,2);
            }else{
                // add the '+' if it's needed (the - is handled by ZeroPad)
                strTimeStamp += '+'+ZeroPad(tzoffset,2);
            }
        }else{
            strTimeStamp += ZeroPad(tzoffset,2);
        }
    }

    return strTimeStamp;

}

function ConvertIso8601TimeStampToDate(strTimeStamp){

    strTimeStamp = new String(strTimeStamp);
    var ary = new Array();
    ary = strTimeStamp.split(/[\:T+-]/);

    var Year   = ary[0];
    var Month  = ary[1]-1;
    var Day    = ary[2];
    var Hour   = ary[3];
    var Minute = ary[4];
    var Second = ary[5];


    return new Date(Year, Month, Day, Hour, Minute, Second, 0);

}

function ConvertDateToCMIDate(dtmDate){

    WriteToDebug("In ConvertDateToCMIDate");

    var strYear;
    var strMonth;
    var strDay;
    var strReturn;

    dtmDate = new Date(dtmDate);

    strYear = dtmDate.getFullYear()
    strMonth = (dtmDate.getMonth() + 1);
    strDay = dtmDate.getDate();

    strReturn = ZeroPad(strYear, 4) + "/" + ZeroPad(strMonth, 2) + "/" + ZeroPad(strDay, 2);

    return strReturn;
}

function ConvertDateToCMITime(dtmDate){

    var strHours;
    var strMinutes;
    var strSeconds;
    var strReturn;

    dtmDate = new Date(dtmDate);

    strHours = dtmDate.getHours();
    strMinutes = dtmDate.getMinutes();
    strSeconds = dtmDate.getSeconds();

    strReturn = ZeroPad(strHours, 2) + ":" + ZeroPad(strMinutes, 2) + ":" + ZeroPad(strSeconds, 2);

    return strReturn;
}


function ConvertCMITimeSpanToMS(strTime){

    WriteToDebug("In ConvertCMITimeSpanToMS, strTime=" + strTime);

    var aryParts;
    var intHours;
    var intMinutes;
    var intSeconds;

    var intTotalMilliSeconds;

    //split the string into its parts
    aryParts = strTime.split(":");

    //make sure it's valid before we knock ourselves out
    if (! IsValidCMITimeSpan(strTime)){
        WriteToDebug("ERROR - Invalid TimeSpan");
        SetErrorInfo(SCORM_ERROR_GENERAL, "LMS ERROR - Invalid time span passed to ConvertCMITimeSpanToMS, please contact technical support");
        return 0;
    }

    //seperate the parts and multiply by the appropriate constant (3600000 = num milliseconds in an hour, etc)
    intHours   = aryParts[0];
    intMinutes = aryParts[1];
    intSeconds = aryParts[2];   //don't need to worry about milliseconds b/c they are expressed as fractions of a second

    WriteToDebug("intHours=" + intHours + " intMinutes=" + intMinutes + " intSeconds=" + intSeconds);

    intTotalMilliSeconds = (intHours * 3600000) + (intMinutes * 60000) + (intSeconds * 1000);

    //necessary because in JavaScript, some values for intSeconds (such as 2.01) will have a lot of decimal
    //places when multiplied by 1000. For instance, 2.01 turns into 2009.999999999999997.
    intTotalMilliSeconds = Math.round(intTotalMilliSeconds);

    WriteToDebug("Returning " + intTotalMilliSeconds);

    return intTotalMilliSeconds;
}


function ConvertScorm2004TimeToMS(strIso8601Time){

    WriteToDebug("In ConvertScorm2004TimeToMS, strIso8601Time=" + strIso8601Time);

    var intTotalMs = 0;

    var strNumberBuilder;
    var strCurrentCharacter;
    var blnInTimeSection;


    var Seconds = 0;    // 100 hundreths of a seconds
    var Minutes = 0;    // 60 seconds
    var Hours = 0;      // 60 minutes
    var Days = 0;       // 24 hours
    var Months = 0;     // assumed to be an "average" month (figures a leap year every 4 years) = ((365*4) + 1) / 48 days - 30.4375 days per month
    var Years = 0;      // assumed to be 12 "average" months

    var MILLISECONDS_PER_SECOND = 1000;
    var MILLISECONDS_PER_MINUTE = MILLISECONDS_PER_SECOND * 60;
    var MILLISECONDS_PER_HOUR   = MILLISECONDS_PER_MINUTE * 60;
    var MILLISECONDS_PER_DAY    = MILLISECONDS_PER_HOUR * 24;
    var MILLISECONDS_PER_MONTH  = MILLISECONDS_PER_DAY * (((365 * 4) + 1) / 48);
    var MILLISECONDS_PER_YEAR   = MILLISECONDS_PER_MONTH * 12;


    strIso8601Time = new String(strIso8601Time);

    strNumberBuilder = "";
    strCurrentCharacter = "";
    blnInTimeSection = false;

    //start at 1 to get past the "P"
    for (var i=1; i < strIso8601Time.length; i++){

        strCurrentCharacter = strIso8601Time.charAt(i);


        if (IsIso8601SectionDelimiter(strCurrentCharacter)){

            switch (strCurrentCharacter.toUpperCase()){

                case "Y":
                    Years = parseInt(strNumberBuilder, 10);
                break;

                case "M":
                    if (blnInTimeSection){
                        Minutes = parseInt(strNumberBuilder, 10);
                    }
                    else{
                        Months = parseInt(strNumberBuilder, 10);
                    }
                break;

                case "D":
                    Days = parseInt(strNumberBuilder, 10);
                break;

                case "H":
                    Hours = parseInt(strNumberBuilder, 10);
                break;

                case "S":
                    Seconds = parseFloat(strNumberBuilder);
                break;

                case "T":
                    blnInTimeSection = true;
                break;

            }

            strNumberBuilder = "";
        }
        else{
            strNumberBuilder += "" + strCurrentCharacter;       //use "" to keep the number as string concats instead of numeric additions
        }

    }

    WriteToDebug("Years=" + Years + "\n" +
                "Months=" + Months + "\n" +
                "Days=" + Days + "\n" +
                "Hours=" + Hours + "\n" +
                "Minutes=" + Minutes + "\n" +
                "Seconds=" + Seconds + "\n"
                );

    intTotalMs = (Years * MILLISECONDS_PER_YEAR) +
                 (Months * MILLISECONDS_PER_MONTH) +
                 (Days * MILLISECONDS_PER_DAY) +
                 (Hours * MILLISECONDS_PER_HOUR) +
                 (Minutes * MILLISECONDS_PER_MINUTE) +
                 (Seconds * MILLISECONDS_PER_SECOND);

    //necessary because in JavaScript, some values (such as 2.01) will have a lot of decimal
    //places when multiplied by a larger number. For instance, 2.01 turns into 2009.999999999999997.
    intTotalMs = Math.round(intTotalMs);

    WriteToDebug ("returning-" + intTotalMs);

    return intTotalMs;

}

function IsIso8601SectionDelimiter(str){

    if (str.search(/[PYMDTHS]/) >=0 ){
        return true;
    }
    else{
        return false;
    }

}


function IsValidCMITimeSpan(strValue){

    WriteToDebug("In IsValidCMITimeSpan strValue=" + strValue);

    //note that the spec does not say that minutes or seconds have to be < 60

    var regValid = /^\d?\d?\d?\d:\d?\d:\d?\d(.\d\d?)?$/;

    if (strValue.search(regValid) > -1){
        WriteToDebug("Returning True");
        return true;
    }
    else{
        WriteToDebug("Returning False");
        return false;
    }
}



function IsValidIso8601TimeSpan(strValue){

    WriteToDebug("In IsValidIso8601TimeSpan strValue=" + strValue);

    var regValid = /^P(\d+Y)?(\d+M)?(\d+D)?(T(\d+H)?(\d+M)?(\d+(.\d\d?)?S)?)?$/;

    if (strValue.search(regValid) > -1){
        WriteToDebug("Returning True");
        return true;
    }
    else{
        WriteToDebug("Returning False");
        return false;
    }
}

function ConvertMilliSecondsToTCAPITime(intTotalMilliseconds, blnIncludeFraction){

    var intHours;
    var intMinutes;
    var intSeconds;
    var intMilliseconds;
    var intHundredths;
    var strCMITimeSpan;

    WriteToDebug("In ConvertMilliSecondsToTCAPITime, intTotalMilliseconds = " + intTotalMilliseconds + ", blnIncludeFraction = " + blnIncludeFraction);

    if (blnIncludeFraction == null || blnIncludeFraction == undefined){
        blnIncludeFraction = true;
    }

    //extract time parts
    intMilliseconds = intTotalMilliseconds % 1000;

    intSeconds = ((intTotalMilliseconds - intMilliseconds) / 1000) % 60;

    intMinutes = ((intTotalMilliseconds - intMilliseconds - (intSeconds * 1000)) / 60000) % 60;

    intHours = (intTotalMilliseconds - intMilliseconds - (intSeconds * 1000) - (intMinutes * 60000)) / 3600000;

    WriteToDebug("Separated Parts, intHours=" + intHours + ", intMinutes=" + intMinutes + ", intSeconds=" + intSeconds + ", intMilliseconds=" + intMilliseconds);

    /*
    deal with exceptional case when content used a huge amount of time and interpreted CMITimstamp
    to allow a number of intMinutes and seconds greater than 60 i.e. 9999:99:99.99 instead of 9999:60:60:99
    note - this case is permissable under SCORM, but will be exceptionally rare
    */

    if (intHours == 10000)
    {
        WriteToDebug("Max intHours detected");

        intHours = 9999;

        intMinutes = (intTotalMilliseconds - (intHours * 3600000)) / 60000;
        if (intMinutes == 100)
        {
            intMinutes = 99;
        }
        intMinutes = Math.floor(intMinutes);

        intSeconds = (intTotalMilliseconds - (intHours * 3600000) - (intMinutes * 60000)) / 1000;
        if (intSeconds == 100)
        {
            intSeconds = 99;
        }
        intSeconds = Math.floor(intSeconds);

        intMilliseconds = (intTotalMilliseconds - (intHours * 3600000) - (intMinutes * 60000) - (intSeconds * 1000));

        WriteToDebug("Separated Parts, intHours=" + intHours + ", intMinutes=" + intMinutes + ", intSeconds=" + intSeconds + ", intMilliseconds=" + intMilliseconds);

    }

    //drop the extra precision from the milliseconds
    intHundredths = Math.floor(intMilliseconds / 10);

    //put in padding 0's and concatinate to get the proper format
    strCMITimeSpan = ZeroPad(intHours, 4) + ":" + ZeroPad(intMinutes, 2) + ":" + ZeroPad(intSeconds, 2);

    if (blnIncludeFraction){
        strCMITimeSpan += "." + intHundredths;
    }

    WriteToDebug("strCMITimeSpan=" + strCMITimeSpan);

    //check for case where total milliseconds is greater than max supported by strCMITimeSpan
    if (intHours > 9999)
    {
        strCMITimeSpan = "9999:99:99";

        if (blnIncludeFraction){
            strCMITimeSpan += ".99";
        }
    }

    WriteToDebug("returning " + strCMITimeSpan);

    return strCMITimeSpan;

}

function ConvertMilliSecondsToSCORMTime(intTotalMilliseconds, blnIncludeFraction){

    var intHours;
    var intMinutes;
    var intSeconds;
    var intMilliseconds;
    var intHundredths;
    var strCMITimeSpan;

    WriteToDebug("In ConvertMilliSecondsToSCORMTime, intTotalMilliseconds = " + intTotalMilliseconds + ", blnIncludeFraction = " + blnIncludeFraction);

    if (blnIncludeFraction == null || blnIncludeFraction == undefined){
        blnIncludeFraction = true;
    }

    //extract time parts
    intMilliseconds = intTotalMilliseconds % 1000;

    intSeconds = ((intTotalMilliseconds - intMilliseconds) / 1000) % 60;

    intMinutes = ((intTotalMilliseconds - intMilliseconds - (intSeconds * 1000)) / 60000) % 60;

    intHours = (intTotalMilliseconds - intMilliseconds - (intSeconds * 1000) - (intMinutes * 60000)) / 3600000;

    WriteToDebug("Separated Parts, intHours=" + intHours + ", intMinutes=" + intMinutes + ", intSeconds=" + intSeconds + ", intMilliseconds=" + intMilliseconds);

    /*
    deal with exceptional case when content used a huge amount of time and interpreted CMITimstamp
    to allow a number of intMinutes and seconds greater than 60 i.e. 9999:99:99.99 instead of 9999:60:60:99
    note - this case is permissable under SCORM, but will be exceptionally rare
    */

    if (intHours == 10000)
    {
        WriteToDebug("Max intHours detected");

        intHours = 9999;

        intMinutes = (intTotalMilliseconds - (intHours * 3600000)) / 60000;
        if (intMinutes == 100)
        {
            intMinutes = 99;
        }
        intMinutes = Math.floor(intMinutes);

        intSeconds = (intTotalMilliseconds - (intHours * 3600000) - (intMinutes * 60000)) / 1000;
        if (intSeconds == 100)
        {
            intSeconds = 99;
        }
        intSeconds = Math.floor(intSeconds);

        intMilliseconds = (intTotalMilliseconds - (intHours * 3600000) - (intMinutes * 60000) - (intSeconds * 1000));

        WriteToDebug("Separated Parts, intHours=" + intHours + ", intMinutes=" + intMinutes + ", intSeconds=" + intSeconds + ", intMilliseconds=" + intMilliseconds);

    }

    //drop the extra precision from the milliseconds
    intHundredths = Math.floor(intMilliseconds / 10);

    //put in padding 0's and concatinate to get the proper format
    strCMITimeSpan = ZeroPad(intHours, 4) + ":" + ZeroPad(intMinutes, 2) + ":" + ZeroPad(intSeconds, 2);

    if (blnIncludeFraction){
        strCMITimeSpan += "." + intHundredths;
    }

    WriteToDebug("strCMITimeSpan=" + strCMITimeSpan);

    //check for case where total milliseconds is greater than max supported by strCMITimeSpan
    if (intHours > 9999)
    {
        strCMITimeSpan = "9999:99:99";

        if (blnIncludeFraction){
            strCMITimeSpan += ".99";
        }
    }

    WriteToDebug("returning " + strCMITimeSpan);

    return strCMITimeSpan;

}


// TODO: using years and months in the ISO8601 interval gives varying results
//       because of the change in number of days in months (and years--leap)
function ConvertMilliSecondsIntoSCORM2004Time(intTotalMilliseconds){

    WriteToDebug("In ConvertMilliSecondsIntoSCORM2004Time intTotalMilliseconds=" + intTotalMilliseconds);

    var ScormTime = "";

    var HundredthsOfASecond;    //decrementing counter - work at the hundreths of a second level because that is all the precision that is required

    var Seconds;    // 100 hundreths of a seconds
    var Minutes;    // 60 seconds
    var Hours;      // 60 minutes
    var Days;       // 24 hours
    var Months;     // assumed to be an "average" month (figures a leap year every 4 years) = ((365*4) + 1) / 48 days - 30.4375 days per month
    var Years;      // assumed to be 12 "average" months

    var HUNDREDTHS_PER_SECOND = 100;
    var HUNDREDTHS_PER_MINUTE = HUNDREDTHS_PER_SECOND * 60;
    var HUNDREDTHS_PER_HOUR   = HUNDREDTHS_PER_MINUTE * 60;
    var HUNDREDTHS_PER_DAY    = HUNDREDTHS_PER_HOUR * 24;
    var HUNDREDTHS_PER_MONTH  = HUNDREDTHS_PER_DAY * (((365 * 4) + 1) / 48);
    var HUNDREDTHS_PER_YEAR   = HUNDREDTHS_PER_MONTH * 12;


    HundredthsOfASecond = Math.floor(intTotalMilliseconds / 10);

    Years = Math.floor(HundredthsOfASecond / HUNDREDTHS_PER_YEAR);
    HundredthsOfASecond -= (Years * HUNDREDTHS_PER_YEAR);

    Months = Math.floor(HundredthsOfASecond / HUNDREDTHS_PER_MONTH);
    HundredthsOfASecond -= (Months * HUNDREDTHS_PER_MONTH);

    Days = Math.floor(HundredthsOfASecond / HUNDREDTHS_PER_DAY);
    HundredthsOfASecond -= (Days * HUNDREDTHS_PER_DAY);

    Hours = Math.floor(HundredthsOfASecond / HUNDREDTHS_PER_HOUR);
    HundredthsOfASecond -= (Hours * HUNDREDTHS_PER_HOUR);

    Minutes = Math.floor(HundredthsOfASecond / HUNDREDTHS_PER_MINUTE);
    HundredthsOfASecond -= (Minutes * HUNDREDTHS_PER_MINUTE);

    Seconds = Math.floor(HundredthsOfASecond / HUNDREDTHS_PER_SECOND);
    HundredthsOfASecond -= (Seconds * HUNDREDTHS_PER_SECOND);


    if (Years > 0) {
        ScormTime += Years + "Y";
    }
    if (Months > 0){
        ScormTime += Months + "M";
    }
    if (Days > 0){
        ScormTime += Days + "D";
    }

    //check to see if we have any time before adding the "T"
    if ((HundredthsOfASecond + Seconds + Minutes + Hours) > 0 ){

        ScormTime += "T";

        if (Hours > 0){
            ScormTime += Hours + "H";
        }

        if (Minutes > 0){
            ScormTime += Minutes + "M";
        }

        if ((HundredthsOfASecond + Seconds) > 0){
            ScormTime += Seconds;

            if (HundredthsOfASecond > 0){
                ScormTime += "." + HundredthsOfASecond;
            }

            ScormTime += "S";
        }

    }


    if (ScormTime == ""){
        ScormTime = "T0S";
    }

    ScormTime = "P" + ScormTime;

    WriteToDebug("Returning-" + ScormTime);

    return ScormTime;
}



function ZeroPad(intNum, intNumDigits){

    WriteToDebug("In ZeroPad intNum=" + intNum + " intNumDigits=" + intNumDigits);

    var strTemp;
    var intLen;
    var decimalToPad;
    var i;
    var isNeg = false;
    //need to check to see if intNum has +/- at the front

    strTemp = new String(intNum);

    if(strTemp.indexOf('-') != -1){
        isNeg = true;
        //remove the '-' for now
        strTemp = strTemp.substr(1,strTemp.length);
    }

    //check for .5 (used in TimeZone parsing)
    if(strTemp.indexOf('.') != -1){
        strTemp.replace('.','');
        decimalToPad = strTemp.substr(strTemp.indexOf('.')+1,strTemp.length);
        strTemp = strTemp.substr(0,strTemp.indexOf('.'));
    }

    intLen = strTemp.length;

    if (intLen > intNumDigits){
        WriteToDebug("Length of string is greater than num digits, trimming string");
        strTemp = strTemp.substr(0,intNumDigits);
    }
    else{
        for (i=intLen; i<intNumDigits; i++){
            strTemp = "0" + strTemp;
        }
    }

    if(isNeg==true){
        //add the '-' back if needed
        strTemp = '-'+strTemp;
    }

    if(decimalToPad!=null && decimalToPad!=''){
        if(decimalToPad.length==1){
            strTemp += ':'+decimalToPad+'0';
        }else{
            strTemp += ':'+decimalToPad;
        }
    }

    WriteToDebug("Returning - " + strTemp);
    return strTemp;
}

function IsValidDecimalRange(strValue){
    WriteToDebug("In IsDecimalRange, strValue=" + strValue);

    var REG_EX_REAL_DELIMITER = "[:]",
        aryNumericRangeSplit,
        strMin,
        strMax;

    strValue = new String(strValue);

    aryNumericRangeSplit = strValue.split(REG_EX_REAL_DELIMITER);

    if (aryNumericRangeSplit.length === 2){
        //data is in min[:]max notation. Let's check it....
        strMin = Trim(aryNumericRangeSplit[0]);
        strMax = Trim(aryNumericRangeSplit[1]);

        //It's valid to have N[:]N, N[:]M, N[:], [:]N, [:] so we only check if the part is a valid decimal if it's been supplied.
        if (strMin.length > 0 && !IsValidDecimal(strMin))
        {
            WriteToDebug("Returning False - min value supplied range is not a valid decimal, min=" + strMin);
            return false;
        }
        if (strMax.length > 0 && !IsValidDecimal(strMax))
        {
            WriteToDebug("Returning False - max value supplied for range is not a valid decimal, max=" + strMax);
            return false;
        }
        //min cannot be greater than max. It can be the same however....
        if (strMin.length > 0 && strMax.length > 0 && parseFloat(strMin) > parseFloat(strMax))
        {
            WriteToDebug("Returning False - min value supplied for range is greater than the max, min=" + strMin + ", max=" + strMax);
            return false;
        }
        return true;

    }

    WriteToDebug("Returning false - string supplied for range has incorrect number of parts, parts=" + aryNumericRangeSplit.length + ", strValue=" +strValue);
    return false;
}


//This function assumes that the strValue parameter that is the numeric range has been previously
//checked for validity using IsValidDecimalRange() prior to calling this function.


//The scenarios for conversion are described below (format is <min>[:]<max>).....

//If the learner's response was correct return their response. This covers scenarios:
//N[:]N + correct -> learner response
//N[:]M + correct -> learner response
//N[:] + correct -> learner response
//[:]N + correct -> learner response
//[:] + correct -> learner response

//If <min> is present and the learner was incorrect, then return the min. This covers scenarios:
//N[:]N + incorrect -> N
//N[:]M + incorrect -> N
//N[:] + incorrect -> N

//If <max> is present and learner was incorrect, then return the max. This covers scenarios:
//[:]N + incorrect -> N

//[:] + incorrect -> this should not be possible as [:] indicates any number is correct and the learner response should
//have been checked for being a decimal. Return null here. Calling methods should check for null and act accordingly
function ConvertDecimalRangeToDecimalBasedOnLearnerResponse(strValue, strLearnerResponse, blnCorrect)
{
    WriteToDebug("In ConvertDecimalRangeToDecimalBasedOnLearnerResponse strValue=" + strValue + ",strLearnerResponse=" + strLearnerResponse + ",blnCorrect=" + blnCorrect);

    var REG_EX_REAL_DELIMITER = "[:]",
        aryNumericRangeSplit,
        strMin,
        strMax;

    //If it's indicated the learner was correct then just return their answer.
    if (blnCorrect) {
        WriteToDebug("Returning strLearnerResponse");
        return strLearnerResponse;
    }

    strValue = new String(strValue);
    aryNumericRangeSplit = strValue.split(REG_EX_REAL_DELIMITER);

    if (aryNumericRangeSplit.length === 2) {
        strMin = Trim(aryNumericRangeSplit[0]);
        strMax = Trim(aryNumericRangeSplit[1]);

        //if min is present
        if (strMin.length > 0) {
            WriteToDebug("Returning strMin");
            return strMin;
        }
        //if max is present
        else if (strMax.length > 0) {
            WriteToDebug("Returning strMax");
            return strMax;
        }
    }

    //[:] + incorrect (plus if aryNumberRangeSplit doesn't contain 2 parts,
    //which it never should if range is checked for validity prior to this function)
    WriteToDebug("Returning null");
    return null;
}


function IsValidDecimal(strValue){

    WriteToDebug("In IsValidDecimal, strValue=" + strValue);

    strValue = new String(strValue);

    //check for characters "0-9", ".", and "-" only
    if (strValue.search(/[^.\d-]/) > -1){
        WriteToDebug("Returning False - character other than a digit, dash or period found");
        return false;
    }

    //if contains a dash, ensure it is first and that there is only 1
    if (strValue.search("-") > -1){
        if (strValue.indexOf("-", 1) > -1){
            WriteToDebug("Returning False - dash found in the middle of the string");
            return false;
        }
    }

    //ensure only 1 decimal point
    if (strValue.indexOf(".") != strValue.lastIndexOf(".")){
        WriteToDebug("Returning False - more than one decimal point found");
        return false;
    }

    //ensure there is at least 1 digit
    if (strValue.search(/\d/) < 0){
        WriteToDebug("Returning False - no digits found");
        return false;
    }

    WriteToDebug("Returning True");
    return true;

}


function IsAlphaNumeric(strValue){
    WriteToDebug("In IsAlphaNumeric");
    if (strValue.search(/\w/) < 0){
        WriteToDebug("Returning false");
        return false;
    }
    else{
        WriteToDebug("Returning true");
        return true;
    }
}


function ReverseNameSequence(strName)
{
    var strFirstName;
    var strLastName;
    var intCommaLoc;


    //debug
    if (strName=="") strName="Not Found, Learner Name";

    intCommaLoc = strName.indexOf(",");
    strFirstName = strName.slice(intCommaLoc+1);
    strLastName = strName.slice(0, intCommaLoc);

    strFirstName = Trim(strFirstName);
    strLastName = Trim(strLastName);



    return strFirstName + ' ' + strLastName;
}


function LTrim(str) {                   // remove leading spaces
    str = new String(str);
    return(str.replace(/^\s+/, ''));

}

function RTrim(str) {                   // remove trailing spaces
    str = new String(str);
    return(str.replace(/\s+$/, ''));
}

function Trim(strToTrim) {
    var str = LTrim(RTrim(strToTrim)); // remove leading and trailing spaces
  return (str.replace(/\s{2,}/g," ")); // replace multiple spaces with single spaces
}




function GetValueFromDataChunk(strID)
{
    var strChunk = new String(GetDataChunk());
    var aryPairs = new Array();
    var aryValues = new Array();
    var i;

    aryPairs = strChunk.split(parent.DATA_CHUNK_PAIR_SEPARATOR);

    for (i=0;i<aryPairs.length;i++)
    {
        aryValues = aryPairs[i].split(parent.DATA_CHUNK_VALUE_SEPARATOR);
        if (aryValues[0]==strID) return aryValues[1];
    }

    return '';
}


function SetDataChunkValue(strID, strValue)
{
    var strChunk = new String(GetDataChunk());
    var aryPairs = new Array();
    var aryValues = new Array();
    var i;
    var blnFound = new Boolean(false);



    aryPairs = strChunk.split(parent.DATA_CHUNK_PAIR_SEPARATOR);

    for (i=0;i<aryPairs.length;i++)
    {
        aryValues = aryPairs[i].split(parent.DATA_CHUNK_VALUE_SEPARATOR);
        if (aryValues[0]==strID)
        {
            aryValues[1] = strValue;
            blnFound = true;
            aryPairs[i] = aryValues[0] + parent.DATA_CHUNK_VALUE_SEPARATOR + aryValues[1];
        }
    }


    if (blnFound==true)
    {
        strChunk = aryPairs.join(parent.DATA_CHUNK_PAIR_SEPARATOR);
    }
    else
    {
        if (strChunk=='')
        {
            //if data chunk is empty, just add this value
            strChunk = strID + parent.DATA_CHUNK_VALUE_SEPARATOR + strValue;
        }
        else
        {
            //otherwise, append
            strChunk += parent.DATA_CHUNK_PAIR_SEPARATOR + strID + parent.DATA_CHUNK_VALUE_SEPARATOR + strValue;
        }
    }

    SetDataChunk(strChunk);

    return true;
}


function GetLastDirAndPageName(str)
{
    var page = new String(str);
    var LastSlashLocation = page.lastIndexOf("/");
    var SecondLastSlashLocation = page.lastIndexOf("/", LastSlashLocation-1);
    return page.substr(SecondLastSlashLocation+1);

}


function RoundToPrecision(number, significantDigits){

    number = parseFloat(number);
    return (Math.round(number * Math.pow(10, significantDigits)) / Math.pow(10, significantDigits))
}

function IsAbsoluteUrl(urlStr) {
    return urlStr != null && (urlStr.indexOf("http://") == 0 || urlStr.indexOf("https://") == 0)
}

function TouchCloud() {
    if(APPID != null && APPID != "" && APPID != "__APPID__" && CLOUDURL !== null && CLOUDURL.indexOf("http") === 0){
        var cloudForm=document.createElement("form");
		cloudForm.name = "cloudform";
		cloudForm.id = "cloudform";
		cloudForm.style = "display:none;";

		document.body.appendChild(cloudForm);

        var elAppId = document.createElement("input");
        elAppId.name = "appId";
        elAppId.value = APPID;
        elAppId.type = "hidden";
        cloudForm.appendChild(elAppId);

        var elUrl = document.createElement("input");
        elUrl.name = "servingUrl";
        elUrl.type = "hidden";
        elUrl.value = document.location.href;
        cloudForm.appendChild(elUrl);

        var elVersion = document.createElement("input");
        elVersion.name = "version";
        elVersion.type = "hidden";
        elVersion.value = VERSION;
        cloudForm.appendChild(elVersion);

        cloudForm.target = "rusticisoftware_aicc_results";      //keep this name fairly unique to avoid a potential naming conflict with LMS frames
        cloudForm.action = CLOUDURL;
        document.getElementById('cloudform').submit();
		return true;

    }else{
        return false;
    }
}

function IsNumeric(n) {
	return !isNaN(parseFloat(n)) && isFinite(n);
}

function loadScript(url, callback) {
    // add the script tag to the head
    var head = document.getElementsByTagName('head')[0],
        script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = url;

    //
    // this is a beast of browser compatibility woes, so some of this
    // is gleaned from looking at the source for YUI's Y.Get.Transaction
    // and based on IE < 9 not handling .onload as seen here:
    //
    // http://msdn.microsoft.com/en-us/library/ie/hh180173(v=vs.85).aspx
    //
    // and based on this table:
    //
    // http://pieisgood.org/test/script-link-events/
    //
    // which was generated when creating Y.Get for YUI and linked from MDN here:
    //
    // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#See_also
    //
    if (! script.addEventListener || (document.documentMode && document.documentMode < 9)) {
        script.onreadystatechange = function () {
            if (/loaded|complete/.test(script.readyState)) {
                script.onreadystatechange = null;
                callback();
            }
        };
    }
    else {
        script.addEventListener("load", callback, false);
    }

    // fire the loading
    head.appendChild(script);
}
var STANDARD = 'SCORM2004';

var SCORM2004_LOGOUT = "logout";
var SCORM2004_SUSPEND = "suspend";
var SCORM2004_NORMAL_EXIT = "normal";
var SCORM2004_TIMEOUT = "time-out";

var SCORM2004_PASSED = "passed";
var SCORM2004_FAILED = "failed";
var SCORM2004_UNKNOWN = "unknown";

var SCORM2004_COMPLETED = "completed";
var SCORM2004_INCOMPLETE = "incomplete";
var SCORM2004_NOT_ATTEMPTED = "not attempted";

//var SCORM2004_BROWSED = "browsed";

var SCORM2004_CREDIT = "credit";
var SCORM2004_NO_CREDIT = "no-credit";

var SCORM2004_BROWSE = "browse";
var SCORM2004_NORMAL = "normal";
var SCORM2004_REVIEW = "review";

var SCORM2004_ENTRY_ABINITIO = "ab-initio";
var SCORM2004_ENTRY_RESUME = "resume";
var SCORM2004_ENTRY_NORMAL = "";

var SCORM2004_TLA_EXIT_MESSAGE = "exit,message";
var SCORM2004_TLA_EXIT_NO_MESSAGE = "exit,no message";
var SCORM2004_TLA_CONTINUE_MESSAGE = "continue,message";
var SCORM2004_TLA_CONTINUE_NO_MESSAGE = "continue,no message";


var SCORM2004_RESULT_CORRECT = "correct";
var SCORM2004_RESULT_WRONG = "incorrect";
var SCORM2004_RESULT_UNANTICIPATED = "unanticipated";
var SCORM2004_RESULT_NEUTRAL = "neutral";

var SCORM2004_INTERACTION_TYPE_TRUE_FALSE = "true-false";
var SCORM2004_INTERACTION_TYPE_CHOICE = "choice";
var SCORM2004_INTERACTION_TYPE_FILL_IN = "fill-in";
var SCORM2004_INTERACTION_TYPE_LONG_FILL_IN = "long-fill-in";
var SCORM2004_INTERACTION_TYPE_MATCHING = "matching";
var SCORM2004_INTERACTION_TYPE_PERFORMANCE = "performance";
var SCORM2004_INTERACTION_TYPE_SEQUENCING = "sequencing";
var SCORM2004_INTERACTION_TYPE_LIKERT = "likert";
var SCORM2004_INTERACTION_TYPE_NUMERIC = "numeric";


var SCORM2004_NO_ERROR = "0";
var SCORM2004_ERROR_INVALID_PREFERENCE = "-1";
var SCORM2004_ERROR_INVALID_STATUS = "-2";
var SCORM2004_ERROR_INVALID_SPEED = "-3";
var SCORM2004_ERROR_INVALID_TIMESPAN = "-4";
var SCORM2004_ERROR_INVALID_TIME_LIMIT_ACTION = "-5";
var SCORM2004_ERROR_INVALID_DECIMAL = "-6";
var SCORM2004_ERROR_INVALID_CREDIT = "-7";
var SCORM2004_ERROR_INVALID_LESSON_MODE = "-8";
var SCORM2004_ERROR_INVALID_ENTRY = "-9";

var SCORM2004_TRUE = "true";
var SCORM2004_FALSE = "false";

var SCORM2004_EARLIEST_DATE = new Date("1/1/1900");




var intSCORM2004Error = SCORM2004_NO_ERROR;
var strSCORM2004ErrorString = "";
var strSCORM2004ErrorDiagnostic = "";

var SCORM2004_objAPI = null;

var blnReviewModeSoReadOnly = false;

var blnSCORM2004_SSP_Is_Supported = null;

function SCORM2004_Initialize(){

    WriteToDebug("In SCORM2004_Initialize");

    var blnResult = true;

    SCORM2004_ClearErrorInfo();

    WriteToDebug("Grabbing API");

    try{
        SCORM2004_objAPI = SCORM2004_GrabAPI();
    }
    catch (e){
        WriteToDebug("Error grabbing 1.2 API-" + e.name + ":" + e.message);
    }

    if (typeof(SCORM2004_objAPI) == "undefined" || SCORM2004_objAPI == null){
        WriteToDebug("Unable to acquire SCORM API:")
        WriteToDebug("SCORM2004_objAPI=" + typeof(SCORM2004_objAPI));

        InitializeExecuted(false, "Error - unable to acquire LMS API, content may not play properly and results may not be recorded.  Please contact technical support.");
        return false;
    }

    WriteToDebug("Calling LMSInit");

    blnResult = SCORM2004_CallInitialize();

    if (! blnResult){
        WriteToDebug("ERROR Initializing LMS");

        InitializeExecuted(false, "Error initializing communications with LMS");

        return false;
    }



    if (! PREVENT_STATUS_CHANGE_DURING_INIT) {
        //only set the status to incomplete if it's not attempted yet
        if (SCORM2004_GetStatus() == LESSON_STATUS_NOT_ATTEMPTED){
            WriteToDebug("Setting Status to Incomplete");
            blnResult = SCORM2004_CallSetValue("cmi.completion_status", SCORM2004_INCOMPLETE);
        }
    }


    //we want to set the exit type to suspend immediately because some LMS's only store data if they get a suspend request
    blnResult = SCORM2004_CallSetValue("cmi.exit", SCORM2004_TranslateExitTypeToSCORM(DEFAULT_EXIT_TYPE)) && blnResult;

    //if mode is review, check if we should go to read only mode
    if (SCORM2004_GetLessonMode() == MODE_REVIEW){
        if (!(typeof(REVIEW_MODE_IS_READ_ONLY) == "undefined") && REVIEW_MODE_IS_READ_ONLY === true){
            blnReviewModeSoReadOnly = true;
        }
    }

    WriteToDebug("Calling InitializeExecuted with parameter-" + blnResult);

    InitializeExecuted(blnResult, "");

    return;

}




function SCORM2004_Finish(strExitType, blnStatusWasSet){

    WriteToDebug("In SCORM2004_Finish strExitType=" + strExitType + ", blnStatusWasSet=" + blnStatusWasSet);

    var strStatusAfterCompletion;
    var blnResult = true;

    SCORM2004_ClearErrorInfo();

    if ( (strExitType == EXIT_TYPE_FINISH) && ! blnStatusWasSet ){

        WriteToDebug("Getting completion status");

        strStatusAfterCompletion = SCORM2004_GetCompletionStatus();

        WriteToDebug("Setting completion status to " + strStatusAfterCompletion);

        blnResult = SCORM2004_CallSetValue("cmi.completion_status", strStatusAfterCompletion) && blnResult;

    }

    if( strExitType == EXIT_TYPE_SUSPEND && USE_2004_SUSPENDALL_NAVREQ ) {
        WriteToDebug("Setting adl.nav.request to suspendAll");
        blnResult = SCORM2004_CallSetValue("adl.nav.request","suspendAll");
    }

    WriteToDebug("Setting Exit");

    blnResult = SCORM2004_CallSetValue("cmi.exit", SCORM2004_TranslateExitTypeToSCORM(strExitType)) && blnResult;

    WriteToDebug("Calling Commit");

    blnResult = SCORM2004_CallCommit() && blnResult;

    WriteToDebug("Calling Finish");

    blnResult = SCORM2004_CallTerminate() && blnResult;

    WriteToDebug("Returning " + blnResult);

    return blnResult;

}


function SCORM2004_CommitData(){

    WriteToDebug("In SCORM2004_CommitData");
    SCORM2004_ClearErrorInfo();
    return SCORM2004_CallCommit();

}



//---------------------------------------------------------------------------------
//General Get and Set Values

function SCORM2004_GetStudentID(){

    WriteToDebug("In SCORM2004_GetStudentID");
    SCORM2004_ClearErrorInfo();
    return SCORM2004_CallGetValue("cmi.learner_id");

}

function SCORM2004_GetStudentName(){

    WriteToDebug("In SCORM2004_GetStudentName");
    SCORM2004_ClearErrorInfo();
    return SCORM2004_CallGetValue("cmi.learner_name");

}

function SCORM2004_GetBookmark(){
    WriteToDebug("In SCORM2004_GetBookmark");
    SCORM2004_ClearErrorInfo();
    return SCORM2004_CallGetValue("cmi.location");

}

function SCORM2004_SetBookmark(strBookmark){
    WriteToDebug("In SCORM2004_SetBookmark strBookmark=" + strBookmark);
    SCORM2004_ClearErrorInfo();
    return SCORM2004_CallSetValue("cmi.location", strBookmark);
}

function SCORM2004_GetDataChunk(){
    WriteToDebug("In SCORM2004_GetDataChunk");
    SCORM2004_ClearErrorInfo();
    return SCORM2004_CallGetValue("cmi.suspend_data");
}

function SCORM2004_SetDataChunk(strData){
    // 4000 characters for 2nd edition
    // 64000 characters here for 3rd edition
    WriteToDebug("In SCORM2004_SetDataChunk");
    SCORM2004_ClearErrorInfo();

    if(USE_STRICT_SUSPEND_DATA_LIMITS==true) {
        if(strData.length > 4000) {
            WriteToDebug("SCORM2004_SetDataChunk - suspend_data too large for SCORM 2004 2nd ed (4000 character limit) but will try to persist anyway.");
            if(strData.length > 64000) {
                WriteToDebug("SCORM2004_SetDataChunk - suspend_data too large for SCORM 2004 3rd & 4th ed (64000 character limit) so failing to persist.");
                return false;
            }else{
                //value is between 4000 and 64000 so we can attempt to persist
                return SCORM2004_CallSetValue("cmi.suspend_data", strData);
            }
        }else{
            //length is less than 4000 so it should be fine for all standards
            return SCORM2004_CallSetValue("cmi.suspend_data", strData);
        }
    }else{
        //don't check any lengths and just send it to the API
        return SCORM2004_CallSetValue("cmi.suspend_data", strData);
    }

}

function SCORM2004_GetLaunchData(){
    WriteToDebug("In SCORM2004_GetLaunchData");
    SCORM2004_ClearErrorInfo();
    return SCORM2004_CallGetValue("cmi.launch_data");
}

function SCORM2004_GetComments(){

    WriteToDebug("In SCORM2004_GetComments");

    SCORM2004_ClearErrorInfo();

    var intCommentCount;
    var strComments = "";

    intCommentCount = SCORM2004_CallGetValue("cmi.comments_from_learner._count");

    for (var i=0; i < intCommentCount; i++){

        if (strComments.length > 0){
            strComments += " | ";
        }

        strComments += SCORM2004_CallGetValue("cmi.comments_from_learner." + i + ".comment");
    }

    return strComments;

}

function SCORM2004_WriteComment(strComment){

    WriteToDebug("In SCORM2004_WriteComment strComment=" + strComment);

    var intCurrentIndex;
    var blnResult;

    SCORM2004_ClearErrorInfo();

    //remove the "|" since we can use the comments collection
    if (strComment.search(/ \| /) == 0){
        strComment = strComment.substr(3);
    }

    //remove encoding of "|"
    strComment.replace(/\|\|/g, "|")

    intCurrentIndex = SCORM2004_CallGetValue("cmi.comments_from_learner._count");

    blnResult = SCORM2004_CallSetValue("cmi.comments_from_learner." + intCurrentIndex + ".comment", strComment);
    blnResult = SCORM2004_CallSetValue("cmi.comments_from_learner." + intCurrentIndex + ".timestamp", ConvertDateToIso8601TimeStamp(new Date())) && blnResult;

    return blnResult;
}


function SCORM2004_GetLMSComments(){

    WriteToDebug("In SCORM2004_GetLMSComments");

    SCORM2004_ClearErrorInfo();

    var intCommentCount;
    var strComments = "";

    intCommentCount = SCORM2004_CallGetValue("cmi.comments_from_lms._count");

    for (var i=0; i < intCommentCount; i++){

        if (strComments.length > 0){
            strComments += " \r\n";
        }

        strComments += SCORM2004_CallGetValue("cmi.comments_from_lms." + i + ".comment");
    }

    return strComments;
}


function SCORM2004_GetAudioPlayPreference(){

    var intTempPreference;

    WriteToDebug("In SCORM2004_GetAudioPlayPreference");

    SCORM2004_ClearErrorInfo();

    intTempPreference = SCORM2004_CallGetValue("cmi.learner_preference.audio_level");

    if (intTempPreference == ""){
        intTempPreference = 0;
    }

    intTempPreference = parseInt(intTempPreference, 10);

    WriteToDebug("intTempPreference=" + intTempPreference);

    if (intTempPreference > 0){
        WriteToDebug("Returning On");
        return PREFERENCE_ON;
    }
    else if (intTempPreference <= 0) {
        WriteToDebug("Returning Off");
        return PREFERENCE_OFF;
    }
    else{
        WriteToDebug("Error: Invalid preference");
        SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_PREFERENCE,
                                  "Invalid audio preference received from LMS",
                                  "intTempPreference=" + intTempPreference);
        return null;
    }
}



function SCORM2004_GetAudioVolumePreference(){

    var intTempPreference;

    WriteToDebug("In SCORM2004_GetAudioVollumePreference");

    SCORM2004_ClearErrorInfo();

    intTempPreference = SCORM2004_CallGetValue("cmi.learner_preference.audio_level");

    WriteToDebug("intTempPreference=" + intTempPreference);

    if (intTempPreference == "") {
        intTempPreference = 100;
    }

    intTempPreference = parseInt(intTempPreference, 10);

    if (intTempPreference <= 0){
        WriteToDebug("Setting to 100");
        intTempPreference = 100;
    }

    if (! (intTempPreference > 0 && intTempPreference <= 100)){
        WriteToDebug("ERROR: invalid preference");
        SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_PREFERENCE,
                                  "Invalid audio preference received from LMS",
                                  "intTempPreference=" + intTempPreference);
        return null;
    }

    WriteToDebug("Returning " + intTempPreference);
    return intTempPreference;

}

function SCORM2004_SetAudioPreference(PlayPreference, intPercentOfMaxVolume){

    WriteToDebug("In SCORM2004_SetAudioPreference PlayPreference=" + PlayPreference + ", intPercentOfMaxVolume=" + intPercentOfMaxVolume);

    SCORM2004_ClearErrorInfo();

    if (PlayPreference == PREFERENCE_OFF){
        WriteToDebug("Setting percent to 0");
        intPercentOfMaxVolume = 0;
    }

    return SCORM2004_CallSetValue("cmi.learner_preference.audio_level", intPercentOfMaxVolume);
}



function SCORM2004_SetLanguagePreference(strLanguage){
    WriteToDebug("In SCORM2004_SetLanguagePreference strLanguage=" + strLanguage);
    SCORM2004_ClearErrorInfo();
    return SCORM2004_CallSetValue("cmi.learner_preference.language", strLanguage);

}


function SCORM2004_GetLanguagePreference(){
    WriteToDebug("In SCORM2004_GetLanguagePreference");
    SCORM2004_ClearErrorInfo();
    return SCORM2004_CallGetValue("cmi.learner_preference.language");

}


function SCORM2004_SetSpeedPreference(intPercentOfMax){

    //SCORM 2004's scale is greater than 0, our range is 1 to 100, just store our range

    WriteToDebug("In SCORM2004_SetSpeedPreference intPercentOfMax=" + intPercentOfMax);

    SCORM2004_ClearErrorInfo();

    return SCORM2004_CallSetValue("cmi.learner_preference.delivery_speed", intPercentOfMax);

}


function SCORM2004_GetSpeedPreference(){

    var intSCORMSpeed;
    var intPercentOfMax;

    WriteToDebug("In SCORM2004_GetSpeedPreference");

    SCORM2004_ClearErrorInfo();

    intSCORMSpeed = SCORM2004_CallGetValue("cmi.learner_preference.delivery_speed");

    WriteToDebug("intSCORMSpeed=" + intSCORMSpeed);

    if (intSCORMSpeed == ""){
        WriteToDebug("Detected empty string, defaulting to 100");
        intSCORMSpeed = 100;
    }

    intSCORMSpeed = parseInt(intSCORMSpeed, 10);

    if (intSCORMSpeed < 0){
        WriteToDebug("ERROR - out of range");
        SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_SPEED,
                                  "Invalid speed preference received from LMS - out of range",
                                  "intSCORMSpeed=" + intSCORMSpeed);
        return null;
    }

    WriteToDebug("intSCORMSpeed " + intSCORMSpeed);

    return intSCORMSpeed;

}




function SCORM2004_SetTextPreference(intPreference){

    WriteToDebug("In SCORM2004_SetTextPreference intPreference=" + intPreference);

    SCORM2004_ClearErrorInfo();

    return SCORM2004_CallSetValue("cmi.learner_preference.audio_captioning", intPreference);

}


function SCORM2004_GetTextPreference(){

    var intTempPreference;

    WriteToDebug("In SCORM2004_GetTextPreference");

    SCORM2004_ClearErrorInfo();

    intTempPreference = SCORM2004_CallGetValue("cmi.learner_preference.audio_captioning");
    intTempPreference = parseInt(intTempPreference, 10);

    WriteToDebug("intTempPreference=" + intTempPreference);

    if (intTempPreference > 0){
        WriteToDebug("Returning On");
        return PREFERENCE_ON;
    }
    else if (intTempPreference == 0 || intTempPreference == ""){
        WriteToDebug("Returning Default");
        return PREFERENCE_DEFAULT;
    }
    else if (intTempPreference < 0) {
        WriteToDebug("Returning Off");
        return PREFERENCE_OFF;
    }
    else{
        WriteToDebug("Error: Invalid preference");
        SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_PREFERENCE,
                                  "Invalid text preference received from LMS",
                                  "intTempPreference=" + intTempPreference);
        return null;
    }
}



//---------------------------------------------------------------------------------
//Time Management Functions
function SCORM2004_GetPreviouslyAccumulatedTime(){

    var strIso8601Time;
    var intMilliseconds;

    WriteToDebug("In SCORM2004_GetPreviouslyAccumulatedTime");


    SCORM2004_ClearErrorInfo();

    strIso8601Time = SCORM2004_CallGetValue("cmi.total_time")

    WriteToDebug("strIso8601Time=" + strIso8601Time);

    if (! IsValidIso8601TimeSpan(strIso8601Time)){
        WriteToDebug("ERROR - Invalid Iso8601Time");
        SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_TIMESPAN,
                                    "Invalid timespan received from LMS",
                                    "strTime=" + strIso8601Time);
        return null;
    }

    intMilliseconds = ConvertScorm2004TimeToMS(strIso8601Time);

    WriteToDebug("Returning " + intMilliseconds);

    return intMilliseconds;

}


function SCORM2004_SaveTime(intMilliSeconds){

    var strISO8601Time;

    WriteToDebug("In SCORM2004_SaveTime intMilliSeconds=" + intMilliSeconds);

    SCORM2004_ClearErrorInfo();

    strISO8601Time = ConvertMilliSecondsIntoSCORM2004Time(intMilliSeconds);

    WriteToDebug("strISO8601Time=" + strISO8601Time);

    return SCORM2004_CallSetValue("cmi.session_time", strISO8601Time);
}

function SCORM2004_GetMaxTimeAllowed(){

    var strIso8601Time;
    var intMilliseconds;

    WriteToDebug("In SCORM2004_GetMaxTimeAllowed");


    SCORM2004_ClearErrorInfo();

    strIso8601Time = SCORM2004_CallGetValue("cmi.max_time_allowed")

    WriteToDebug("strIso8601Time=" + strIso8601Time);

    if (strIso8601Time == ""){
        strIso8601Time = "20Y";
    }

    if (! IsValidIso8601TimeSpan(strIso8601Time)){
        WriteToDebug("ERROR - Invalid Iso8601Time");
        SCORM2004_SetErrorInfoManually(SCORM_ERROR_INVALID_TIMESPAN,
                                    "Invalid timespan received from LMS",
                                    "strIso8601Time=" + strIso8601Time);
        return null;
    }

    intMilliseconds = ConvertScorm2004TimeToMS(ConvertScorm2004TimeToMS);

    WriteToDebug("intMilliseconds=" + intMilliseconds);

    return intMilliseconds;

}



function SCORM2004_DisplayMessageOnTimeout(){

    var strTLA;
    WriteToDebug("In SCORM2004_DisplayMessageOnTimeout");

    SCORM2004_ClearErrorInfo();


    strTLA = SCORM2004_CallGetValue("cmi.time_limit_action");

    WriteToDebug("strTLA=" + strTLA);

    if (strTLA == SCORM2004_TLA_EXIT_MESSAGE || strTLA == SCORM2004_TLA_CONTINUE_MESSAGE){
        WriteToDebug("returning true");
        return true;
    }
    else if(strTLA == SCORM2004_TLA_EXIT_NO_MESSAGE || strTLA == SCORM2004_TLA_CONTINUE_NO_MESSAGE || strTLA == ""){
        WriteToDebug("returning false");
        return false;
    }
    else{
        WriteToDebug("Error invalid TLA");
        SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_TIME_LIMIT_ACTION,
                                  "Invalid time limit action received from LMS",
                                  "strTLA=" + strTLA);

        return null;
    }

}

function SCORM2004_ExitOnTimeout(){

    var strTLA;

    WriteToDebug("In SCORM2004_ExitOnTimeout");

    SCORM2004_ClearErrorInfo();

    strTLA = SCORM2004_CallGetValue("cmi.time_limit_action");

    WriteToDebug("strTLA=" + strTLA);

    if (strTLA == SCORM2004_TLA_EXIT_MESSAGE || strTLA == SCORM2004_TLA_EXIT_NO_MESSAGE){
        WriteToDebug("returning true");
        return true;
    }
    else if(strTLA == SCORM2004_TLA_CONTINUE_MESSAGE || strTLA == SCORM2004_TLA_CONTINUE_NO_MESSAGE || strTLA == ""){
        WriteToDebug("returning false");
        return false;
    }
    else{
        WriteToDebug("ERROR invalid TLA");
        SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_TIME_LIMIT_ACTION,
                                  "Invalid time limit action received from LMS",
                                  "strTLA=" + strTLA);

        return null;
    }

}


function SCORM2004_GetPassingScore(){
    var fltScore;

    WriteToDebug("In SCORM2004_GetPassingScore");

    SCORM2004_ClearErrorInfo();

    fltScore = SCORM2004_CallGetValue("cmi.scaled_passing_score")

    WriteToDebug("fltScore=" + fltScore);

    if (fltScore == ""){
        fltScore = 0;
    }

    if ( ! IsValidDecimal(fltScore)){
        WriteToDebug("Error - score is not a valid decimal");
        SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_DECIMAL,
                                  "Invalid mastery score received from LMS",
                                  "fltScore=" + fltScore);
        return null;
    }

    fltScore = parseFloat(fltScore);

    //multiply by 100 to normalize from -1 to 1 to 0-100 - (we always set the score so we know it won't be negative)
    fltScore = fltScore * 100;

    WriteToDebug("returning fltScore-" + fltScore);

    return fltScore;
}


function SCORM2004_SetScore(intScore, intMaxScore, intMinScore){

    var blnResult;
    var fltNormalizedScore;

    intScore = RoundToPrecision(intScore, 7);
    intMaxScore = RoundToPrecision(intMaxScore, 7);
    intMinScore = RoundToPrecision(intMinScore, 7);

    WriteToDebug("In SCORM2004_SetScore intScore=" + intScore + ", intMaxScore=" + intMaxScore + ", intMinScore=" + intMinScore);

    SCORM2004_ClearErrorInfo();

    fltNormalizedScore = RoundToPrecision(intScore / 100, 7);

    blnResult = SCORM2004_CallSetValue("cmi.score.raw", intScore);
    blnResult = SCORM2004_CallSetValue("cmi.score.max", intMaxScore) && blnResult;
    blnResult = SCORM2004_CallSetValue("cmi.score.min", intMinScore) && blnResult;

    blnResult = SCORM2004_CallSetValue("cmi.score.scaled", fltNormalizedScore) && blnResult;

    WriteToDebug("Returning " + blnResult);

    return blnResult;
}

function SCORM2004_GetScore(){


    WriteToDebug("In SCORM2004_GetScore");

    SCORM2004_ClearErrorInfo();

    return SCORM2004_CallGetValue("cmi.score.raw");

}

function SCORM2004_GetScaledScore(){


    WriteToDebug("In SCORM2004_GetScaledScore");

    SCORM2004_ClearErrorInfo();

    return SCORM2004_CallGetValue("cmi.score.scaled");

}

function SCORM2004_RecordInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime, SCORM2004InteractionType){

    var blnResult;
    var intInteractionIndex;
    var strResult;

    if (!IsNumeric(blnCorrect)) {
	    blnCorrect = new String(blnCorrect);  //have to cast as string to support false
	}

    SCORM2004_ClearErrorInfo();

    intInteractionIndex = SCORM2004_CallGetValue("cmi.interactions._count");

    WriteToDebug("intInteractionIndex=" + intInteractionIndex);

    if (intInteractionIndex == ""){
        WriteToDebug("Setting Interaction Index to 0");
        intInteractionIndex = 0;
    }

    //need to leave support for blnCorrect=t/f for legacy implementations of RSECA
    if (blnCorrect == true || blnCorrect =="true" || blnCorrect == INTERACTION_RESULT_CORRECT){
        strResult = SCORM2004_RESULT_CORRECT;
    }
    else if (String(blnCorrect) == "false" || blnCorrect == INTERACTION_RESULT_WRONG){      //compare against the string "false" because ("" == false) evaluates to true
        strResult = SCORM2004_RESULT_WRONG;
    }
    else if (blnCorrect == INTERACTION_RESULT_UNANTICIPATED){
        strResult = SCORM2004_RESULT_UNANTICIPATED;
    }
    else if (blnCorrect == INTERACTION_RESULT_NEUTRAL){
        strResult = SCORM2004_RESULT_NEUTRAL;
    }
    else if (IsNumeric(blnCorrect)) {
    	strResult = blnCorrect;
    }
    else{
        strResult = "";
    }

    WriteToDebug("strResult=" + strResult);

    strID = CreateValidIdentifier(strID);

    blnResult = SCORM2004_CallSetValue("cmi.interactions." + intInteractionIndex + ".id", strID);
    blnResult = SCORM2004_CallSetValue("cmi.interactions." + intInteractionIndex + ".type", SCORM2004InteractionType) && blnResult;

    if (strResponse !== null) {
        blnResult = SCORM2004_CallSetValue("cmi.interactions." + intInteractionIndex + ".learner_response", strResponse) && blnResult;
    }

    if (strResult != undefined && strResult != null && strResult != ""){
        blnResult = SCORM2004_CallSetValue("cmi.interactions." + intInteractionIndex + ".result", strResult) && blnResult;
    }

    if (strCorrectResponse != undefined && strCorrectResponse != null && strCorrectResponse != ""){
        blnResult = SCORM2004_CallSetValue("cmi.interactions." + intInteractionIndex + ".correct_responses.0.pattern", strCorrectResponse) && blnResult;
    }

    if (strDescription != undefined && strDescription != null && strDescription != ""){
        blnResult = SCORM2004_CallSetValue("cmi.interactions." + intInteractionIndex + ".description", strDescription) && blnResult;
    }

    if (intWeighting != undefined && intWeighting != null && intWeighting != ""){
        blnResult = SCORM2004_CallSetValue("cmi.interactions." + intInteractionIndex + ".weighting", intWeighting) && blnResult;
    }

    if (intLatency != undefined && intLatency != null && intLatency != ""){
        blnResult = SCORM2004_CallSetValue("cmi.interactions." + intInteractionIndex + ".latency", ConvertMilliSecondsIntoSCORM2004Time(intLatency)) && blnResult;
    }

    if (strLearningObjectiveID != undefined && strLearningObjectiveID != null && strLearningObjectiveID != ""){
        blnResult = SCORM2004_CallSetValue("cmi.interactions." + intInteractionIndex + ".objectives.0.id", strLearningObjectiveID) && blnResult;
    }

    blnResult = SCORM2004_CallSetValue("cmi.interactions." + intInteractionIndex + ".timestamp", ConvertDateToIso8601TimeStamp(dtmTime)) && blnResult;

    WriteToDebug("Returning " + blnResult);

    return blnResult;
}

function SCORM2004_RecordTrueFalseInteraction(strID, blnResponse, blnCorrect, blnCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In SCORM2004_RecordTrueFalseInteraction strID=" + strID + ", strResponse=" + strResponse +
                    ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);

    var strResponse = null;
    var strCorrectResponse = null;

    if (blnResponse){
        strResponse = "true";
    }
    else if (blnResponse !== null) {
        strResponse = "false";
    }

    if (blnCorrectResponse == true){
        strCorrectResponse = "true";
    }
    else if(blnCorrectResponse == false){       //test for false b/c it could be null in which case we want to leave it as null
        strCorrectResponse = "false";
    }

    return SCORM2004_RecordInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime, SCORM2004_INTERACTION_TYPE_TRUE_FALSE);
}


function SCORM2004_RecordMultipleChoiceInteraction(strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In SCORM2004_RecordMultipleChoiceInteraction strID=" + strID + ", aryResponse=" + aryResponse +
                    ", blnCorrect=" + blnCorrect + ", aryCorrectResponse=" + aryCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);

    var strResponse = null;
    var strCorrectResponse = "";

    if (aryResponse !== null) {
        strResponse = "";

        for (var i=0; i < aryResponse.length; i++){

            if (strResponse.length > 0) {strResponse += "[,]";}
            strResponse += aryResponse[i].Long;
        }
    }

    for (var i=0; i < aryCorrectResponse.length; i++){

        if (strCorrectResponse.length > 0) {strCorrectResponse += "[,]";}
        strCorrectResponse += aryCorrectResponse[i].Long;
    }

    return SCORM2004_RecordInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime, SCORM2004_INTERACTION_TYPE_CHOICE);
}


function SCORM2004_RecordFillInInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In SCORM2004_RecordFillInInteraction strID=" + strID + ", strResponse=" + strResponse +
                    ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);

    var interactionType;

    if (strCorrectResponse == null){
        strCorrectResponse = "";
    }

    strCorrectResponse = new String(strCorrectResponse);

    if (strCorrectResponse.length > 250 || strResponse.length > 250){
        interactionType = SCORM2004_INTERACTION_TYPE_LONG_FILL_IN;
    }
    else{
        interactionType = SCORM2004_INTERACTION_TYPE_FILL_IN;
    }

    if (strCorrectResponse.length > 4000){strCorrectResponse = strCorrectResponse.substr(0, 4000);}

    return SCORM2004_RecordInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime, interactionType);
}

function SCORM2004_RecordMatchingInteraction(strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In SCORM2004_RecordMatchingInteraction strID=" + strID + ", aryResponse=" + aryResponse +
                    ", blnCorrect=" + blnCorrect + ", aryCorrectResponse=" + aryCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);

    var strResponse = null;
    var strCorrectResponse = "";

    if (aryResponse !== null) {
        strResponse = "";

        for (var i=0; i < aryResponse.length; i++){

            if (strResponse.length > 0) {strResponse += "[,]";}
            strResponse += aryResponse[i].Source.Long + "[.]" + aryResponse[i].Target.Long;
        }
    }

    for (var i=0; i < aryCorrectResponse.length; i++){

        if (strCorrectResponse.length > 0) {strCorrectResponse += "[,]";}
        strCorrectResponse += aryCorrectResponse[i].Source.Long + "[.]" + aryCorrectResponse[i].Target.Long;
    }

    return SCORM2004_RecordInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime, SCORM2004_INTERACTION_TYPE_MATCHING);
}

function SCORM2004_RecordPerformanceInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In SCORM2004_RecordPerformanceInteraction strID=" + strID + ", strResponse=" + strResponse +
                    ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);

    if (strResponse !== null) {
        strResponse = new String(strResponse);
        if (strResponse.length > 250){strResponse = strResponse.substr(0, 250);}

        //we're only recording the step answer, not the step name
        strResponse = "[.]" + strResponse;
    }

    if (strCorrectResponse == null){
        strCorrectResponse = "";
    }

    strCorrectResponse = new String(strCorrectResponse);
    if (strCorrectResponse.length > 250){strCorrectResponse = strCorrectResponse.substr(0, 250);}

    //we're only recording the step answer, not the step name
    strCorrectResponse = "[.]" + strCorrectResponse;

    return SCORM2004_RecordInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime, SCORM2004_INTERACTION_TYPE_PERFORMANCE);
}

function SCORM2004_RecordSequencingInteraction(strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In SCORM2004_RecordSequencingInteraction strID=" + strID + ", aryResponse=" + aryResponse +
                    ", blnCorrect=" + blnCorrect + ", aryCorrectResponse=" + aryCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);

    var strResponse = null;
    var strCorrectResponse = "";

    if (aryResponse !== null) {
        strResponse = "";

        for (var i=0; i < aryResponse.length; i++){

            if (strResponse.length > 0) {strResponse += "[,]";}
            strResponse += aryResponse[i].Long;
        }
    }

    for (var i=0; i < aryCorrectResponse.length; i++){

        if (strCorrectResponse.length > 0) {strCorrectResponse += "[,]";}
        strCorrectResponse += aryCorrectResponse[i].Long;
    }

    return SCORM2004_RecordInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime, SCORM2004_INTERACTION_TYPE_SEQUENCING);
}


function SCORM2004_RecordLikertInteraction(strID, response, blnCorrect, correctResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In RecordLikertInteraction strID=" + strID + ", response=" + response +
                    ", blnCorrect=" + blnCorrect + ", correctResponse=" + correctResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);

    var strResponse = null;
    var strCorrectResponse = "";

    if (response !== null) {
        strResponse = response.Long;
    }
    if (correctResponse != null){
        strCorrectResponse = correctResponse.Long;
    }

    return SCORM2004_RecordInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime, SCORM2004_INTERACTION_TYPE_LIKERT);
}

function SCORM2004_RecordNumericInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In SCORM2004_RecordNumericInteraction strID=" + strID + ", strResponse=" + strResponse +
                    ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);


    if (strCorrectResponse != undefined && strCorrectResponse != null ) {
        //SCORM 2004 should use decimal ranges. If we receive a plain decimal we convert it to a range with the same
        //min and max values.
        if (IsValidDecimal(strCorrectResponse)) {
            strCorrectResponse = strCorrectResponse + "[:]" + strCorrectResponse;
            WriteToDebug("SCORM2004_RecordNumericInteraction received decimal correct response and converted to range, strCorrectResponse=" + strCorrectResponse);
        }

        //Check that the range is valid. This checks both correct responses converted from decimals above and also non-decimal correct responses received.
        if (!IsValidDecimalRange(strCorrectResponse)) {
            WriteToDebug("Returning False - SCORM2004_RecordNumericInteraction received invalid correct response decimal range, strCorrectResponse=" + strCorrectResponse);
            return false;
        }

        //Handle numeric range interaction edge case.....
        //If the correct response is [:] and it's been indicated that the answer was incorrect we fail here. We _could_ accept this in SCORM2004 but rejecting it means
        //the behavior matches SCORM 1.2 where we reject such an interaction because we are unable to determine what the decimal correct response is (SCORM 1.2 does not accept ranges).
        //The interaction doesn't actually make sense. By this point we've ensured the response is a decimal, the correct response is any decimal..... yet it's indicated as incorrect???
        //In order to ensure both standards remain in lockstep with regard to this we call the same function that SCORM 1.2 uses to convert from a range to a decimal and if null is
        //returned we error
        if(ConvertDecimalRangeToDecimalBasedOnLearnerResponse(strCorrectResponse, strResponse, blnCorrect) === null) {
            WriteToDebug("Returning False - SCORM2004_RecordNumericInteraction received invalid correct response decimal range, response and correct indicator, strCorrectResponse=" +
                strCorrectResponse + ", strResponse=" + strResponse + ", blnCorrect=" + blnCorrect);
            return false;
        }
    }

    return SCORM2004_RecordInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime, SCORM2004_INTERACTION_TYPE_NUMERIC);
}



function SCORM2004_GetEntryMode(){
    var strEntry;

    WriteToDebug("In SCORM2004_GetEntryMode");

    SCORM2004_ClearErrorInfo();

    strEntry = SCORM2004_CallGetValue("cmi.entry");

    WriteToDebug("strEntry=" + strEntry);

    if (strEntry == SCORM2004_ENTRY_ABINITIO){
        WriteToDebug("Returning first time");
        return ENTRY_FIRST_TIME;
    }
    else if (strEntry == SCORM2004_ENTRY_RESUME){
        WriteToDebug("Returning resume");
        return ENTRY_RESUME;
    }
    else if (strEntry == SCORM2004_ENTRY_NORMAL){
        WriteToDebug("returning normal");
        return ENTRY_REVIEW;
    }
    else{
        WriteToDebug("ERROR - invalid entry mode");
        SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_ENTRY,
                        "Invalid entry vocab received from LMS",
                        "strEntry=" + strEntry);
        return null;
    }

}

function SCORM2004_GetLessonMode(){

    var strLessonMode;

    WriteToDebug("In SCORM2004_GetLessonMode");

    SCORM2004_ClearErrorInfo();

    strLessonMode = SCORM2004_CallGetValue("cmi.mode");

    WriteToDebug("strLessonMode=" + strLessonMode);

    if (strLessonMode == SCORM2004_BROWSE){
        WriteToDebug("Returning browse");
        return MODE_BROWSE;
    }
    else if(strLessonMode == SCORM2004_NORMAL){
        WriteToDebug("returning normal");
        return MODE_NORMAL;
    }
    else if(strLessonMode == SCORM2004_REVIEW){
        WriteToDebug("Returning Review");
        return MODE_REVIEW;
    }
    else{
        WriteToDebug("ERROR - invalid lesson mode");
        SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_LESSON_MODE,
                                "Invalid lesson_mode vocab received from LMS",
                                "strLessonMode=" + strLessonMode);
        return null;
    }

}

function SCORM2004_GetTakingForCredit(){

    var strCredit;

    WriteToDebug("In SCORM2004_GetTakingForCredit");

    SCORM2004_ClearErrorInfo();

    strCredit = SCORM2004_CallGetValue("cmi.credit");

    WriteToDebug("strCredit=" + strCredit);

    if (strCredit == "credit"){
        WriteToDebug("Returning true");
        return true;
    }
    else if (strCredit == "no-credit"){
        WriteToDebug("Returning false");
        return false;
    }
    else{
        WriteToDebug("ERROR - invalid credit");
        SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_CREDIT,
                                  "Invalid credit vocab received from LMS",
                                  "strCredit=" + strCredit);
        return null;
    }
}



function SCORM2004_SetObjectiveScore(strObjectiveID, intScore, intMaxScore, intMinScore){

    var intObjectiveIndex;
    var blnResult;
    var fltNormalizedScore;

    intScore = RoundToPrecision(intScore, 7);
    intMaxScore = RoundToPrecision(intMaxScore, 7);
    intMinScore = RoundToPrecision(intMinScore, 7);

    WriteToDebug("In SCORM2004_SetObjectiveScore, strObejctiveID=" + strObjectiveID + ", intScore=" + intScore + ", intMaxScore=" + intMaxScore + ", intMinScore=" + intMinScore);

    SCORM2004_ClearErrorInfo();

    intObjectiveIndex = SCORM2004_FindObjectiveIndexFromID(strObjectiveID);

    WriteToDebug("intObjectiveIndex=" + intObjectiveIndex);

    fltNormalizedScore = RoundToPrecision(intScore / 100, 7);

    blnResult = SCORM2004_CallSetValue("cmi.objectives." + intObjectiveIndex + ".id", strObjectiveID);
    blnResult = SCORM2004_CallSetValue("cmi.objectives." + intObjectiveIndex + ".score.raw", intScore) && blnResult;
    blnResult = SCORM2004_CallSetValue("cmi.objectives." + intObjectiveIndex + ".score.max", intMaxScore) && blnResult;
    blnResult = SCORM2004_CallSetValue("cmi.objectives." + intObjectiveIndex + ".score.min", intMinScore) && blnResult;
    blnResult = SCORM2004_CallSetValue("cmi.objectives." + intObjectiveIndex + ".score.scaled", fltNormalizedScore) && blnResult;

    WriteToDebug("Returning " + blnResult);

    return blnResult;

}


function SCORM2004_SetObjectiveStatus(strObjectiveID, Lesson_Status){

    var intObjectiveIndex;
    var blnResult;
    var strSCORMSuccessStatus = "";
    var strSCORMCompletionStatus = "";

    WriteToDebug("In SCORM2004_SetObjectiveStatus strObjectiveID=" + strObjectiveID + ", Lesson_Status=" + Lesson_Status);

    SCORM2004_ClearErrorInfo();

    intObjectiveIndex = SCORM2004_FindObjectiveIndexFromID(strObjectiveID);

    WriteToDebug("intObjectiveIndex=" + intObjectiveIndex);

    if (Lesson_Status == LESSON_STATUS_PASSED){
        strSCORMSuccessStatus = SCORM2004_PASSED;
        strSCORMCompletionStatus = SCORM2004_COMPLETED;
    }
    else if (Lesson_Status == LESSON_STATUS_FAILED){
        strSCORMSuccessStatus = SCORM2004_FAILED;
        strSCORMCompletionStatus = SCORM2004_COMPLETED;
    }
    else if (Lesson_Status == LESSON_STATUS_COMPLETED){
        strSCORMSuccessStatus = SCORM2004_UNKNOWN;
        strSCORMCompletionStatus = SCORM2004_COMPLETED;
    }
    else if (Lesson_Status == LESSON_STATUS_BROWSED){
        strSCORMSuccessStatus = SCORM2004_UNKNOWN;
        strSCORMCompletionStatus = SCORM2004_COMPLETED;
    }
    else if (Lesson_Status == LESSON_STATUS_INCOMPLETE){
        strSCORMSuccessStatus = SCORM2004_UNKNOWN;
        strSCORMCompletionStatus = SCORM2004_INCOMPLETE;
    }
    else if (Lesson_Status == LESSON_STATUS_NOT_ATTEMPTED){
        strSCORMSuccessStatus = SCORM2004_UNKNOWN;
        strSCORMCompletionStatus = SCORM2004_NOT_ATTEMPTED;
    }

    WriteToDebug("strSCORMSuccessStatus=" + strSCORMSuccessStatus);
    WriteToDebug("strSCORMCompletionStatus=" + strSCORMCompletionStatus);

    blnResult = SCORM2004_CallSetValue("cmi.objectives." + intObjectiveIndex + ".id", strObjectiveID);

    blnResult = SCORM2004_CallSetValue("cmi.objectives." + intObjectiveIndex + ".success_status", strSCORMSuccessStatus) && blnResult;
    blnResult = SCORM2004_CallSetValue("cmi.objectives." + intObjectiveIndex + ".completion_status", strSCORMCompletionStatus) && blnResult;

    WriteToDebug("Returning " + blnResult);

    return blnResult;
}


function SCORM2004_SetObjectiveDescription(strObjectiveID, strObjectiveDescription){

    var intObjectiveIndex;

    WriteToDebug("In SCORM2004_SetObjectiveDescription strObjectiveID=" + strObjectiveID + ", strObjectiveDescription=" + strObjectiveDescription);

    SCORM2004_ClearErrorInfo();

    intObjectiveIndex = SCORM2004_FindObjectiveIndexFromID(strObjectiveID);

    WriteToDebug("intObjectiveIndex=" + intObjectiveIndex);



    blnResult = SCORM2004_CallSetValue("cmi.objectives." + intObjectiveIndex + ".id", strObjectiveID);
    blnResult = SCORM2004_CallSetValue("cmi.objectives." + intObjectiveIndex + ".description", strObjectiveDescription) && blnResult;

    WriteToDebug("Returning " + blnResult);

    return blnResult;
}


function SCORM2004_GetObjectiveScore(strObjectiveID){

    var intObjectiveIndex;

    WriteToDebug("In SCORM2004_GetObjectiveScore, strObejctiveID=" + strObjectiveID);

    SCORM2004_ClearErrorInfo();

    intObjectiveIndex = SCORM2004_FindObjectiveIndexFromID(strObjectiveID);

    WriteToDebug("intObjectiveIndex=" + intObjectiveIndex);

    return SCORM2004_CallGetValue("cmi.objectives." + intObjectiveIndex + ".score.raw");

}


function SCORM2004_GetObjectiveStatus(strObjectiveID){

    var intObjectiveIndex;
    var strSuccessStatus;
    var strCompletionStatus;

    WriteToDebug("In SCORM2004_GetObjectiveStatus, strObejctiveID=" + strObjectiveID);

    SCORM2004_ClearErrorInfo();

    intObjectiveIndex = SCORM2004_FindObjectiveIndexFromID(strObjectiveID);

    WriteToDebug("intObjectiveIndex=" + intObjectiveIndex);

    strSuccessStatus = SCORM2004_CallGetValue("cmi.objectives." + intObjectiveIndex + ".success_status");
    strCompletionStatus = SCORM2004_CallGetValue("cmi.objectives." + intObjectiveIndex + ".completion_status");

    if (strSuccessStatus == SCORM2004_PASSED){
        WriteToDebug("returning Passed");
        return LESSON_STATUS_PASSED;
    }
    else if (strSuccessStatus == SCORM2004_FAILED){
        WriteToDebug("Returning Failed");
        return LESSON_STATUS_FAILED;
    }
    else if (strCompletionStatus == SCORM2004_COMPLETED){
        WriteToDebug("Returning Completed");
        return LESSON_STATUS_COMPLETED;
    }
    else if (strCompletionStatus == SCORM2004_INCOMPLETE){
        WriteToDebug("Returning Incomplete");
        return LESSON_STATUS_INCOMPLETE;
    }
    else if (strCompletionStatus == SCORM2004_NOT_ATTEMPTED || strCompletionStatus == SCORM2004_UNKNOWN || strCompletionStatus == ""){
        WriteToDebug("Returning Not Attempted");
        return LESSON_STATUS_NOT_ATTEMPTED;
    }
    else{
        WriteToDebug("ERROR - status not found");
        SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_STATUS,
                                "Invalid objective status received from LMS or initial status not yet recorded for objective",
                                "strCompletionStatus=" + strCompletionStatus);
        return null;

    }

}

function SCORM2004_GetObjectiveProgressMeasure(strObjectiveID){

    var strProgressMeasure = SCORM2004_CallGetValue("cmi.objectives." + strObjectiveID + ".progress_measure");

    return strProgressMeasure;

}


function SCORM2004_GetObjectiveDescription(strObjectiveID){

    var intObjectiveIndex;
    var strSuccessStatus;
    var strCompletionStatus;

    WriteToDebug("In SCORM2004_GetObjectiveDescription, strObejctiveID=" + strObjectiveID);

    SCORM2004_ClearErrorInfo();

    intObjectiveIndex = SCORM2004_FindObjectiveIndexFromID(strObjectiveID);

    WriteToDebug("intObjectiveIndex=" + intObjectiveIndex);

    strDescription = SCORM2004_CallGetValue("cmi.objectives." + intObjectiveIndex + ".description");

    return strDescription;

}

function SCORM2004_FindObjectiveIndexFromID(strObjectiveID){

    var intCount;
    var i;
    var strTempID;

    WriteToDebug("In SCORM2004_FindObjectiveIndexFromID");

    intCount = SCORM2004_CallGetValue("cmi.objectives._count");

    if (intCount == ""){
        WriteToDebug("Setting intCount=0");
        return 0;
    }

    intCount = parseInt(intCount, 10);

    WriteToDebug("intCount=" + intCount);

    for (i=0; i<intCount; i++){

        WriteToDebug("Checking index " + i);

        strTempID = SCORM2004_CallGetValue("cmi.objectives." + i + ".id");

        WriteToDebug("ID=" + strTempID);

        if (strTempID == strObjectiveID){
            WriteToDebug("Found Matching index");
            return i;
        }
    }

    WriteToDebug("Did not find match, returning count");

    return intCount;
}

function SCORM2004_CreateValidIdentifier(str) {
    //
    // some customers don't like the identifiers being turned into URIs and don't
    // mind that this means that SCORM content may not be spec compliant, because
    // LMSs don't seem to validate (or reject) non-URI identifiers, so allow them
    // to use the process Driver used before the 6.x version where URIs became
    // properly handled
    //
    if (USE_LEGACY_IDENTIFIERS_FOR_2004) {
        return CreateValidIdentifierLegacy(str);
    }

    return CreateUriIdentifier(str);
}


//---------------------------------------------------------------------------------
//Status Management Functions

function SCORM2004_SetFailed(){

    WriteToDebug("In SCORM2004_SetFailed");

    var blnResult;
    var strCompletionStatus;

    SCORM2004_ClearErrorInfo();

    blnResult = SCORM2004_CallSetValue("cmi.success_status", SCORM2004_FAILED);
    if (PASS_FAIL_SETS_COMPLETION_FOR_2004) {
        blnResult = SCORM2004_CallSetValue("cmi.completion_status", SCORM2004_COMPLETED) && blnResult;
    } else {
        // If completion hasn't been set yet, then reset blnStatusWasSet so that SetReachedEnd can set it later.
        strCompletionStatus = SCORM2004_CallGetValue("cmi.completion_status");
        if (strCompletionStatus !== SCORM2004_COMPLETED) {
            WriteToDebug("Overriding blnStatusWasSet to false.");
            blnStatusWasSet = false;
        }
    }
    return blnResult;
}

function SCORM2004_SetPassed(){

    WriteToDebug("In SCORM2004_SetPassed");

    var blnResult;
    var strCompletionStatus;

    SCORM2004_ClearErrorInfo();

    blnResult = SCORM2004_CallSetValue("cmi.success_status", SCORM2004_PASSED);
    if (PASS_FAIL_SETS_COMPLETION_FOR_2004) {
        blnResult = SCORM2004_CallSetValue("cmi.completion_status", SCORM2004_COMPLETED) && blnResult;
    } else {
        // If completion hasn't been set yet, then reset blnStatusWasSet so that SetReachedEnd can set it later.
        strCompletionStatus = SCORM2004_CallGetValue("cmi.completion_status");
        if (strCompletionStatus !== SCORM2004_COMPLETED) {
            WriteToDebug("Overriding blnStatusWasSet to false.");
            blnStatusWasSet = false;
        }
    }
    return blnResult;
}

function  SCORM2004_SetCompleted(){

    WriteToDebug("In SCORM2004_SetCompleted");

    var blnResult;

    SCORM2004_ClearErrorInfo();

    blnResult = SCORM2004_CallSetValue("cmi.completion_status", SCORM2004_COMPLETED);

    return blnResult;
}

function SCORM2004_ResetStatus(){

    WriteToDebug("In SCORM2004_ResetStatus");

    var blnResult;

    SCORM2004_ClearErrorInfo();

    blnResult = SCORM2004_CallSetValue("cmi.success_status", SCORM2004_UNKNOWN);
    blnResult = SCORM2004_CallSetValue("cmi.completion_status", SCORM2004_INCOMPLETE) && blnResult;

    return blnResult;
}

function SCORM2004_GetStatus(){

    var strSuccessStatus;
    var strCompletionStatus;

    WriteToDebug("In SCORM2004_GetStatus");

    SCORM2004_ClearErrorInfo();

    strSuccessStatus = SCORM2004_CallGetValue("cmi.success_status");
    strCompletionStatus = SCORM2004_CallGetValue("cmi.completion_status");

    WriteToDebug("strSuccessStatus=" + strSuccessStatus);
    WriteToDebug("strCompletionStatus=" + strCompletionStatus);

    if (strSuccessStatus == SCORM2004_PASSED){
        WriteToDebug("returning Passed");
        return LESSON_STATUS_PASSED;
    }
    else if (strSuccessStatus == SCORM2004_FAILED){
        WriteToDebug("Returning Failed");
        return LESSON_STATUS_FAILED;
    }
    else if (strCompletionStatus == SCORM2004_COMPLETED){
        WriteToDebug("Returning Completed");
        return LESSON_STATUS_COMPLETED;
    }
    else if (strCompletionStatus == SCORM2004_INCOMPLETE){
        WriteToDebug("Returning Incomplete");
        return LESSON_STATUS_INCOMPLETE;
    }
    else if (strCompletionStatus == SCORM2004_NOT_ATTEMPTED || strCompletionStatus == SCORM2004_UNKNOWN){
        WriteToDebug("Returning Not Attempted");
        return LESSON_STATUS_NOT_ATTEMPTED;
    }
    else{
        WriteToDebug("ERROR - status not found");
        SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_STATUS,
                                "Invalid lesson status received from LMS",
                                "strCompletionStatus=" + strCompletionStatus);
        return null;

    }


}

//public
function SCORM2004_GetProgressMeasure(){
    WriteToDebug("In SCORM2004_GetProgressMeasure");

    var blnResult;

    SCORM2004_ClearErrorInfo();

    blnResult = SCORM2004_CallGetValue("cmi.progress_measure");

    return blnResult;
}
/*
    cmi.progress_measure cmi.completion_status
    0 ?not attempted?
    1 ?completed?
    0 > value < 1 ?incomplete? (typically, unless a cmi.completion_threshold is defined and the cmi.progress_measure is >= the cmi.completion_threshold)


    GetValue(?cmi.progress_measure?)
    SetValue(?cmi.progress_measure?,?0.75?)
    SetValue(?cmi.progress_measure?,?1.0?)

*/

//public
function SCORM2004_SetProgressMeasure(numMeasure){
    WriteToDebug("In SCORM2004_SetProgressMeasure");

    var blnResult;


    SCORM2004_ClearErrorInfo();

    blnResult = SCORM2004_CallSetValue("cmi.progress_measure", numMeasure);

    return blnResult;
}

function SCORM2004_SetObjectiveProgressMeasure(strObjectiveID, numMeasure){
    WriteToDebug("In SCORM2004_SetObjectiveProgressMeasure");
    var intObjectiveIndex;
    var blnResult;

    WriteToDebug("In SCORM2004_SetObjectiveProgressMeasure, strObejctiveID=" + strObjectiveID + ", numMeasure=" + numMeasure );

    SCORM2004_ClearErrorInfo();

    intObjectiveIndex = SCORM2004_FindObjectiveIndexFromID(strObjectiveID);

    WriteToDebug("intObjectiveIndex=" + intObjectiveIndex);

    SCORM2004_ClearErrorInfo();

    blnResult = SCORM2004_CallSetValue("cmi.objectives." + intObjectiveIndex + ".progress_measure", numMeasure);

    return blnResult;
}

function SCORM2004_IsContentInBrowseMode(){

    var strLessonMode

    WriteToDebug("In SCORM2004_IsContentInBrowseMode");

    strLessonMode = SCORM2004_CallGetValue("cmi.mode");

    WriteToDebug("SCORM2004_IsContentInBrowseMode,  strLessonMode=" + strLessonMode);

    if (strLessonMode == SCORM2004_BROWSE){
        WriteToDebug("Returning true");
        return true;
    }
    else{
        WriteToDebug("Returning false");
        return false;
    }
}


function SCORM2004_TranslateExitTypeToSCORM(strExitType){

    WriteToDebug("In SCORM2004_TranslatgeExitTypeToSCORM strExitType-" + strExitType);

    if (strExitType == EXIT_TYPE_SUSPEND){
        WriteToDebug("Returning suspend");
        return SCORM2004_SUSPEND;
    }
    else if (strExitType == EXIT_TYPE_UNLOAD){
        WriteToDebug("Returning Exit");
        return SCORM2004_NORMAL_EXIT;
    }
    else if (strExitType == EXIT_TYPE_FINISH){
        WriteToDebug("Returning Logout");
        return SCORM2004_NORMAL_EXIT;
    }
    else if (strExitType == EXIT_TYPE_TIMEOUT){
        WriteToDebug("Returning Timout");
        return SCORM2004_TIMEOUT;
    }
}


function SCORM2004_GetCompletionStatus(){

    WriteToDebug("In SCORM2004_GetCompletionStatus");

    return SCORM2004_COMPLETED;

}

function SCORM2004_SetPointBasedScore(intScore, intMaxScore, intMinScore){

    var blnResult;
    var fltCalculatedScore;

    WriteToDebug("In SCORM2004_SetPointBasedScore intScore=" + intScore + ", intMaxScore=" + intMaxScore + ", intMinScore=" + intMinScore);

    SCORM2004_ClearErrorInfo();

    if(intScore >= intMinScore)
    {
        fltCalculatedScore = intScore / intMaxScore;
    }else{
        WriteToDebug("intScore is lower than intMinScore. Overriding score with minscore for cmi.score.scaled");
        fltCalculatedScore = intMinScore / intMaxScore;
    }
    fltCalculatedScore = RoundToPrecision(fltCalculatedScore, 7);

    blnResult = SCORM2004_CallSetValue("cmi.score.raw", intScore);
    blnResult = SCORM2004_CallSetValue("cmi.score.max", intMaxScore) && blnResult;
    blnResult = SCORM2004_CallSetValue("cmi.score.min", intMinScore) && blnResult;

    blnResult = SCORM2004_CallSetValue("cmi.score.scaled", fltCalculatedScore) && blnResult;

    WriteToDebug("Returning " + blnResult);

    return blnResult;

}


//___________________________________________________________
//Interaction Retrieval Functionality
//NOTE ON INTERACTION RETRIEVAL
//A.  It is only available in certain standards, standards where it is unavailable will return nothing
//B.  The interaction records are currently reported using "journaling", whereby each entry is appended
//      Retrieval methods will retrieve only the most recent value


//___________________________________________________________
//Helper Methods
function SCORM2004_FindInteractionIndexFromID(strInteractionID){

    //with interactions, this method returns the index for the MOST RECENT (by timestamp)
    //interaction with this identifier

    var intCount;
    var i;
    var strTempID;
    var dtmTempDate = new Date();
    var index;
    var currentIndexTimestamp = new Date("1/1/1900");


    WriteToDebug("In SCORM2004_FindInteractionIndexFromID");

    intCount = SCORM2004_CallGetValue("cmi.interactions._count");

    if (intCount == ""){
        WriteToDebug("Setting intCount=0");
        return null;
    }

    intCount = parseInt(intCount, 10);

    WriteToDebug("intCount=" + intCount);

    for (i=0; i<intCount; i++){

        WriteToDebug("Checking index " + i);

        strTempID = SCORM2004_CallGetValue("cmi.interactions." + i + ".id");

        WriteToDebug("ID=" + strTempID);

        if (strTempID == strInteractionID){
            WriteToDebug("Found Matching index: " + i);
            //this is NOT a call to SCORM2004_GetInteractionTimestamp because that will use this method to loop
            dtmTempDate = ConvertIso8601TimeStampToDate(SCORM2004_CallGetValue("cmi.interactions." + i + ".timestamp"));

            WriteToDebug("timestamp for " + i + ": " + dtmTempDate);

            if (dtmTempDate>currentIndexTimestamp)
            {
                index = i;
                currentIndexTimestamp = dtmTempDate;
            }
        }
    }

    if (index>=0) return index;

    WriteToDebug("Did not find match, returning null");

    return null;
}
//___________________________________________________________


function SCORM2004_GetInteractionType(strInteractionID)
{

    var intInteractionIndex;

    WriteToDebug("In SCORM2004_GetInteractionType, strInteractionID=" + strInteractionID);

    SCORM2004_ClearErrorInfo();

    intInteractionIndex = SCORM2004_FindInteractionIndexFromID(strInteractionID);


    if(intInteractionIndex == undefined || intInteractionIndex == null){
        return null;
    }


    WriteToDebug("intInteractionIndex=" + intInteractionIndex);

    var type = SCORM2004_CallGetValue("cmi.interactions." + intInteractionIndex + ".type");

    switch (type)
    {
        case SCORM2004_INTERACTION_TYPE_FILL_IN:
            return INTERACTION_TYPE_FILL_IN;
        case SCORM2004_INTERACTION_TYPE_LONG_FILL_IN:
            return INTERACTION_TYPE_LONG_FILL_IN;
        case SCORM2004_INTERACTION_TYPE_CHOICE:
            return INTERACTION_TYPE_CHOICE;
        case SCORM2004_INTERACTION_TYPE_LIKERT:
            return INTERACTION_TYPE_LIKERT;
        case SCORM2004_INTERACTION_TYPE_MATCHING:
            return INTERACTION_TYPE_MATCHING;
        case SCORM2004_INTERACTION_TYPE_NUMERIC:
            return INTERACTION_TYPE_NUMERIC;
        case SCORM2004_INTERACTION_TYPE_PERFORMANCE:
            return INTERACTION_TYPE_PERFORMANCE;
        case SCORM2004_INTERACTION_TYPE_SEQUENCING:
            return INTERACTION_TYPE_SEQUENCING;
        case SCORM2004_INTERACTION_TYPE_TRUE_FALSE:
            return INTERACTION_TYPE_TRUE_FALSE;
        default:
            return "";
    }


}

//public
function SCORM2004_GetInteractionTimestamp(strInteractionID)
{
    WriteToDebug("In SCORM2004_GetInteractionTimestamp, strInteractionID=" + strInteractionID);
    var intInteractionIndex = SCORM2004_FindInteractionIndexFromID(strInteractionID);
    WriteToDebug("intInteractionIndex=" + intInteractionIndex);

    SCORM2004_ClearErrorInfo();



    if(intInteractionIndex == undefined || intInteractionIndex == null){
        return null;
    }



    return SCORM2004_CallGetValue(ConvertIso8601TimeStampToDate("cmi.interactions." + intInteractionIndex + ".timestamp"));

}



//public
function SCORM2004_GetInteractionCorrectResponses(strInteractionID)
{

    WriteToDebug("In SCORM2004_GetInteractionCorrectResponses, strInteractionID=" + strInteractionID);

    var intInteractionIndex = SCORM2004_FindInteractionIndexFromID(strInteractionID);
    WriteToDebug("intInteractionIndex=" + intInteractionIndex);

    SCORM2004_ClearErrorInfo();


    if(intInteractionIndex == undefined || intInteractionIndex == null){
        return null;
    }


    var strType = SCORM2004_CallGetValue("cmi.interactions." + intInteractionIndex + ".type");

    var intCorrectResponseCount = SCORM2004_CallGetValue("cmi.interactions." + intInteractionIndex + ".correct_responses._count");


    if (intCorrectResponseCount == ""){
        WriteToDebug("Setting intCorrectResponseCount=0");
        return 0;
    }

    intCorrectResponseCount = parseInt(intCorrectResponseCount, 10);
    WriteToDebug("intCorrectResponseCount=" + intCorrectResponseCount);
    if (intCorrectResponseCount==0) return new Array();
    if (intCorrectResponseCount>1) WriteToDebug("SCORM Driver is not currently implemented to support multiple correct response combinations and will only return the first");

    var strResponse = new String(SCORM2004_CallGetValue("cmi.interactions." + intInteractionIndex + ".correct_responses.0.pattern"));
    var aryResponse = strResponse.split("[,]");
    WriteToDebug("aryResponse.length = " + aryResponse.length);
    aryResponse = SCORM2004_ProcessResponseArray(strType, aryResponse);

    /*
    //this is the basis for handling N correct responses
    //because the API currently does not allow for the posting of multiple
    //correct response combinations, this functionality is not currently
    //implemented
    var aryResponses = new Array();

    for (i=0; i<intCorrectResponseCount; i++)
    {
        WriteToDebug("examining CorrectResponse: " + i);
        var strResponse = new String(SCORM2004_CallGetValue("cmi.interactions." + intInteractionIndex + ".correct_responses." + i + ".pattern"));
        var aryResponse = strResponse.split("[,]");
        WriteToDebug("aryResponses.length = " + aryResponses.length);
        aryResponse = SCORM2004_ProcessResponseArray(strType, aryResponse);
        aryResponses.push(aryResponse);
    }
    */




    WriteToDebug("aryResponse.length = " + aryResponse.length);
    return aryResponse;



}



//public
function SCORM2004_GetInteractionWeighting(strInteractionID)
{
    WriteToDebug("In SCORM2004_GetInteractionWeighting, strInteractionID=" + strInteractionID);

    var intInteractionIndex = SCORM2004_FindInteractionIndexFromID(strInteractionID);
    WriteToDebug("intInteractionIndex=" + intInteractionIndex);

    SCORM2004_ClearErrorInfo();

    if(intInteractionIndex == undefined || intInteractionIndex == null){
        return null;
    }

    return SCORM2004_CallGetValue("cmi.interactions." + intInteractionIndex + ".weighting");

}



//public
function SCORM2004_GetInteractionLearnerResponses(strInteractionID)
{
    WriteToDebug("In SCORM2004_GetInteractionLearnerResponses, strInteractionID=" + strInteractionID);
    var intInteractionIndex = SCORM2004_FindInteractionIndexFromID(strInteractionID);
    WriteToDebug("intInteractionIndex=" + intInteractionIndex);

    SCORM2004_ClearErrorInfo();

    if(intInteractionIndex == undefined || intInteractionIndex == null){
        return null;
    }


    var strType = SCORM2004_CallGetValue("cmi.interactions." + intInteractionIndex + ".type");

    var strResponse = new String(SCORM2004_CallGetValue("cmi.interactions." + intInteractionIndex + ".learner_response"));

    var aryResponses = strResponse.split("[,]");

    WriteToDebug("aryResponses.length = " + aryResponses.length);

    aryResponses = SCORM2004_ProcessResponseArray(strType, aryResponses);

    return aryResponses;
}

function SCORM2004_ProcessResponseArray(strInteractionType, aryResponses)
{
    //process them (into ResponseIdentifiers, etc?)
    //only include the right amount of stuff

    WriteToDebug("Processing Response Array with " + aryResponses.length + " pieces");


    for(var i=0; i<aryResponses.length; i++)
    {
        if (strInteractionType == SCORM2004_INTERACTION_TYPE_MATCHING)
        {
            WriteToDebug("processing matching type, i=" + i);
            aryResponses[i] = CreateMatchingResponse(aryResponses[i]);
        }
    }

    return aryResponses;

}

//public
function SCORM2004_GetInteractionResult(strInteractionID)
{
    WriteToDebug("In SCORM2004_GetInteractionResult, strInteractionID=" + strInteractionID);

    var intInteractionIndex = SCORM2004_FindInteractionIndexFromID(strInteractionID);
    WriteToDebug("intInteractionIndex=" + intInteractionIndex);

    SCORM2004_ClearErrorInfo();

    if(intInteractionIndex == undefined || intInteractionIndex == null){
        return null;
    }


    return SCORM2004_CallGetValue("cmi.interactions." + intInteractionIndex + ".result");


}



//public
function SCORM2004_GetInteractionLatency(strInteractionID)
{
    WriteToDebug("In SCORM2004_GetInteractionLatency, strInteractionID=" + strInteractionID);
    var intInteractionIndex = SCORM2004_FindInteractionIndexFromID(strInteractionID);
    WriteToDebug("intInteractionIndex=" + intInteractionIndex);

    SCORM2004_ClearErrorInfo();

    if(intInteractionIndex == undefined || intInteractionIndex == null){
        return null;
    }

    var strLatency = SCORM2004_CallGetValue("cmi.interactions." + intInteractionIndex + ".latency");

    WriteToDebug("latency returns: " + strLatency);
    var intLatency = ConvertScorm2004TimeToMS(strLatency);
    WriteToDebug("latency in milliseconds: " + intLatency);

    return intLatency;

}



//public
function SCORM2004_GetInteractionDescription(strInteractionID)
{
    WriteToDebug("In SCORM2004_GetInteractionDescription, strInteractionID=" + strInteractionID);
    var intInteractionIndex = SCORM2004_FindInteractionIndexFromID(strInteractionID);
    WriteToDebug("intInteractionIndex=" + intInteractionIndex);

    SCORM2004_ClearErrorInfo();

    if(intInteractionIndex == undefined || intInteractionIndex == null){
        return null;
    }


    return SCORM2004_CallGetValue("cmi.interactions." + intInteractionIndex + ".description");


}

//________________________________________________


//public
function SCORM2004_CreateDataBucket(strBucketId, intMinSize, intMaxSize, strPersistenceType){

    WriteToDebug("In SCORM2004_CreateDataBucket, strBucketId=" + strBucketId + ", intMinSize=" + intMinSize + ", intMaxSize=" + intMaxSize + ", course=" + strPersistenceType);

    if (SCORM2004_DetectSSPSupport()){

        if (SCORM2004_DoesBucketExist(strBucketId) == true){
            WriteToDebug("Bucket already exists and can't be re-allocated.");
            return false;
        }
        else{
            return SCORM2004_CallSetValue("ssp.allocate", "{bucketID=" + strBucketId + "}{requested=" + intMaxSize + "}{minimum=" + intMinSize + "}{reducible=true}{persistence="+ strPersistenceType +"}")//")
        }
    }
    else{
        WriteToDebug("SSP is not supported in this LMS, returning false.");
        return false;
    }
}

//public
function SCORM2004_GetDataFromBucket(strBucketId){
    WriteToDebug("In SCORM2004_GetDataFromBucket, strBucketId=" + strBucketId);

    if (SCORM2004_DetectSSPSupport()){
        var data = SCORM2004_CallGetValue("ssp.data.{bucketID=" + strBucketId + "}");
        return data;
    }
    else{
        WriteToDebug("SSP is not supported in this LMS, returning empty string.");
        return "";
    }
}

//public
function SCORM2004_PutDataInBucket(strBucketId, strData, blnAppendToEnd){
    WriteToDebug("In SCORM2004_PutDataInBucket, strBucketId=" + strBucketId + ", blnAppendToEnd=" + blnAppendToEnd + ", strData=" + strData);

    if (SCORM2004_DetectSSPSupport()){

        if (blnAppendToEnd == true){
            return SCORM2004_CallSetValue("ssp.appendData", "{bucketID=" + strBucketId + "}" + strData);
        }
        else{
            return SCORM2004_CallSetValue("ssp.data", "{bucketID=" + strBucketId + "}" + strData);
        }
    }
    else{
        WriteToDebug("SSP is not supported in this LMS, returning false.");
        return false;
    }
}

//public
function SCORM2004_DetectSSPSupport(){

    WriteToDebug("In SCORM2004_DetectSSPSupport");

    if (blnSCORM2004_SSP_Is_Supported == true){
        WriteToDebug("Support already detected, returning true");
        return true;
    }

    else if (blnSCORM2004_SSP_Is_Supported == false){
        WriteToDebug("Support already determined to me missing, returning false");
        return false;
    }

    else{
        //make an error-free call to the SSP data model to see if it is supported
        var intBucketCount = SCORM2004_CallGetValue("ssp._count");

        //if the call succeed, then SSP is supported, otherwise it is not
        if (SCORM2004_GetLastError() == NO_ERROR){
            WriteToDebug("SSP data model call succeeded, SSP is supported");
            blnSCORM2004_SSP_Is_Supported = true;
            return true;
        }
        else{
            WriteToDebug("SSP data model call failed, SSP is NOT supported");
            blnSCORM2004_SSP_Is_Supported = false;
            return false;
        }
    }
}

//public
function SCORM2004_GetBucketInfo(strBucketId){

    WriteToDebug("In SCORM2004_GetBucketInfo, strBucketId=" + strBucketId);

    var intTotalSpace = 0;
    var intUsedSpace = 0;

    var strBucketState = new String(SCORM2004_CallGetValue("ssp.bucket_state.{bucketID=" + strBucketId + "}"));

    if (strBucketState == "" || strBucketState == null || strBucketState == undefined){
        WriteToDebug ("Could not retrieve bucket state, returning 0 total size and 0 used size");
        return new SSPBucketSize(0, 0);
    }

    var sectionArray = strBucketState.split("{");

    for (var section in sectionArray){

        section = new String(sectionArray[section]);

        section = section.replace("}", "");

        if (section.indexOf("totalSpace", 0) == 0){
            WriteToDebug("Found total space");
            intTotalSpace = parseInt(section.substr(11), 10);
            WriteToDebug("total space=" + intTotalSpace);
        }
        else if (section.indexOf("used", 0) == 0){
            WriteToDebug("Found used space");
            intUsedSpace = parseInt(section.substr(5), 10);
            WriteToDebug("used=" + intUsedSpace);
        }
    }

    var returnValue = new SSPBucketSize(intTotalSpace, intUsedSpace);

    return returnValue;
}

//private
function SCORM2004_DoesBucketExist(strBucketId){

    WriteToDebug("In SCORM2004_DoesBucketExist, strBucketId=" + strBucketId);

    var intBucketCount = SCORM2004_CallGetValue("ssp._count");
    intBucketCount = parseInt(intBucketCount, 10);

    for (var i=0; i < intBucketCount; i++){
        if (strBucketId == SCORM2004_CallGetValue("ssp." + i + ".id")){
            WriteToDebug("Bucket '" + strBucketId + "' Exists");
            return true;
        }
    }

    WriteToDebug("Bucket '" + strBucketId + "' DOES NOT Exist");
    return false;
}

function SCORM2004_SetNavigationRequest(strNavRequest){
     WriteToDebug("In SCORM2004_SetNavigationRequest, strNavRequest=" + strNavRequest);
    SCORM2004_ClearErrorInfo();
    //first check to see if it is a choice nav request
    var regValidChoice = /^\{target=[.A-Za-z0-9_-]+\}choice$/;
    if (strNavRequest.match(regValidChoice)){
        SCORM2004_CallSetValue("adl.nav.request",strNavRequest);
        return true;
    }else{
        //check for other valid options
        switch(strNavRequest){
            case "continue":
                break;
            case "previous":
                break;
            case "exit":
                break;
            case "exitAll":
                break;
            case "abandon":
                break;
            case "abandonAll":
                break;
            case "suspendAll":
                break;
            case "_none_":
                break;
            default:
            WriteToDebug("In SCORM2004_SetNavigationRequest, NavRequest is not valid - strNavRequest=" + strNavRequest);
                return false;
        }
        SCORM2004_CallSetValue("adl.nav.request",strNavRequest);
        return true;
    }
}

function SCORM2004_GetNavigationRequest(){
    WriteToDebug("In SCORM2004_GetNavigationRequest");
    SCORM2004_ClearErrorInfo();
    return SCORM2004_CallGetValue("adl.nav.request");
}

/*
o ?continue?: Indicates to the LMS that the content asserts that a Continue navigation request should be processed immediately following the SCO?s termination.
o ?previous?: Indicates to the LMS that the content asserts that a Previous navigation request should be processed immediately following the SCO?s termination.
o ?choice?: Indicates to the LMS that the content asserts that a Choice navigation request should be processed immediately following the SCO?s termination.
o ?exit?: Indicates to the LMS that the content asserts that an Exit navigation request should be processed immediately following the SCO?s termination.
o ?exitAll?: Indicates to the LMS that the content asserts that an Exit All navigation request should be processed immediately following the SCO?s termination.
o ?abandon?: Indicates to the LMS that the content asserts that an Abandon navigation request should be processed immediately following the SCO?s termination.
o ?abandonAll?: Indicates to the LMS that the content asserts that an Abandon All navigation request should be processed immediately following the SCO?s termination.
o ?suspendAll?: Indicates to the LMS that the content asserts that a Suspend All navigation request should be processed immediately following the SCO?s termination.
o ?_none_?: Indicates to the LMS that the content asserts that any previous navigation request indicated by the SCO should not be processed immediately following the SCO?s termination. Setting this value effectively clears any pending navigation request.
*/

//---------------------------------------------------------------------------------
//Functions to Call the SCORM API

//note: in all functions that interact with API, we concact any returned strings with "" to convert
//the value to a string type, do this b/c many SCORM API's will return a Java String instead of a Javascript
//string


function SCORM2004_CallInitialize(){

    var strResult;

    WriteToDebug("In SCORM2004_CallInitialize");

    SCORM2004_objAPI = SCORM2004_GrabAPI();

    WriteToDebug("Calling Initialize");

    strResult = SCORM2004_objAPI.Initialize("");
    strResult = strResult + "";

    WriteToDebug("strResult=" + strResult);

    if (strResult == SCORM2004_FALSE){

        WriteToDebug("Detected failed call to initialize");

        SCORM2004_SetErrorInfo();

        WriteToDebug ("Error calling Initialize:");
        WriteToDebug ("              Error Number=" + intSCORM2004Error);
        WriteToDebug ("              Error String=" + strSCORM2004ErrorString);
        WriteToDebug ("              Diagnostic=" + strSCORM2004ErrorDiagnostic);

        return false;

    }

    WriteToDebug("Returning true");

    return true;
}

function SCORM2004_CallSetValue(strElement, strValue){

    var strResult;

    WriteToDebug("SCORM2004_CallSetValue strElement=" + strElement + ", strValue=" + strValue);

    if (blnReviewModeSoReadOnly === true){
        WriteToDebug("Mode is Review and configuration setting dictates this should be read only so exiting.");
        return true;
    }

    SCORM2004_objAPI = SCORM2004_GrabAPI();

    WriteToDebug("Calling SetValue");

    strElement = strElement + "";
    strValue = strValue + "";

    strResult = SCORM2004_objAPI.SetValue(strElement, strValue)
    strResult = strResult + "";

    WriteToDebug("strResult=" + strResult);

    if (strResult == SCORM2004_FALSE){

        WriteToDebug("Detected Failed call to SetValue");

        SCORM2004_SetErrorInfo();

        WriteToDebug ("Error calling SetValue:");
        WriteToDebug ("              strElement=" + strElement);
        WriteToDebug ("              strValue=" + strValue);
        WriteToDebug ("              Error Number=" + intSCORM2004Error);
        WriteToDebug ("              Error String=" + strSCORM2004ErrorString);
        WriteToDebug ("              Diagnostic=" + strSCORM2004ErrorDiagnostic);

        return false;
    }

    WriteToDebug("Returning true");

    return true;
}

function SCORM2004_CallGetValue(strElement){

    var strResult

    WriteToDebug("In SCORM2004_CallGetValue strElement=" + strElement);

    SCORM2004_objAPI = SCORM2004_GrabAPI();

    WriteToDebug("Call GetValue");

    strElement = strElement + "";

    strResult = SCORM2004_objAPI.GetValue(strElement) + ""

    WriteToDebug("strResult=" + strResult);

    intSCORM2004Error = SCORM2004_objAPI.GetLastError()
    intSCORM2004Error = intSCORM2004Error + "";

    WriteToDebug("intSCORM2004Error=" + intSCORM2004Error);

    if (intSCORM2004Error != SCORM2004_NO_ERROR){

        WriteToDebug("Detected failed called to GetValue");

        SCORM2004_SetErrorInfo();

        WriteToDebug ("Error calling LMSGetValue:");
        WriteToDebug ("              strElement=" + strElement);
        WriteToDebug ("              Error Number=" + intSCORM2004Error);
        WriteToDebug ("              Error String=" + strSCORM2004ErrorString);
        WriteToDebug ("              Diagnostic=" + strSCORM2004ErrorDiagnostic);
    }

    WriteToDebug("Returning " + strResult);

    return strResult;

}


function SCORM2004_CallCommit(){

    var strResult;

    WriteToDebug("In SCORM2004_CallCommit");

    SCORM2004_objAPI = SCORM2004_GrabAPI();

    WriteToDebug("Calling Commit");

    strResult = SCORM2004_objAPI.Commit("");

    strResult = strResult + "";

    WriteToDebug("strResult=" + strResult);

    if (strResult == SCORM2004_FALSE){

        WriteToDebug("Detected failed call to Commit");

        SCORM2004_SetErrorInfo();

        WriteToDebug ("Error calling Commit:");
        WriteToDebug ("              Error Number=" + intSCORM2004Error);
        WriteToDebug ("              Error String=" + strSCORM2004ErrorString);
        WriteToDebug ("              Diagnostic=" + strSCORM2004ErrorDiagnostic);

        return false;
    }

    WriteToDebug("Returning true");

    return true;
}


function SCORM2004_CallTerminate(){

    var strResult;

    WriteToDebug("In SCORM2004_CallTerminate");

    SCORM2004_objAPI = SCORM2004_GrabAPI();

    WriteToDebug("Calling Terminate");

    strResult = SCORM2004_objAPI.Terminate("");

    strResult = strResult + "";

    WriteToDebug("strResult=" + strResult);

    if (strResult == SCORM2004_FALSE){

        WriteToDebug("Detected failed call to Terminate");

        SCORM2004_SetErrorInfo();

        WriteToDebug ("Error calling Terminate:");
        WriteToDebug ("              Error Number=" + intSCORM2004Error);
        WriteToDebug ("              Error String=" + strSCORM2004ErrorString);
        WriteToDebug ("              Diagnostic=" + strSCORM2004ErrorDiagnostic);

        return false;
    }

    WriteToDebug("Returning True");

    return true;
}


//---------------------------------------------------------------------------------
//Error Handling Functions
function SCORM2004_ClearErrorInfo(){

    WriteToDebug("In SCORM2004_ClearErrorInfo");

    intSCORM2004Error = SCORM2004_NO_ERROR;
    strSCORM2004ErrorString = "";
    strSCORM2004ErrorDiagnostic = "";
}

function SCORM2004_SetErrorInfo(){

    WriteToDebug("In SCORM2004_SetErrorInfo");

    intSCORM2004Error = SCORM2004_objAPI.GetLastError();
    strSCORM2004ErrorString = SCORM2004_objAPI.GetErrorString(intSCORM2004Error);
    strSCORM2004ErrorDiagnostic = SCORM2004_objAPI.GetDiagnostic("");

    intSCORM2004Error = intSCORM2004Error + "";
    strSCORM2004ErrorString = strSCORM2004ErrorString + "";
    strSCORM2004ErrorDiagnostic = strSCORM2004ErrorDiagnostic + "";

    WriteToDebug("intSCORM2004Error=" + intSCORM2004Error);
    WriteToDebug("strSCORM2004ErrorString=" + strSCORM2004ErrorString);
    WriteToDebug("strSCORM2004ErrorDiagnostic=" + strSCORM2004ErrorDiagnostic);
}

function SCORM2004_SetErrorInfoManually(intNum, strString, strDiagnostic){

        WriteToDebug("In SCORM2004_SetErrorInfoManually");
        WriteToDebug("ERROR-Num=" + intNum);
        WriteToDebug("      String=" + strString);
        WriteToDebug("      Diag=" + strDiagnostic);

        intSCORM2004Error = intNum;
        strSCORM2004ErrorString = strString;
        strSCORM2004ErrorDiagnostic = strDiagnostic;

}

function SCORM2004_GetLastError(){

    WriteToDebug("In SCORM2004_GetLastError");

    if (intSCORM2004Error == SCORM2004_NO_ERROR){
        WriteToDebug("Returning No Error");
        return NO_ERROR;
    }
    else {
        WriteToDebug("Returning " + intSCORMError);
        return intSCORM2004Error;
    }
}

function SCORM2004_GetLastErrorDesc(){
    WriteToDebug("In SCORM2004_GetLastErrorDesc, " + strSCORM2004ErrorString + "\n" + strSCORM2004ErrorDiagnostic);
    return strSCORM2004ErrorString + "\n" + strSCORM2004ErrorDiagnostic;
}



//---------------------------------------------------------------------------------
//API Locating Functions

function SCORM2004_GrabAPI(){

    WriteToDebug("In SCORM2004_GrabAPI");

    if (typeof(SCORM2004_objAPI) == "undefined" || SCORM2004_objAPI == null){
        WriteToDebug("Searching with Rustici Software algorithm");
        SCORM2004_objAPI = SCORM2004_GetAPI();
    }

    if (typeof(SCORM2004_objAPI) == "undefined" || SCORM2004_objAPI == null || SCORM2004_objAPI ==false){
        WriteToDebug("Searching with SearchForAPI");
        SCORM2004_objAPI = SCORM2004_SearchForAPI(window);
    }

    WriteToDebug("Grab API, returning, found API = " + (SCORM2004_objAPI != null));

    return SCORM2004_objAPI;

}




/*******************************************************************
    * SCORM 2004 API Search Algorithm
    * Description - Improvement of the algorithm developed by ADL to
        find the SCORM 2004 API Adapter. The improvements eliminate
        errors, improve code clarity and eliminate the dependence
        on global variables. The errors removed include:
            - The "win" variable was never declared in a scope
              accessible to the GetAPI function
            - A call to API.version which is not part of the SCORM
              2004 specification
            - The previous algorithm was not able to find the API
              if it was located in the window's parent's opener
    * Original Author - ADL & Concurrent Technologies Corporation
    * Author -  Mike Rustici (April 1, 2004)
                Rustici Software, LLC
                http://www.scorm.com
                mike@scorm.com
*******************************************************************/




/*
ScanParentsForApi
-Searches all the parents of a given window until
 it finds an object named "API_1484_11". If an
 object of that name is found, a reference to it
 is returned. Otherwise, this function returns null.
*/
function SCORM2004_ScanParentsForApi(win)
{

    /*
    Establish an outrageously high maximum number of
    parent windows that we are will to search as a
    safe guard against an infinite loop. This is
    probably not strictly necessary, but different
    browsers can do funny things with undefined objects.
    */
    var MAX_PARENTS_TO_SEARCH = 500;
    var nParentsSearched = 0;

    /*
    Search each parent window until we either:
         -find the API,
         -encounter a window with no parent (parent is null
                or the same as the current window)
         -or, have reached our maximum nesting threshold
    */
    while ( (win.API_1484_11 == null || win.API_1484_11 == undefined) &&
            (win.parent != null) && (win.parent != win) &&
            (nParentsSearched <= MAX_PARENTS_TO_SEARCH)
          )
    {

        nParentsSearched++;
        win = win.parent;
    }

    /*
    If the API doesn't exist in the window we stopped looping on,
    then this will return null.
    */
    return win.API_1484_11;
}


/*
GetAPI
-Searches all parent and opener windows relative to the
 current window for the SCORM 2004 API Adapter.
 Returns a reference to the API Adapter if found or null
 otherwise.
*/
function SCORM2004_GetAPI()
{

    var API = null;

    //Search all the parents of the current window if there are any
    if ((window.parent != null) && (window.parent != window))
    {
        API = SCORM2004_ScanParentsForApi(window.parent);
    }

    /*
    If we didn't find the API in this window's chain of parents,
    then search all the parents of the opener window if there is one
    */
    if ((API == null) && (window.top.opener != null))
    {
        API = SCORM2004_ScanParentsForApi(window.top.opener);
    }

    return API;
}

function SCORM2004_SearchForAPI(wndLookIn){

    WriteToDebug("SCORM2004_SearchForAPI");

    var objAPITemp = null;
    var strDebugID = "";

    strDebugID = "Name=" + wndLookIn.name + ", href=" + wndLookIn.location.href

    objAPITemp = wndLookIn.API_1484_11;

    if (SCORM2004_APIFound(objAPITemp)){
        WriteToDebug("Found API in this window - "  + strDebugID);
        return objAPITemp;
    }

    if (SCORM2004_WindowHasParent(wndLookIn)){
        WriteToDebug("Searching Parent - "  + strDebugID);
        objAPITemp = SCORM2004_SearchForAPI(wndLookIn.parent);
    }

    if (SCORM2004_APIFound(objAPITemp)){
        WriteToDebug("Found API in a parent - "  + strDebugID);
        return objAPITemp;
    }

    if (SCORM2004_WindowHasOpener(wndLookIn)){
        WriteToDebug("Searching Opener - "  + strDebugID);
        objAPITemp = SCORM2004_SearchForAPI(wndLookIn.opener);
    }

    if (SCORM2004_APIFound(objAPITemp)){
        WriteToDebug("Found API in an opener - "  + strDebugID);
        return objAPITemp;
    }

    //look in child frames individually, don't call this function recursively
    //on them to prevent an infinite loop when it looks back up to the parents
    WriteToDebug("Looking in children - "  + strDebugID);
    objAPITemp = SCORM2004_LookInChildren(wndLookIn);

    if (SCORM2004_APIFound(objAPITemp)){
        WriteToDebug("Found API in Children - "  + strDebugID);
        return objAPITemp;
    }

    WriteToDebug("Didn't find API in this window - "  + strDebugID);
    return null;
}

function SCORM2004_LookInChildren(wnd){

    WriteToDebug("SCORM2004_LookInChildren");

    var objAPITemp = null;

    var strDebugID = "";

    strDebugID = "Name=" + wnd.name + ", href=" + wnd.location.href

    for (var i=0; i < wnd.frames.length; i++){

        WriteToDebug("Looking in child frame " + i);

        objAPITemp = wnd.frames[i].API_1484_11;

        if (SCORM2004_APIFound(objAPITemp)){
            WriteToDebug("Found API in child frame of " + strDebugID);
            return objAPITemp;
        }

        WriteToDebug("Looking in this child's children " + strDebugID);
        objAPITemp = SCORM2004_LookInChildren(wnd.frames[i]);

        if (SCORM2004_APIFound(objAPITemp)){
            WriteToDebug("API found in this child's children " + strDebugID);
            return objAPITemp;
        }
    }

    return null;
}

function SCORM2004_WindowHasOpener(wnd){
    WriteToDebug("In SCORM2004_WindowHasOpener");
    if ((wnd.opener != null) && (wnd.opener != wnd) && (typeof(wnd.opener) != "undefined")){
        WriteToDebug("Window Does Have Opener");
        return true;
    }
    else{
        WriteToDebug("Window Does Not Have Opener");
        return false;
    }
}

function SCORM2004_WindowHasParent(wnd){
    WriteToDebug("In SCORM2004_WindowHasParent");
    if ((wnd.parent != null) && (wnd.parent != wnd) && (typeof(wnd.parent) != "undefined")){
        WriteToDebug("Window Does Have Parent");
        return true;
    }
    else{
        WriteToDebug("Window Does Not Have Parent");
        return false;
    }
}


function SCORM2004_APIFound(obj){
    WriteToDebug("In SCORM2004_APIFound");
    if (obj == null || typeof(obj) == "undefined"){
        WriteToDebug("API NOT Found");
        return false;
    }
    else{
        WriteToDebug("API Found");
        return true;
    }
}

var STANDARD = 'SCORM';

var SCORM_LOGOUT = "logout";
var SCORM_SUSPEND = "suspend";
var SCORM_NORMAL_EXIT = "";
var SCORM_TIMEOUT = "time-out";

var SCORM_PASSED = "passed";
var SCORM_FAILED = "failed";
var SCORM_COMPLETED = "completed";
var SCORM_BROWSED = "browsed";
var SCORM_INCOMPLETE = "incomplete";
var SCORM_NOT_ATTEMPTED = "not attempted";

var SCORM_CREDIT = "credit";
var SCORM_NO_CREDIT = "no-credit";

var SCORM_BROWSE = "browse";
var SCORM_NORMAL = "normal";
var SCORM_REVIEW = "review";

var SCORM_ENTRY_ABINITIO = "ab-initio";
var SCORM_ENTRY_RESUME = "resume";
var SCORM_ENTRY_NORMAL = "";


var SCORM_TLA_EXIT_MESSAGE = "exit,message";
var SCORM_TLA_EXIT_NO_MESSAGE = "exit,no message";
var SCORM_TLA_CONTINUE_MESSAGE = "continue,message";
var SCORM_TLA_CONTINUE_NO_MESSAGE = "continue,no message";

var SCORM_RESULT_CORRECT = "correct";
var SCORM_RESULT_WRONG = "wrong";
var SCORM_RESULT_UNANTICIPATED = "unanticipated";
var SCORM_RESULT_NEUTRAL = "neutral";

var SCORM_INTERACTION_TYPE_TRUE_FALSE = "true-false";
var SCORM_INTERACTION_TYPE_CHOICE = "choice";
var SCORM_INTERACTION_FILL_IN = "fill-in";
var SCORM_INTERACTION_TYPE_MATCHING = "matching";
var SCORM_INTERACTION_TYPE_PERFORMANCE = "performance";
var SCORM_INTERACTION_TYPE_SEQUENCING = "sequencing";
var SCORM_INTERACTION_TYPE_LIKERT = "likert";
var SCORM_INTERACTION_TYPE_NUMERIC = "numeric";


var SCORM_NO_ERROR = "0";
var SCORM_ERROR_INVALID_PREFERENCE = "-1";
var SCORM_ERROR_INVALID_STATUS = "-2";
var SCORM_ERROR_INVALID_SPEED = "-3";
var SCORM_ERROR_INVALID_TIMESPAN = "-4";
var SCORM_ERROR_INVALID_TIME_LIMIT_ACTION = "-5";
var SCORM_ERROR_INVALID_DECIMAL = "-6";
var SCORM_ERROR_INVALID_CREDIT = "-7";
var SCORM_ERROR_INVALID_LESSON_MODE = "-8";
var SCORM_ERROR_INVALID_ENTRY = "-9";

var SCORM_TRUE = "true";
var SCORM_FALSE = "false";

var SCORM_findAPITries = 0;
var SCORM_objAPI = null;

var intSCORMError = SCORM_NO_ERROR;
var strSCORMErrorString = "";
var strSCORMErrorDiagnostic = "";

var blnReviewModeSoReadOnly = false;

function SCORM_Initialize(){

    var blnResult = true;

    WriteToDebug("In SCORM_Initialize");

    SCORM_ClearErrorInfo();

    WriteToDebug("Grabbing API");

    try{
        SCORM_objAPI = SCORM_GrabAPI();
    }
    catch (e){
        WriteToDebug("Error grabbing 1.2 API-" + e.name + ":" + e.message);
    }

    if (typeof(SCORM_objAPI) == "undefined" || SCORM_objAPI == null){
        WriteToDebug("Unable to acquire SCORM API:")
        WriteToDebug("SCORM_objAPI=" + typeof(SCORM_objAPI));

        InitializeExecuted(false, "Error - unable to acquire LMS API, content may not play properly and results may not be recorded.  Please contact technical support.");
        return false;
    }

    WriteToDebug("Calling LMSInit");

    blnResult = SCORM_CallLMSInitialize();

    if (! blnResult){
        WriteToDebug("ERROR Initializing LMS");

        InitializeExecuted(false, "Error initializing communications with LMS");

        return false;
    }

    //only reset status and such if we are not reviewing
    if (SCORM_GetLessonMode() != MODE_REVIEW){

        if (SCORM_IsContentInBrowseMode()){
            WriteToDebug("Setting Status to Browsed");
            blnResult = SCORM_CallLMSSetValue("cmi.core.lesson_status", SCORM_BROWSED);
        }
        else{
            if (! PREVENT_STATUS_CHANGE_DURING_INIT) {
                //only set the status to incomplete if it's not attempted yet
                if (SCORM_GetStatus() == LESSON_STATUS_NOT_ATTEMPTED){
                    WriteToDebug("Setting Status to Incomplete");
                    blnResult = SCORM_CallLMSSetValue("cmi.core.lesson_status", SCORM_INCOMPLETE);
                }
            }
        }

        //we want to set the exit type to suspend immediately because some LMS's only store data if they get a suspend request
        blnResult = SCORM_CallLMSSetValue("cmi.core.exit", SCORM_TranslateExitTypeToSCORM(DEFAULT_EXIT_TYPE)) && blnResult;
    }
    else{
        //mode is review, check if we should go to read only mode
        if (!(typeof(REVIEW_MODE_IS_READ_ONLY) == "undefined") && REVIEW_MODE_IS_READ_ONLY === true){
            blnReviewModeSoReadOnly = true;
        }
    }

    WriteToDebug("Calling InitializeExecuted with parameter-" + blnResult);

    InitializeExecuted(blnResult, "");

    return;
}




function SCORM_Finish(strExitType, blnStatusWasSet){

    var strStatusAfterCompletion;
    var blnResult = true;

    WriteToDebug("In SCORM_Finish strExitType=" + strExitType + ", blnStatusWasSet=" + blnStatusWasSet);

    SCORM_ClearErrorInfo();

    if ( (strExitType == EXIT_TYPE_FINISH) && ! blnStatusWasSet ){

        WriteToDebug("Getting completion status");

        strStatusAfterCompletion = SCORM_GetCompletionStatus();

        WriteToDebug("Setting completion status to " + strStatusAfterCompletion);

        blnResult = SCORM_CallLMSSetValue("cmi.core.lesson_status", strStatusAfterCompletion) && blnResult;
    }

    WriteToDebug("Setting Exit");

    blnResult = SCORM_CallLMSSetValue("cmi.core.exit", SCORM_TranslateExitTypeToSCORM(strExitType)) && blnResult;

    WriteToDebug("Calling Commit");

    blnResult = SCORM_CallLMSCommit() && blnResult;

    WriteToDebug("Calling Finish");

    blnResult = SCORM_CallLMSFinish() && blnResult;

    WriteToDebug("Returning " + blnResult);

    return blnResult;

}


function SCORM_CommitData(){
    WriteToDebug("In SCORM_CommitData");
    SCORM_ClearErrorInfo();
    return SCORM_CallLMSCommit();
}



//---------------------------------------------------------------------------------
//General Get and Set Values

function SCORM_GetStudentID(){
    WriteToDebug("In SCORM_GetStudentID");
    SCORM_ClearErrorInfo();
    return SCORM_CallLMSGetValue("cmi.core.student_id");
}

function SCORM_GetStudentName(){
    WriteToDebug("In SCORM_GetStudentName");
    SCORM_ClearErrorInfo();
    return SCORM_CallLMSGetValue("cmi.core.student_name");
}

function SCORM_GetBookmark(){
    WriteToDebug("In SCORM_GetBookmark");
    SCORM_ClearErrorInfo();
    return SCORM_CallLMSGetValue("cmi.core.lesson_location");
}

function SCORM_SetBookmark(strBookmark){
    WriteToDebug("In SCORM_SetBookmark strBookmark=" + strBookmark);
    SCORM_ClearErrorInfo();
    return SCORM_CallLMSSetValue("cmi.core.lesson_location", strBookmark);
}

function SCORM_GetDataChunk(){
    WriteToDebug("In SCORM_GetDataChunk");
    SCORM_ClearErrorInfo();
    return SCORM_CallLMSGetValue("cmi.suspend_data");
}

function SCORM_SetDataChunk(strData){
    //need to check for character limits here 4096 characters
    WriteToDebug("In SCORM_SetDataChunk");
    SCORM_ClearErrorInfo();

    if(USE_STRICT_SUSPEND_DATA_LIMITS==true) {
        if(strData.length > 4096) {
            WriteToDebug("SCORM_SetDataChunk - suspend_data too large (4096 character limit for SCORM 1.2)");
            return false;
        }else{
            return SCORM_CallLMSSetValue("cmi.suspend_data", strData);
        }
    }else{
        return SCORM_CallLMSSetValue("cmi.suspend_data", strData);
    }
}


function SCORM_GetLaunchData(){
    WriteToDebug("In SCORM_GetLaunchData");
    SCORM_ClearErrorInfo();
    return SCORM_CallLMSGetValue("cmi.launch_data");
}

function SCORM_GetComments(){
    WriteToDebug("In SCORM_GetComments");
    SCORM_ClearErrorInfo();
    return SCORM_CallLMSGetValue("cmi.comments");
}

function SCORM_WriteComment(strComment){
    WriteToDebug("In SCORM_WriteComment strComment=" + strComment);
    SCORM_ClearErrorInfo();
    return SCORM_CallLMSSetValue("cmi.comments", strComment);
}

function SCORM_GetLMSComments(){
    WriteToDebug("In SCORM_GetLMSComments");
    SCORM_ClearErrorInfo();
    return SCORM_CallLMSGetValue("cmi.comments_from_lms");
}


function SCORM_GetAudioPlayPreference(){

    var intTempPreference;

    WriteToDebug("In SCORM_GetAudioPlayPreference");

    SCORM_ClearErrorInfo();

    intTempPreference = SCORM_CallLMSGetValue("cmi.student_preference.audio");

    if (intTempPreference == ""){
        intTempPreference = 0;
    }

    intTempPreference = parseInt(intTempPreference, 10);

    WriteToDebug("intTempPreference=" + intTempPreference);

    if (intTempPreference > 0){
        WriteToDebug("Returning On");
        return PREFERENCE_ON;
    }
    else if (intTempPreference == 0){
        WriteToDebug("Returning Default");
        return PREFERENCE_DEFAULT;
    }
    else if (intTempPreference < 0) {
        WriteToDebug("returning Off");
        return PREFERENCE_OFF;
    }
    else{
        WriteToDebug("Error: Invalid preference");
        SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_PREFERENCE,
                                  "Invalid audio preference received from LMS",
                                  "intTempPreference=" + intTempPreference);
        return null;
    }
}



function SCORM_GetAudioVolumePreference(){

    var intTempPreference;

    WriteToDebug("In SCORM_GetAudioVollumePreference");

    SCORM_ClearErrorInfo();

    intTempPreference = SCORM_CallLMSGetValue("cmi.student_preference.audio");

    WriteToDebug("intTempPreference=" + intTempPreference);

    if (intTempPreference == "") {
        intTempPreference = 100;
    }

    intTempPreference = parseInt(intTempPreference, 10);

    if (intTempPreference <= 0){
        WriteToDebug("Setting to 100");
        intTempPreference = 100;
    }

    if (! (intTempPreference > 0 && intTempPreference <= 100)){
        WriteToDebug("ERROR: invalid preference");
        SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_PREFERENCE,
                                  "Invalid audio preference received from LMS",
                                  "intTempPreference=" + intTempPreference);
        return null;
    }

    WriteToDebug("Returning " + intTempPreference);
    return intTempPreference;

}

function SCORM_SetAudioPreference(PlayPreference, intPercentOfMaxVolume){

    WriteToDebug("In SCORM_SetAudioPreference PlayPreference=" + PlayPreference + ", intPercentOfMaxVolume=" + intPercentOfMaxVolume);

    SCORM_ClearErrorInfo();

    if (PlayPreference == PREFERENCE_OFF){
        WriteToDebug("Setting percent to -1 - OFF");
        intPercentOfMaxVolume = -1;
    }

    return SCORM_CallLMSSetValue("cmi.student_preference.audio", intPercentOfMaxVolume);
}



function SCORM_SetLanguagePreference(strLanguage){
    WriteToDebug("In SCORM_SetLanguagePreference strLanguage=" + strLanguage);
    SCORM_ClearErrorInfo();
    return SCORM_CallLMSSetValue("cmi.student_preference.language", strLanguage);

}


function SCORM_GetLanguagePreference(){
    WriteToDebug("In SCORM_GetLanguagePreference");
    SCORM_ClearErrorInfo();
    return SCORM_CallLMSGetValue("cmi.student_preference.language");

}


function SCORM_SetSpeedPreference(intPercentOfMax){

    var intSCORMSpeed;  //SCORM's scale is -100 to +100, our range is 1 to 100

    WriteToDebug("In SCORM_SetSpeedPreference intPercentOfMax=" + intPercentOfMax);

    SCORM_ClearErrorInfo();

    intSCORMSpeed = (intPercentOfMax * 2) - 100;

    WriteToDebug("intSCORMSpeed=" + intSCORMSpeed);

    return SCORM_CallLMSSetValue("cmi.student_preference.speed", intSCORMSpeed);

}


function SCORM_GetSpeedPreference(){

    var intSCORMSpeed;
    var intPercentOfMax;

    WriteToDebug("In SCORM_GetSpeedPreference");

    SCORM_ClearErrorInfo();

    intSCORMSpeed = SCORM_CallLMSGetValue("cmi.student_preference.speed");

    WriteToDebug("intSCORMSpeed=" + intSCORMSpeed);

    if (intSCORMSpeed == ""){
        WriteToDebug("Detected empty string, defaulting to 100");
        intSCORMSpeed = 100;
    }

    if ( ! ValidInteger(intSCORMSpeed) ){
        WriteToDebug("ERROR - invalid integer");
        SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_SPEED,
                                  "Invalid speed preference received from LMS - not an integer",
                                  "intSCORMSpeed=" + intSCORMSpeed);
        return null;
    }

    intSCORMSpeed = parseInt(intSCORMSpeed, 10);

    if (intSCORMSpeed < -100 || intSCORMSpeed > 100){
        WriteToDebug("ERROR - out of range");
        SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_SPEED,
                                  "Invalid speed preference received from LMS - out of range",
                                  "intSCORMSpeed=" + intSCORMSpeed);
        return null;
    }

    intPercentOfMax = (intSCORMSpeed + 100) / 2;
    intPercentOfMax = parseInt(intPercentOfMax, 10);

    WriteToDebug("Returning " + intPercentOfMax);

    return intPercentOfMax;

}




function SCORM_SetTextPreference(intPreference){
    WriteToDebug("In SCORM_SetTextPreference intPreference=" + intPreference);

    SCORM_ClearErrorInfo();

    return SCORM_CallLMSSetValue("cmi.student_preference.text", intPreference);
}


function SCORM_GetTextPreference(){
    var intTempPreference;

    WriteToDebug("In SCORM_GetTextPreference");

    SCORM_ClearErrorInfo();

    intTempPreference = SCORM_CallLMSGetValue("cmi.student_preference.text");
    intTempPreference = parseInt(intTempPreference, 10);

    WriteToDebug("intTempPreference=" + intTempPreference);

    if (intTempPreference > 0){
        WriteToDebug("Returning On");
        return PREFERENCE_ON;
    }
    else if (intTempPreference == 0 || intTempPreference == ""){
        WriteToDebug("Returning Default");
        return PREFERENCE_DEFAULT;
    }
    else if (intTempPreference < 0) {
        WriteToDebug("returning Off");
        return PREFERENCE_OFF;
    }
    else{
        WriteToDebug("Error: Invalid preference");
        SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_PREFERENCE,
                                  "Invalid text preference received from LMS",
                                  "intTempPreference=" + intTempPreference);
        return null;
    }
}



//---------------------------------------------------------------------------------
//Time Management Functions
function SCORM_GetPreviouslyAccumulatedTime(){

    var strCMITime;
    var intMilliseconds;

    WriteToDebug("In SCORM_GetPreviouslyAccumulatedTime");

    SCORM_ClearErrorInfo();

    strCMITime = SCORM_CallLMSGetValue("cmi.core.total_time")

    WriteToDebug("strCMITime=" + strCMITime);

    if (! IsValidCMITimeSpan(strCMITime)){
        WriteToDebug("ERROR - Invalid CMITimeSpan");
        SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_TIMESPAN,
                                    "Invalid timespan received from LMS",
                                    "strTime=" + strCMITime);
        return null;
    }

    intMilliseconds = ConvertCMITimeSpanToMS(strCMITime);

    WriteToDebug("Returning " + intMilliseconds);

    return intMilliseconds;

}


function SCORM_SaveTime(intMilliSeconds){

    var strCMITime;

    WriteToDebug("In SCORM_SaveTime intMilliSeconds=" + intMilliSeconds);

    SCORM_ClearErrorInfo();

    strCMITime = ConvertMilliSecondsToSCORMTime(intMilliSeconds, true);

    WriteToDebug("strCMITime=" + strCMITime);

    return SCORM_CallLMSSetValue("cmi.core.session_time", strCMITime);
}

function SCORM_GetMaxTimeAllowed(){

    var strCMITime;
    var intMilliseconds;

    WriteToDebug("In SCORM_GetMaxTimeAllowed");

    SCORM_ClearErrorInfo();

    strCMITime = SCORM_CallLMSGetValue("cmi.student_data.max_time_allowed")

    WriteToDebug("strCMITime=" + strCMITime);

    if (strCMITime == ""){
        strCMITime = "9999:99:99.99";
    }

    if (! IsValidCMITimeSpan(strCMITime)){
        WriteToDebug("ERROR - Invalid CMITimeSpan");
        SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_TIMESPAN,
                                    "Invalid timespan received from LMS",
                                    "strTime=" + strCMITime);
        return null;
    }

    intMilliseconds = ConvertCMITimeSpanToMS(strCMITime);

    WriteToDebug("intMilliseconds=" + intMilliseconds);

    return intMilliseconds;
}



function SCORM_DisplayMessageOnTimeout(){

    var strTLA;

    SCORM_ClearErrorInfo();

    WriteToDebug("In SCORM_DisplayMessageOnTimeout");

    strTLA = SCORM_CallLMSGetValue("cmi.student_data.time_limit_action");

    WriteToDebug("strTLA=" + strTLA);

    if (strTLA == SCORM_TLA_EXIT_MESSAGE || strTLA == SCORM_TLA_CONTINUE_MESSAGE){
        WriteToDebug("returning true");
        return true;
    }
    else if(strTLA == SCORM_TLA_EXIT_NO_MESSAGE || strTLA == SCORM_TLA_CONTINUE_NO_MESSAGE || strTLA == ""){
        WriteToDebug("returning false");
        return false;
    }
    else{
        WriteToDebug("Error invalid TLA");
        SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_TIME_LIMIT_ACTION,
                                  "Invalid time limit action received from LMS",
                                  "strTLA=" + strTLA);

        return null;
    }

}

function SCORM_ExitOnTimeout(){

    var strTLA;

    WriteToDebug("In SCORM_ExitOnTimeout");

    SCORM_ClearErrorInfo();

    strTLA = SCORM_CallLMSGetValue("cmi.student_data.time_limit_action");

    WriteToDebug("strTLA=" + strTLA);

    if (strTLA == SCORM_TLA_EXIT_MESSAGE || strTLA == SCORM_TLA_EXIT_NO_MESSAGE){
        WriteToDebug("returning true");
        return true;
    }
    else if(strTLA == SCORM_TLA_CONTINUE_MESSAGE || strTLA == SCORM_TLA_CONTINUE_NO_MESSAGE || strTLA == ""){
        WriteToDebug("returning false");
        return false;
    }
    else{
        WriteToDebug("ERROR invalid TLA");
        SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_TIME_LIMIT_ACTION,
                                  "Invalid time limit action received from LMS",
                                  "strTLA=" + strTLA);

        return null;
    }

}


function SCORM_GetPassingScore(){
    var fltScore;

    WriteToDebug("In SCORM_GetPassingScore");

    SCORM_ClearErrorInfo();

    fltScore = SCORM_CallLMSGetValue("cmi.student_data.mastery_score")

    WriteToDebug("fltScore=" + fltScore);

    if (fltScore == ""){
        fltScore = 0;
    }

    if ( ! IsValidDecimal(fltScore)){
        WriteToDebug("Error - score is not a valid decimal");
        SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_DECIMAL,
                                  "Invalid mastery score received from LMS",
                                  "fltScore=" + fltScore);
        return null;
    }

    fltScore = parseFloat(fltScore);

    WriteToDebug("returning fltScore");

    return fltScore;
}


function SCORM_SetScore(intScore, intMaxScore, intMinScore){

    var blnResult;
    intScore = RoundToPrecision(intScore, 7);
    intMaxScore = RoundToPrecision(intMaxScore, 7);
    intMinScore = RoundToPrecision(intMinScore, 7);

    WriteToDebug("In SCORM_SetScore intScore=" + intScore + ", intMaxScore=" + intMaxScore + ", intMinScore=" + intMinScore);

    SCORM_ClearErrorInfo();

    blnResult = SCORM_CallLMSSetValue("cmi.core.score.raw", intScore);
    blnResult = SCORM_CallLMSSetValue("cmi.core.score.max", intMaxScore) && blnResult;
    blnResult = SCORM_CallLMSSetValue("cmi.core.score.min", intMinScore) && blnResult;

    WriteToDebug("Returning " + blnResult);

    return blnResult;
}

function SCORM_GetScore(){


    WriteToDebug("In SCORM_GetScore");

    SCORM_ClearErrorInfo();

    return SCORM_CallLMSGetValue("cmi.core.score.raw");

}

function SCORM_SetPointBasedScore(intScore, intMaxScore, intMinScore){
    WriteToDebug("SCORM_SetPointBasedScore - SCORM 1.1 and 1.2 do not support SetPointBasedScore, falling back to SetScore");
    return SCORM_SetScore(intScore, intMaxScore, intMinScore);
}

function SCORM_GetScaledScore(intScore, intMaxScore, intMinScore){
    WriteToDebug("SCORM_GetScaledScore - SCORM 1.1 and 1.2 do not support GetScaledScore, returning false");
    return false;
}

function SCORM_RecordInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription,
                                intWeighting, intLatency, strLearningObjectiveID, dtmTime, scormInteractionType,
                                strAlternateResponse, strAlternateCorrectResponse){

    var blnResult;
    var blnTempResult;
    var intInteractionIndex;
    var strResult;


    SCORM_ClearErrorInfo();

    //In SCORM 1.2, we always want to add a new interaction rather than updating an old interaction.
    //This is because some LMS vendors have mis-interpreted the "write only" stipulation on interactions to mean "write once"
    intInteractionIndex = SCORM_CallLMSGetValue("cmi.interactions._count");

    WriteToDebug("intInteractionIndex=" + intInteractionIndex);

    if (intInteractionIndex == ""){
        WriteToDebug("Setting Interaction Index to 0");
        intInteractionIndex = 0;
    }

    //need to leave support for blnCorrect=t/f for legacy implementations of RSECA
    if (IsNumeric(blnCorrect)) {
    	strResult = blnCorrect;
    }
    else {
		if (blnCorrect == true || blnCorrect == INTERACTION_RESULT_CORRECT){
			strResult = SCORM_RESULT_CORRECT;
		}
		else if (blnCorrect == "" || blnCorrect == "false" || blnCorrect == INTERACTION_RESULT_WRONG){  //compare against the string "false" because ("" == false) evaluates to true
			strResult = SCORM_RESULT_WRONG;
		}
		else if (blnCorrect == INTERACTION_RESULT_UNANTICIPATED){
			strResult = SCORM_RESULT_UNANTICIPATED;
		}
		else if (blnCorrect == INTERACTION_RESULT_NEUTRAL){
			strResult = SCORM_RESULT_NEUTRAL;
		}
	}

    WriteToDebug("strResult=" + strResult);

    blnResult = SCORM_CallLMSSetValue("cmi.interactions." + intInteractionIndex + ".id", strID);
    blnResult = SCORM_CallLMSSetValue("cmi.interactions." + intInteractionIndex + ".type", scormInteractionType) && blnResult;


    //try to save the data using the verbose description of the interaciton results (this is not strictly conformant, but most LMS's will allow it and it still passes the Test Suite
    //if the long version errs, try again with the short version (strict adherence to the standard) - this applies to response and correct response

    if (strResponse !== null) {
        blnTempResult = SCORM_CallLMSSetValue("cmi.interactions." + intInteractionIndex + ".student_response", strResponse);

        if (blnTempResult == false && strAlternateResponse !== null){
            blnTempResult = SCORM_CallLMSSetValue("cmi.interactions." + intInteractionIndex + ".student_response", strAlternateResponse);
        }
    }

    blnResult = blnResult && blnTempResult;

    if (strCorrectResponse != undefined && strCorrectResponse != null && strCorrectResponse != ""){

        blnTempResult = SCORM_CallLMSSetValue("cmi.interactions." + intInteractionIndex + ".correct_responses.0.pattern", strCorrectResponse);
        if (blnTempResult == false){
            blnTempResult = SCORM_CallLMSSetValue("cmi.interactions." + intInteractionIndex + ".correct_responses.0.pattern", strAlternateCorrectResponse);
        }

        blnResult = blnResult && blnTempResult;
    }

    if (strResult != undefined && strResult != null && strResult != ""){
        blnResult = SCORM_CallLMSSetValue("cmi.interactions." + intInteractionIndex + ".result", strResult) && blnResult;
    }

    //ignore the description parameter in SCORM 1.2, there is nothing we can do with it

    if (intWeighting != undefined && intWeighting != null && intWeighting != ""){
        blnResult = SCORM_CallLMSSetValue("cmi.interactions." + intInteractionIndex + ".weighting", intWeighting) && blnResult;
    }

    if (intLatency != undefined && intLatency != null && intLatency != ""){
        blnResult = SCORM_CallLMSSetValue("cmi.interactions." + intInteractionIndex + ".latency", ConvertMilliSecondsToSCORMTime(intLatency, true)) && blnResult;
    }

    if (strLearningObjectiveID != undefined && strLearningObjectiveID != null && strLearningObjectiveID != ""){
        blnResult = SCORM_CallLMSSetValue("cmi.interactions." + intInteractionIndex + ".objectives.0.id", strLearningObjectiveID) && blnResult;
    }

    blnResult = SCORM_CallLMSSetValue("cmi.interactions." + intInteractionIndex + ".time", ConvertDateToCMITime(dtmTime)) && blnResult;

    WriteToDebug("Returning " + blnResult);

    return blnResult;
}

function SCORM_RecordTrueFalseInteraction(strID, blnResponse, blnCorrect, blnCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In SCORM_RecordTrueFalseInteraction strID=" + strID + ", strResponse=" + strResponse +
                    ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);

    var strResponse = null;
    var strCorrectResponse = null;

    if (blnResponse == true){
        strResponse = "t";
    }
    else if (blnResponse !== null) {
        strResponse = "f";
    }

    if (blnCorrectResponse == true){
        strCorrectResponse = "t";
    }
    else if(blnCorrectResponse == false){       //test for false b/c it could be null in which case we want to leave it as `null`
        strCorrectResponse = "f";
    }

    return SCORM_RecordInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription,
                                   intWeighting, intLatency, strLearningObjectiveID, dtmTime, SCORM_INTERACTION_TYPE_TRUE_FALSE,
                                   strResponse, strCorrectResponse);
}

function SCORM_RecordMultipleChoiceInteraction(strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In SCORM_RecordMultipleChoiceInteraction strID=" + strID + ", aryResponse=" + aryResponse +
                    ", blnCorrect=" + blnCorrect + ", aryCorrectResponse=" + aryCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);


    var strResponse = null;
    var strResponseLong = null;

    var strCorrectResponse = "";
    var strCorrectResponseLong = "";

    if (aryResponse !== null) {
        strResponse = "";
        strResponseLong = "";

        for (var i=0; i < aryResponse.length; i++){

            if (strResponse.length > 0) {strResponse += ",";}
            if (strResponseLong.length > 0) {strResponseLong += ",";}

            strResponse += aryResponse[i].Short;
            strResponseLong += aryResponse[i].Long;
        }
    }

    for (var i=0; i < aryCorrectResponse.length; i++){

        if (strCorrectResponse.length > 0) {strCorrectResponse += ",";}
        if (strCorrectResponseLong.length > 0) {strCorrectResponseLong += ",";}

        strCorrectResponse += aryCorrectResponse[i].Short;
        strCorrectResponseLong += aryCorrectResponse[i].Long;
    }

    var blnSuccessfullySaved;

    blnSuccessfullySaved = SCORM_RecordInteraction(strID, strResponseLong, blnCorrect, strCorrectResponseLong, strDescription,
                                                   intWeighting, intLatency, strLearningObjectiveID, dtmTime, SCORM_INTERACTION_TYPE_CHOICE,
                                                   strResponse, strCorrectResponse);

    return blnSuccessfullySaved;

}


function SCORM_RecordFillInInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In SCORM_RecordFillInInteraction strID=" + strID + ", strResponse=" + strResponse +
                    ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);


    strResponse = new String(strResponse);
    if (strResponse.length > 255){strResponse = strResponse.substr(0, 255);}

    if (strCorrectResponse == null){
        strCorrectResponse = "";
    }

    strCorrectResponse = new String(strCorrectResponse);
    if (strCorrectResponse.length > 255){strCorrectResponse = strCorrectResponse.substr(0, 255);}

    return SCORM_RecordInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription,
                                   intWeighting, intLatency, strLearningObjectiveID, dtmTime, SCORM_INTERACTION_FILL_IN,
                                   strResponse, strCorrectResponse);
}

function SCORM_RecordMatchingInteraction(strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In SCORM_RecordMatchingInteraction strID=" + strID + ", aryResponse=" + aryResponse +
                    ", blnCorrect=" + blnCorrect + ", aryCorrectResponse=" + aryCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);

    var strResponse = null;
    var strResponseLong = null;

    var strCorrectResponse = "";
    var strCorrectResponseLong = "";

    if (aryResponse !== null) {
        strResponse = "";
        strResponseLong = "";

        for (var i=0; i < aryResponse.length; i++){

            if (strResponse.length > 0) {strResponse += ",";}
            if (strResponseLong.length > 0) {strResponseLong += ",";}

            strResponse += aryResponse[i].Source.Short + "." + aryResponse[i].Target.Short;
            strResponseLong += aryResponse[i].Source.Long + "." + aryResponse[i].Target.Long;
        }
    }

    for (var i=0; i < aryCorrectResponse.length; i++){

        if (strCorrectResponse.length > 0) {strCorrectResponse += ",";}
        if (strCorrectResponseLong.length > 0) {strCorrectResponseLong += ",";}

        strCorrectResponse += aryCorrectResponse[i].Source.Short + "." + aryCorrectResponse[i].Target.Short;
        strCorrectResponseLong += aryCorrectResponse[i].Source.Long + "." + aryCorrectResponse[i].Target.Long;
    }

    var blnSuccessfullySaved;

    blnSuccessfullySaved = SCORM_RecordInteraction(strID, strResponseLong, blnCorrect, strCorrectResponseLong, strDescription,
                                                   intWeighting, intLatency, strLearningObjectiveID, dtmTime, SCORM_INTERACTION_TYPE_MATCHING,
                                                   strResponse, strCorrectResponse);


    return blnSuccessfullySaved;

}

function SCORM_RecordPerformanceInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In SCORM_RecordPerformanceInteraction strID=" + strID + ", strResponse=" + strResponse +
                    ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);

    if (strResponse !== null) {
        strResponse = new String(strResponse);
        if (strResponse.length > 255){strResponse = strResponse.substr(0, 255);}
    }

    if (strCorrectResponse == null){
        strCorrectResponse = "";
    }

    strCorrectResponse = new String(strCorrectResponse);
    if (strCorrectResponse.length > 255){strCorrectResponse = strCorrectResponse.substr(0, 255);}

    return SCORM_RecordInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription,
                                   intWeighting, intLatency, strLearningObjectiveID, dtmTime, SCORM_INTERACTION_TYPE_PERFORMANCE,
                                   strResponse, strCorrectResponse);
}

function SCORM_RecordSequencingInteraction(strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In SCORM_RecordSequencingInteraction strID=" + strID + ", aryResponse=" + aryResponse +
                    ", blnCorrect=" + blnCorrect + ", aryCorrectResponse=" + aryCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);


    var strResponse = null;
    var strResponseLong = null;

    var strCorrectResponse = "";
    var strCorrectResponseLong = "";

    if (aryResponse !== null) {
        strResponse = "";
        strResponseLong = "";

        for (var i=0; i < aryResponse.length; i++){

            if (strResponse.length > 0) {strResponse += ",";}
            if (strResponseLong.length > 0) {strResponseLong += ",";}

            strResponse += aryResponse[i].Short;
            strResponseLong += aryResponse[i].Long;
        }
    }

    for (var i=0; i < aryCorrectResponse.length; i++){

        if (strCorrectResponse.length > 0) {strCorrectResponse += ",";}
        if (strCorrectResponseLong.length > 0) {strCorrectResponseLong += ",";}

        strCorrectResponse += aryCorrectResponse[i].Short;
        strCorrectResponseLong += aryCorrectResponse[i].Long;
    }

    var blnSuccessfullySaved;

    blnSuccessfullySaved = SCORM_RecordInteraction(strID, strResponseLong, blnCorrect, strCorrectResponseLong, strDescription,
                                                   intWeighting, intLatency, strLearningObjectiveID, dtmTime, SCORM_INTERACTION_TYPE_SEQUENCING,
                                                   strResponse, strCorrectResponse);


    return blnSuccessfullySaved;

}

function SCORM_RecordLikertInteraction(strID, response, blnCorrect, correctResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In SCORM_RecordLikertInteraction strID=" + strID + ", response=" + response +
                    ", blnCorrect=" + blnCorrect + ", correctResponse=" + correctResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);

    var strResponse = null;
    var strResponseLong = null;

    var strCorrectResponse = "";
    var strCorrectResponseLong = "";

    if (response !== null) {
        strResponse = response.Short;
        strResponseLong = response.Long;
    }

    if (correctResponse != null){
        strCorrectResponse = correctResponse.Short;
        strCorrectResponseLong = correctResponse.Long;
    }

    var blnSuccessfullySaved;

    blnSuccessfullySaved = SCORM_RecordInteraction(strID, strResponseLong, blnCorrect, strCorrectResponseLong, strDescription,
                                                   intWeighting, intLatency, strLearningObjectiveID, dtmTime, SCORM_INTERACTION_TYPE_LIKERT,
                                                   strResponse, strCorrectResponse);

    return blnSuccessfullySaved;
}

function SCORM_RecordNumericInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In SCORM_RecordNumericInteraction strID=" + strID + ", strResponse=" + strResponse +
                    ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);

    if (strCorrectResponse != undefined && strCorrectResponse != null) {
        if (IsValidDecimalRange(strCorrectResponse))
        {
            //we could receive a decimal range correct response from SCORM 2004/Tin Can/Cmi5. If we do we need to convert that
            //to a decimal for use in SCORM 1.2.
            strCorrectResponse = ConvertDecimalRangeToDecimalBasedOnLearnerResponse(strCorrectResponse, strResponse, blnCorrect);
        }

        //SCORM 1.2 does not accept decimal range correct answers, so if it's not a decimal by this point we fail
        if (!IsValidDecimal(strCorrectResponse)){
            WriteToDebug("Returning False - SCORM_RecordNumericInteraction received invalid correct response (not a decimal), strCorrectResponse=" +strCorrectResponse);
            return false;
        }
    }

    return SCORM_RecordInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription,
                                   intWeighting, intLatency, strLearningObjectiveID, dtmTime, SCORM_INTERACTION_TYPE_NUMERIC,
                                   strResponse, strCorrectResponse);
}


function SCORM_GetEntryMode(){
    var strEntry;

    WriteToDebug("In SCORM_GetEntryMode");

    SCORM_ClearErrorInfo();

    strEntry = SCORM_CallLMSGetValue("cmi.core.entry");

    WriteToDebug("strEntry=" + strEntry);

    if (strEntry == SCORM_ENTRY_ABINITIO){
        WriteToDebug("Returning first time");
        return ENTRY_FIRST_TIME;
    }
    else if (strEntry == SCORM_ENTRY_RESUME){
        WriteToDebug("Returning resume");
        return ENTRY_RESUME;
    }
    else if (strEntry == SCORM_ENTRY_NORMAL){
        WriteToDebug("returning normal");
        return ENTRY_REVIEW;
    }
    else{
        WriteToDebug("ERROR - invalide entry mode");
        SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_ENTRY,
                        "Invalid entry vocab received from LMS",
                        "strEntry=" + strEntry);
        return null;
    }

}

function SCORM_GetLessonMode(){

    var strLessonMode;

    WriteToDebug("In SCORM_GetLessonMode");

    SCORM_ClearErrorInfo();

    strLessonMode = SCORM_CallLMSGetValue("cmi.core.lesson_mode");

    WriteToDebug("strLessonMode=" + strLessonMode);

    if (strLessonMode == SCORM_BROWSE){
        WriteToDebug("Returning browse");
        return MODE_BROWSE;
    }
    else if(strLessonMode == SCORM_NORMAL){
        WriteToDebug("returning normal");
        return MODE_NORMAL;
    }
    else if(strLessonMode == SCORM_REVIEW){
        WriteToDebug("Returning Review");
        return MODE_REVIEW;
    }
    else{
        WriteToDebug("ERROR - invalid lesson mode");
        SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_LESSON_MODE,
                                "Invalid lesson_mode vocab received from LMS",
                                "strLessonMode=" + strLessonMode);
        return null;
    }

}

function SCORM_GetTakingForCredit(){

    var strCredit;

    WriteToDebug("In SCORM_GetTakingForCredit");

    SCORM_ClearErrorInfo();

    strCredit = SCORM_CallLMSGetValue("cmi.core.credit");

    WriteToDebug("strCredit=" + strCredit);

    if (strCredit == "credit"){
        WriteToDebug("Returning true");
        return true;
    }
    else if (strCredit == "no-credit"){
        WriteToDebug("Returning false");
        return false;
    }
    else{
        WriteToDebug("ERROR - invalid credit");
        SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_CREDIT,
                                  "Invalid credit vocab received from LMS",
                                  "strCredit=" + strCredit);
        return null;
    }
}



function SCORM_SetObjectiveScore(strObjectiveID, intScore, intMaxScore, intMinScore){

    var intObjectiveIndex;
    var blnResult;

    intScore = RoundToPrecision(intScore, 7);
    intMaxScore = RoundToPrecision(intMaxScore, 7);
    intMinScore = RoundToPrecision(intMinScore, 7);


    WriteToDebug("In SCORM_SetObjectiveScore, strObejctiveID=" + strObjectiveID + ", intScore=" + intScore + ", intMaxScore=" + intMaxScore + ", intMinScore=" + intMinScore);

    SCORM_ClearErrorInfo();

    intObjectiveIndex = SCORM_FindObjectiveIndexFromID(strObjectiveID);

    WriteToDebug("intObjectiveIndex=" + intObjectiveIndex);

    blnResult = SCORM_CallLMSSetValue("cmi.objectives." + intObjectiveIndex + ".id", strObjectiveID);
    blnResult = SCORM_CallLMSSetValue("cmi.objectives." + intObjectiveIndex + ".score.raw", intScore) && blnResult;
    blnResult = SCORM_CallLMSSetValue("cmi.objectives." + intObjectiveIndex + ".score.max", intMaxScore) && blnResult;
    blnResult = SCORM_CallLMSSetValue("cmi.objectives." + intObjectiveIndex + ".score.min", intMinScore) && blnResult;

    WriteToDebug("Returning " + blnResult);

    return blnResult;

}

function SCORM_SetObjectiveDescription(strObjectiveID, strObjectiveDescription){

    var intObjectiveIndex;
    var blnResult;

    WriteToDebug("In SCORM_SetObjectiveDescription, strObjectiveDescription=" + strObjectiveDescription);
    WriteToDebug("Objective Descriptions are not supported prior to SCORM 2004");

    SCORM_ClearErrorInfo();

    blnResult = SCORM_TRUE;

    WriteToDebug("Returning " + blnResult);

    return blnResult;

}



function SCORM_SetObjectiveStatus(strObjectiveID, Lesson_Status){

    var intObjectiveIndex;
    var blnResult;
    var strSCORMStatus = "";

    WriteToDebug("In SCORM_SetObjectiveStatus strObjectiveID=" + strObjectiveID + ", Lesson_Status=" + Lesson_Status);

    SCORM_ClearErrorInfo();

    intObjectiveIndex = SCORM_FindObjectiveIndexFromID(strObjectiveID);

    WriteToDebug("intObjectiveIndex=" + intObjectiveIndex);

    if (Lesson_Status == LESSON_STATUS_PASSED){
        strSCORMStatus = SCORM_PASSED;
    }
    else if (Lesson_Status == LESSON_STATUS_FAILED){
        strSCORMStatus = SCORM_FAILED;
    }
    else if (Lesson_Status == LESSON_STATUS_COMPLETED){
        strSCORMStatus = SCORM_COMPLETED;
    }
    else if (Lesson_Status == LESSON_STATUS_BROWSED){
        strSCORMStatus = SCORM_BROWSED;
    }
    else if (Lesson_Status == LESSON_STATUS_INCOMPLETE){
        strSCORMStatus = SCORM_INCOMPLETE;
    }
    else if (Lesson_Status == LESSON_STATUS_NOT_ATTEMPTED){
        strSCORMStatus = SCORM_NOT_ATTEMPTED;
    }

    WriteToDebug("strSCORMStatus=" + strSCORMStatus);

    blnResult = SCORM_CallLMSSetValue("cmi.objectives." + intObjectiveIndex + ".id", strObjectiveID);
    blnResult = SCORM_CallLMSSetValue("cmi.objectives." + intObjectiveIndex + ".status", strSCORMStatus) && blnResult;

    WriteToDebug("Returning " + blnResult);

    return blnResult;
}



function SCORM_GetObjectiveScore(strObjectiveID){

    var intObjectiveIndex;

    WriteToDebug("In SCORM_GetObjectiveScore, strObejctiveID=" + strObjectiveID);

    SCORM_ClearErrorInfo();

    intObjectiveIndex = SCORM_FindObjectiveIndexFromID(strObjectiveID);

    WriteToDebug("intObjectiveIndex=" + intObjectiveIndex);

    return SCORM_CallLMSGetValue("cmi.objectives." + intObjectiveIndex + ".score.raw");

}


function SCORM_GetObjectiveDescription(strObjectiveID){

   WriteToDebug("In SCORM_GetObjectiveDescription, strObejctiveID=" + strObjectiveID);
   WriteToDebug("ObjectiveDescription is not supported prior to SCORM 2004");
   return "";

}


function SCORM_GetObjectiveStatus(strObjectiveID){

    var intObjectiveIndex;
    var strStatus;

    WriteToDebug("In SCORM_GetObjectiveStatus, strObejctiveID=" + strObjectiveID);

    SCORM_ClearErrorInfo();

    intObjectiveIndex = SCORM_FindObjectiveIndexFromID(strObjectiveID);

    WriteToDebug("intObjectiveIndex=" + intObjectiveIndex);

    strStatus = SCORM_CallLMSGetValue("cmi.objectives." + intObjectiveIndex + ".status");

    if (strStatus == SCORM_PASSED){
        WriteToDebug("returning Passed");
        return LESSON_STATUS_PASSED;
    }
    else if (strStatus == SCORM_FAILED){
        WriteToDebug("Returning Failed");
        return LESSON_STATUS_FAILED;
    }
    else if (strStatus == SCORM_COMPLETED){
        WriteToDebug("Returning Completed");
        return LESSON_STATUS_COMPLETED;
    }
    else if (strStatus == SCORM_BROWSED){
        WriteToDebug("Returning Browsed");
        return LESSON_STATUS_BROWSED;
    }
    else if (strStatus == SCORM_INCOMPLETE){
        WriteToDebug("Returning Incomplete");
        return LESSON_STATUS_INCOMPLETE;
    }
    else if (strStatus == SCORM_NOT_ATTEMPTED || strStatus == ""){
        WriteToDebug("Returning Not Attempted");
        return LESSON_STATUS_NOT_ATTEMPTED;
    }
    else{
        WriteToDebug("ERROR - status not found");
        SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_STATUS,
                                "Invalid objective status received from LMS or initial status not yet recorded for objective",
                                "strStatus=" + strStatus);
        return null;

    }

}


function SCORM_FindObjectiveIndexFromID(strObjectiveID){

    var intCount;
    var i;
    var strTempID;

    WriteToDebug("In SCORM_FindObjectiveIndexFromID");

    intCount = SCORM_CallLMSGetValue("cmi.objectives._count");

    if (intCount == ""){
        WriteToDebug("Setting intCount=0");
        return 0;
    }

    intCount = parseInt(intCount, 10);

    WriteToDebug("intCount=" + intCount);

    for (i=0; i<intCount; i++){

        WriteToDebug("Checking index " + i);

        strTempID = SCORM_CallLMSGetValue("cmi.objectives." + i + ".id");

        WriteToDebug("ID=" + strTempID);

        if (strTempID == strObjectiveID){
            WriteToDebug("Found Matching index");
            return i;
        }
    }

    WriteToDebug("Did not find match, returning count");

    return intCount;
}

function SCORM_CreateValidIdentifier(str) {
    return CreateValidIdentifierLegacy(str);
}

//___________________________________________________________
//Interaction Retrieval Functionality
//NOTE ON INTERACTION RETRIEVAL
//A.  It is only available in certain standards, standards where it is unavailable will return nothing
//B.  The interaction records are currently reported using "journaling", whereby each entry is appended
//      Retrieval methods will retrieve only the most recent value


//___________________________________________________________
//Helper Methods
function SCORM_FindInteractionIndexFromID(strInteractionID){

    WriteToDebug("SCORM_FindInteractionIndexFromID - SCORM does not support interaction retrieval, returning null");

    return null;
}
//___________________________________________________________


function SCORM_GetInteractionType(strInteractionID)
{

    WriteToDebug("SCORM_GetInteractionType - SCORM does not support interaction retrieval, returning empty string");
    return '';


}

//public
function SCORM_GetInteractionTimestamp(strInteractionID)
{
    WriteToDebug("SCORM_GetInteractionTimestamp - SCORM does not support interaction retrieval, returning empty string");
    return '';
}



//public
function SCORM_GetInteractionCorrectResponses(strInteractionID)
{

    WriteToDebug("SCORM_GetInteractionCorrectResponses - SCORM does not support interaction retrieval, returning empty array");
    return new Array();


}



//public
function SCORM_GetInteractionWeighting(strInteractionID)
{
    WriteToDebug("SCORM_GetInteractionWeighting - SCORM does not support interaction retrieval, returning empty string");
    return '';

}



//public
function SCORM_GetInteractionLearnerResponses(strInteractionID)
{
    WriteToDebug("SCORM_GetInteractionLearnerResponses - SCORM does not support interaction retrieval, returning empty array");
    return new Array();


}


//public
function SCORM_GetInteractionResult(strInteractionID)
{
    WriteToDebug("SCORM_GetInteractionResult - SCORM does not support interaction retrieval, returning empty string");
    return '';

}



//public
function SCORM_GetInteractionLatency(strInteractionID)
{
    WriteToDebug("SCORM_GetInteractionDescription - SCORM does not support interaction retrieval, returning empty string");
    return '';

}



//public
function SCORM_GetInteractionDescription(strInteractionID)
{
    WriteToDebug("SCORM_GetInteractionDescription - SCORM does not support interaction retrieval, returning empty string");
    return '';


}

//________________________________________________


//public
function SCORM_CreateDataBucket(strBucketId, intMinSize, intMaxSize){
    WriteToDebug("SCORM_CreateDataBucket - SCORM 1.1 and 1.2 do not support SSP, returning false");
    return false;
}

//public
function SCORM_GetDataFromBucket(strBucketId){
    WriteToDebug("SCORM_GetDataFromBucket - SCORM 1.1 and 1.2 do not support SSP, returning empty string");
    return "";
}

//public
function SCORM_PutDataInBucket(strBucketId, strData, blnAppendToEnd){
    WriteToDebug("SCORM_PutDataInBucket - SCORM 1.1 and 1.2 do not support SSP, returning false");
    return false;
}

//public
function SCORM_DetectSSPSupport(){
    WriteToDebug("SCORM_DetectSSPSupport - SCORM 1.1 and 1.2 do not support SSP, returning false");
    return false;
}

//public
function SCORM_GetBucketInfo(strBucketId){
    WriteToDebug("AICC_DetectSSPSupport - SCORM 1.1 and 1.2 do not support SSP, returning empty SSPBucketSize");
    return new SSPBucketSize(0, 0);
}

//---------------------------------------------------------------------------------
//Status Management Functions

function SCORM_SetFailed(){
    WriteToDebug("In SCORM_SetFailed");
    SCORM_ClearErrorInfo();
    return SCORM_CallLMSSetValue("cmi.core.lesson_status", SCORM_FAILED);
}

function SCORM_SetPassed(){
    WriteToDebug("In SCORM_SetPassed");
    SCORM_ClearErrorInfo();
    return SCORM_CallLMSSetValue("cmi.core.lesson_status", SCORM_PASSED);
}

function SCORM_SetCompleted(){
    WriteToDebug("In SCORM_SetCompleted");
    SCORM_ClearErrorInfo();
    return SCORM_CallLMSSetValue("cmi.core.lesson_status", SCORM_COMPLETED);
}

function SCORM_ResetStatus(){
    WriteToDebug("In SCORM_ResetStatus");
    SCORM_ClearErrorInfo();
    return SCORM_CallLMSSetValue("cmi.core.lesson_status", SCORM_INCOMPLETE);
}

function SCORM_GetStatus(){

    var strStatus;

    WriteToDebug("In SCORM_GetStatus");

    SCORM_ClearErrorInfo();

    strStatus = SCORM_CallLMSGetValue("cmi.core.lesson_status");

    WriteToDebug("strStatus=" + strStatus);

    if (strStatus == SCORM_PASSED){
        WriteToDebug("returning Passed");
        return LESSON_STATUS_PASSED;
    }
    else if (strStatus == SCORM_FAILED){
        WriteToDebug("Returning Failed");
        return LESSON_STATUS_FAILED;
    }
    else if (strStatus == SCORM_COMPLETED){
        WriteToDebug("Returning Completed");
        return LESSON_STATUS_COMPLETED;
    }
    else if (strStatus == SCORM_BROWSED){
        WriteToDebug("Returning Browsed");
        return LESSON_STATUS_BROWSED;
    }
    else if (strStatus == SCORM_INCOMPLETE){
        WriteToDebug("Returning Incomplete");
        return LESSON_STATUS_INCOMPLETE;
    }
    else if (strStatus == SCORM_NOT_ATTEMPTED){
        WriteToDebug("Returning Not Attempted");
        return LESSON_STATUS_NOT_ATTEMPTED;
    }
    else{
        WriteToDebug("ERROR - status not found");
        SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_STATUS,
                                "Invalid lesson status received from LMS",
                                "strStatus=" + strStatus);
        return null;

    }

}

//public
function SCORM_GetProgressMeasure(){
    WriteToDebug("SCORM_GetProgressMeasure - SCORM 1.1 and 1.2 do not support progress_measure, returning false");
    return false;
}
//public
function SCORM_SetProgressMeasure(){
    WriteToDebug("SCORM_SetProgressMeasure - SCORM 1.1 and 1.2 do not support progress_measure, returning false");
    return false;
}

//public
function SCORM_GetObjectiveProgressMeasure(){
    WriteToDebug("SCORM_GetObjectiveProgressMeasure - SCORM 1.1 and 1.2 do not support progress_measure, returning false");
    return false;
}
//public
function SCORM_SetObjectiveProgressMeasure(){
    WriteToDebug("SCORM_SetObjectiveProgressMeasure - SCORM 1.1 and 1.2 do not support progress_measure, returning false");
    return false;
}


function SCORM_IsContentInBrowseMode(){

    var strLessonMode

    WriteToDebug("In SCORM_IsContentInBrowseMode");

    strLessonMode = SCORM_CallLMSGetValue("cmi.core.lesson_mode");

    WriteToDebug("SCORM_IsContentInBrowseMode,  strLessonMode=" + strLessonMode);

    if (strLessonMode == SCORM_BROWSE){
        WriteToDebug("Returning true");
        return true;
    }
    else{
        WriteToDebug("Returning false");
        return false;
    }
}


function SCORM_TranslateExitTypeToSCORM(strExitType){

    WriteToDebug("In SCORM_TranslatgeExitTypeToSCORM strExitType-" + strExitType);

    if (strExitType == EXIT_TYPE_SUSPEND){
        WriteToDebug("Returning suspend");
        return SCORM_SUSPEND;
    }
    else if (strExitType == EXIT_TYPE_UNLOAD){
        WriteToDebug("Returning Exit");
        return SCORM_NORMAL_EXIT;
    }
    else if (strExitType == EXIT_TYPE_FINISH){
        WriteToDebug("Returning Logout");
        return SCORM_NORMAL_EXIT;
    }
    else if (strExitType == EXIT_TYPE_TIMEOUT){
        WriteToDebug("Returning Timout");
        return SCORM_TIMEOUT;
    }
}


function SCORM_GetCompletionStatus(){

    WriteToDebug("In SCORM_GetCompletionStatus");

    if (SCORM_IsContentInBrowseMode()){
        WriteToDebug("Returning browsed");
        return SCORM_BROWSED;
    }
    else{
        WriteToDebug("Returning Completed");
        return SCORM_COMPLETED;
    }

}


function SCORM_SetNavigationRequest(strNavRequest){
    WriteToDebug("SCORM_GetNavigationRequest - SCORM 1.1 and 1.2 do not support navigation requests, returning false");
    return false;
}

function SCORM_GetNavigationRequest(){
    WriteToDebug("SCORM_GetNavigationRequest - SCORM 1.1 and 1.2 do not support navigation requests, returning false");
    return false;
}

//---------------------------------------------------------------------------------
//Functions to Call the SCORM API

//note: in all functions that interact with API, we concact any returned strings with "" to convert
//the value to a string type, do this b/c many SCORM API's will return a Java String instead of a Javascript
//string


function SCORM_CallLMSInitialize(){

    var strResult;

    WriteToDebug("In SCORM_CallLMSInitialize");

    SCORM_objAPI = SCORM_GrabAPI();

    WriteToDebug("Calling LMSInitialize");

    strResult = SCORM_objAPI.LMSInitialize("");
    strResult = strResult + "";

    WriteToDebug("strResult=" + strResult);

    if (strResult == SCORM_FALSE){

        WriteToDebug("Detected failed call to initialize");

        SCORM_SetErrorInfo();

        WriteToDebug ("Error calling LMSInitialize:");
        WriteToDebug ("              intSCORMError=" + intSCORMError);
        WriteToDebug ("              SCORMErrorString=" + strSCORMErrorString);
        WriteToDebug ("              Diagnostic=" + strSCORMErrorDiagnostic);

        return false;

    }

    WriteToDebug("Returning true");

    return true;
}

function SCORM_CallLMSSetValue(strElement, strValue){

    var strResult;

    WriteToDebug("SCORM_CallLMSSetValue strElement=" + strElement + ", strValue=" + strValue);

    if (blnReviewModeSoReadOnly === true){
        WriteToDebug("Mode is Review and configuration setting dictates this should be read only so exiting.");
        return true;
    }

    SCORM_objAPI = SCORM_GrabAPI();

    WriteToDebug("Calling LMSSetValue");

    strElement = strElement + "";
    strValue = strValue + "";

    strResult = SCORM_objAPI.LMSSetValue(strElement, strValue)
    strResult = strResult + "";

    WriteToDebug("strResult=" + strResult);

    if (strResult == SCORM_FALSE){

        WriteToDebug("Detected Failed call to LMSSetvalue");

        SCORM_SetErrorInfo();

        WriteToDebug ("Error calling LMSSetValue:");
        WriteToDebug ("              strElement=" + strElement);
        WriteToDebug ("              strValue=" + strValue);
        WriteToDebug ("              intSCORMError=" + intSCORMError);
        WriteToDebug ("              SCORMErrorString=" + strSCORMErrorString);
        WriteToDebug ("              Diagnostic=" + strSCORMErrorDiagnostic);

        return false;
    }

    WriteToDebug("Returning true");

    return true;
}

function SCORM_CallLMSGetValue(strElement){

    var strResult;

    WriteToDebug("In SCORM_CallLMSGetValue strElement=" + strElement);

    SCORM_objAPI = SCORM_GrabAPI();

    WriteToDebug("Call LMSGetValue");

    strElement = strElement + "";

    strResult = SCORM_objAPI.LMSGetValue(strElement) + ""

    WriteToDebug("strResult=" + strResult);

    intSCORMError = SCORM_objAPI.LMSGetLastError()
    intSCORMError = intSCORMError + "";

    WriteToDebug("intSCORMError=" + intSCORMError);

    if (intSCORMError != SCORM_NO_ERROR){

        WriteToDebug("Detected failed called to LMSGetValue");

        SCORM_SetErrorInfo();

        WriteToDebug ("Error calling LMSGetValue:");
        WriteToDebug ("              strElement=" + strElement);
        WriteToDebug ("              intSCORMError=" + intSCORMError);
        WriteToDebug ("              SCORMErrorString=" + strSCORMErrorString);
        WriteToDebug ("              Diagnostic=" + strSCORMErrorDiagnostic);
    }

    WriteToDebug("Returning " + strResult);

    return strResult;

}


function SCORM_CallLMSCommit(){

    var strResult;

    WriteToDebug("In SCORM_CallLMSCommit");

    SCORM_objAPI = SCORM_GrabAPI();

    WriteToDebug("Calling LMSCommit");

    strResult = SCORM_objAPI.LMSCommit("");

    strResult = strResult + "";

    WriteToDebug("strResult=" + strResult);

    if (strResult == SCORM_FALSE){

        WriteToDebug("Detected failed call to LMSCommit");

        SCORM_SetErrorInfo();

        WriteToDebug ("Error calling LMSCommit:");
        WriteToDebug ("              intSCORMError=" + intSCORMError);
        WriteToDebug ("              SCORMErrorString=" + strSCORMErrorString);
        WriteToDebug ("              Diagnostic=" + strSCORMErrorDiagnostic);

        return false;
    }

    WriteToDebug("Returning true");

    return true;
}


function SCORM_CallLMSFinish(){

    var strResult;

    WriteToDebug("In SCORM_CallLMSFinish");

    SCORM_objAPI = SCORM_GrabAPI();

    WriteToDebug("Calling LMS Finish");

    strResult = SCORM_objAPI.LMSFinish("");

    strResult = strResult + "";

    WriteToDebug("strResult=" + strResult);

    if (strResult == SCORM_FALSE){

        WriteToDebug("Detected failed call to LMSFinish");

        SCORM_SetErrorInfo();

        WriteToDebug ("Error calling LMSFinish:");
        WriteToDebug ("              intSCORMError=" + intSCORMError);
        WriteToDebug ("              SCORMErrorString=" + strSCORMErrorString);
        WriteToDebug ("              Diagnostic=" + strSCORMErrorDiagnostic);

        return false;
    }

    WriteToDebug("Returning True");

    return true;
}


//---------------------------------------------------------------------------------
//Error Handling Functions
function SCORM_ClearErrorInfo(){

    WriteToDebug("In SCORM_ClearErrorInfo");

    intSCORMError = SCORM_NO_ERROR;
    strSCORMErrorString = "";
    strSCORMErrorDiagnostic = "";
}

function SCORM_SetErrorInfo(){

    WriteToDebug("In SCORM_SetErrorInfo");

    intSCORMError = SCORM_objAPI.LMSGetLastError();
    strSCORMErrorString = SCORM_objAPI.LMSGetErrorString(intSCORMError);
    strSCORMErrorDiagnostic = SCORM_objAPI.LMSGetDiagnostic("");

    intSCORMError = intSCORMError + "";
    strSCORMErrorString = strSCORMErrorString + "";
    strSCORMErrorDiagnostic = strSCORMErrorDiagnostic + "";

    WriteToDebug("intSCORMError=" + intSCORMError);
    WriteToDebug("strSCORMErrorString=" + strSCORMErrorString);
    WriteToDebug("strSCORMErrorDiagnostic=" + strSCORMErrorDiagnostic);
}

function SCORM_SetErrorInfoManually(intNum, strString, strDiagnostic){

        WriteToDebug("In SCORM_SetErrorInfoManually");
        WriteToDebug("ERROR-Num=" + intNum);
        WriteToDebug("      String=" + strString);
        WriteToDebug("      Diag=" + strDiagnostic);

        intSCORMError = intNum;
        strSCORMErrorString = strString;
        strSCORMErrorDiagnostic = strDiagnostic;

}

function SCORM_GetLastError(){

    WriteToDebug("In SCORM_GetLastError");

    if (intSCORMError == SCORM_NO_ERROR){
        WriteToDebug("Returning No Error");
        return NO_ERROR;
    }
    else {
        WriteToDebug("Returning " + intSCORMError);
        return intSCORMError;
    }
}

function SCORM_GetLastErrorDesc(){
    WriteToDebug("In SCORM_GetLastErrorDesc, " + strSCORMErrorString + "\n" + strSCORMErrorDiagnostic);
    return strSCORMErrorString + "\n" + strSCORMErrorDiagnostic;
}



//---------------------------------------------------------------------------------
//API Locating Functions

function SCORM_GrabAPI(){

    WriteToDebug("In SCORM_GrabAPI");

    //if we haven't already located the API, find it using our improved ADL algorithm
    if (typeof(SCORM_objAPI) == "undefined" || SCORM_objAPI == null){
        WriteToDebug("Searching with improved ADL algorithm");
        SCORM_objAPI = SCORM_GetAPI();
    }

    //if it's still not found, look in every concievable spot...some older LMS's bury it in wierd places
    //drop this because it can cause problems when the content is launched in a cross domain envrionment...for instance the
    //standard detection algorithm could come upon a frame from a different domain using this algorithm when the content is
    //launched under AICC

    //TODO: a better solution might be to wrap this in a try/catch block

    //if (typeof(SCORM_objAPI) == "undefined" || SCORM_objAPI == null){
    //  WriteToDebug("Searching everywhere with Rustici Software algorithm");
    //  SCORM_objAPI = SCORM_SearchForAPI(window);
    //}

    if (typeof(SCORM_objAPI) == "undefined" || SCORM_objAPI == null){
        SCORM_objAPI = SCORM_SearchForAPI(window);
    }

    WriteToDebug("SCORM_GrabAPI, returning");

    return SCORM_objAPI;

}


function SCORM_SearchForAPI(wndLookIn){

    WriteToDebug("SCORM_SearchForAPI");

    var objAPITemp = null;
    var strDebugID = "";

    strDebugID = "Name=" + wndLookIn.name + ", href=" + wndLookIn.location.href

    objAPITemp = wndLookIn.API;

    if (SCORM_APIFound(objAPITemp)){
        WriteToDebug("Found API in this window - "  + strDebugID);
        return objAPITemp;
    }

    if (SCORM_WindowHasParent(wndLookIn)){
        WriteToDebug("Searching Parent - "  + strDebugID);
        objAPITemp = SCORM_SearchForAPI(wndLookIn.parent);
    }

    if (SCORM_APIFound(objAPITemp)){
        WriteToDebug("Found API in a parent - "  + strDebugID);
        return objAPITemp;
    }

    if (SCORM_WindowHasOpener(wndLookIn)){
        WriteToDebug("Searching Opener - "  + strDebugID);
        objAPITemp = SCORM_SearchForAPI(wndLookIn.opener);
    }

    if (SCORM_APIFound(objAPITemp)){
        WriteToDebug("Found API in an opener - "  + strDebugID);
        return objAPITemp;
    }

    //look in child frames individually, don't call this function recursively
    //on them to prevent an infinite loop when it looks back up to the parents
    WriteToDebug("Looking in children - "  + strDebugID);
    objAPITemp = SCORM_LookInChildren(wndLookIn);

    if (SCORM_APIFound(objAPITemp)){
        WriteToDebug("Found API in Children - "  + strDebugID);
        return objAPITemp;
    }

    WriteToDebug("Didn't find API in this window - "  + strDebugID);
    return null;
}


function SCORM_LookInChildren(wnd){

    WriteToDebug("SCORM_LookInChildren");

    var objAPITemp = null;

    var strDebugID = "";

    strDebugID = "Name=" + wnd.name + ", href=" + wnd.location.href

    for (var i=0; i < wnd.frames.length; i++){

        WriteToDebug("Looking in child frame " + i);

        objAPITemp = wnd.frames[i].API;

        if (SCORM_APIFound(objAPITemp)){
            WriteToDebug("Found API in child frame of " + strDebugID);
            return objAPITemp;
        }

        WriteToDebug("Looking in this child's children " + strDebugID);
        objAPITemp = SCORM_LookInChildren(wnd.frames[i]);

        if (SCORM_APIFound(objAPITemp)){
            WriteToDebug("API found in this child's children " + strDebugID);
            return objAPITemp;
        }
    }

    return null;
}

function SCORM_WindowHasOpener(wnd){
    WriteToDebug("In SCORM_WindowHasOpener");
    if ((wnd.opener != null) && (wnd.opener != wnd) && (typeof(wnd.opener) != "undefined")){
        WriteToDebug("Window Does Have Opener");
        return true;
    }
    else{
        WriteToDebug("Window Does Not Have Opener");
        return false;
    }
}

function SCORM_WindowHasParent(wnd){
    WriteToDebug("In SCORM_WindowHasParent");
    if ((wnd.parent != null) && (wnd.parent != wnd) && (typeof(wnd.parent) != "undefined")){
        WriteToDebug("Window Does Have Parent");
        return true;
    }
    else{
        WriteToDebug("Window Does Not Have Parent");
        return false;
    }
}


function SCORM_APIFound(obj){
    WriteToDebug("In SCORM_APIFound");
    if (obj == null || typeof(obj) == "undefined"){
        WriteToDebug("API NOT Found");
        return false;
    }
    else{
        WriteToDebug("API Found");
        return true;
    }
}




/*******************************************************************
    * SCORM 2004 API Search Algorithm
    * Description - Improvement of the algorithm developed by ADL to
        find the SCORM 2004 API Adapter. The improvements eliminate
        errors, improve code clarity and eliminate the dependence
        on global variables. The errors removed include:
            - The "win" variable was never declared in a scope
              accessible to the GetAPI function
            - A call to API.version which is not part of the SCORM
              2004 specification
            - The previous algorithm was not able to find the API
              if it was located in the window's parent's opener
    * Original Author - ADL & Concurrent Technologies Corporation
    * Author -  Mike Rustici (April 1, 2004)
                Rustici Software, LLC
                http://www.scorm.com
                mike@scorm.com
*******************************************************************/




/*
ScanParentsForApi
-Searches all the parents of a given window until
 it finds an object named "API". If an
 object of that name is found, a reference to it
 is returned. Otherwise, this function returns null.
*/
function SCORM_ScanParentsForApi(win)
{

    WriteToDebug("In SCORM_ScanParentsForApi, win=" + win.location);

    /*
    Establish an outrageously high maximum number of
    parent windows that we are will to search as a
    safe guard against an infinite loop. This is
    probably not strictly necessary, but different
    browsers can do funny things with undefined objects.
    */
    var MAX_PARENTS_TO_SEARCH = 500;
    var nParentsSearched = 0;

    /*
    Search each parent window until we either:
         -find the API,
         -encounter a window with no parent (parent is null
                or the same as the current window)
         -or, have reached our maximum nesting threshold
    */
    while ( (win.API == null || win.API === undefined) &&
            (win.parent != null) && (win.parent != win) &&
            (nParentsSearched <= MAX_PARENTS_TO_SEARCH)
          )
    {

        nParentsSearched++;
        win = win.parent;
    }

    /*
    If the API doesn't exist in the window we stopped looping on,
    then this will return null.
    */
    return win.API;
}


/*
GetAPI
-Searches all parent and opener windows relative to the
 current window for the SCORM API Adapter.
 Returns a reference to the API Adapter if found or null
 otherwise.
*/
function SCORM_GetAPI()
{
    WriteToDebug("In SCORM_GetAPI");

    var API = null;

    //Search all the parents of the current window if there are any
    if ((window.parent != null) && (window.parent != window))
    {
        WriteToDebug("SCORM_GetAPI, searching parent");
        API = SCORM_ScanParentsForApi(window.parent);
    }

    /*
    If we didn't find the API in this window's chain of parents,
    then search all the parents of the opener window if there is one
    */
    if ((API == null) && (window.top.opener != null))
    {
        WriteToDebug("SCORM_GetAPI, searching opener");
        API = SCORM_ScanParentsForApi(window.top.opener);
    }

    return API;
}


var STANDARD = 'AICC';

var blnDirtyAICCData = false;
var blnCommitSavedData = false;

var intAICCErrorNum = NO_ERROR;
var strAICCErrorDesc = "";

var aryAICCFoundItems = new Array();    //stores a list of data elements that have been found in a AICC GetParam result

var blnUseLongInteractionResultValues = true;   //we start off trying to submit verbose descriptions of interaction results, if this errors, we revert back to strict conformance

var blnReviewModeSoReadOnly = false;

//these variables come from and can go stright to the LMS
var AICC_LMS_Version = "";
var AICC_Student_ID = "";
var AICC_Student_Name = "";
var AICC_Lesson_Location = "";
var AICC_Score = "";
var AICC_Credit = "";
var AICC_Lesson_Status = "";
var AICC_Time = "";
var AICC_Mastery_Score = "";
var AICC_Lesson_Mode = "";
var AICC_Max_Time_Allowed = "";
var AICC_Time_Limit_Action = "";
var AICC_Audio = "";
var AICC_Speed = "";
var AICC_Language = "";
var AICC_Text = "";
var AICC_Launch_Data = "";
var AICC_Data_Chunk = "";
var AICC_Comments = "";
var AICC_Objectives = null;
var AICC_CourseID = "";

//these variables are more logical representations of the above variables
var AICC_fltScoreRaw = "";
var AICC_fltScoreMax = "";
var AICC_fltScoreMin = "";
var AICC_blnCredit = true;
var AICC_strLessonMode = MODE_NORMAL;
var AICC_intPreviouslyAccumulatedMilliseconds = 0;
var AICC_intMaxTimeAllowedMilliseconds = MAX_CMI_TIME;
var AICC_blnExitOnTimeout = false;
var AICC_blnShowMessageOnTimeout = true;
var AICC_TextPreference = PREFERENCE_DEFAULT;
var AICC_Status = LESSON_STATUS_NOT_ATTEMPTED;
var AICC_Entry = AICC_ENTRY_FLAG_DEFAULT;               //must be the default b/c absense of entry flag indicates "review"
var AICC_AudioPlayPreference = PREFERENCE_DEFAULT;
var AICC_intAudioVolume = 100;
var AICC_intPercentOfMaxSpeed = 100;
var AICC_intSessionTimeMilliseconds = 0;
var AICC_aryObjectivesRead = new Array();   //objectives reported from LMS, read results from here, also store copy of currently set objectives here
var AICC_aryObjectivesWrite = new Array();  //objectives altered during this session (that is all we're supposed to report to AICC LMS)
var AICC_aryCommentsFromLearner = new Array();
var AICC_aryInteractions = new Array();

//constants to define the meaning of the positions in the arrays to store objective information
//these arrays contain a top level array index by the obj index, these positions contain subarrays with these indicies
var AICC_OBJ_ARRAY_ID     = 0;
var AICC_OBJ_ARRAY_SCORE  = 1;
var AICC_OBJ_ARRAY_STATUS = 2;

//constants to define the meaning of the positions in the arrays to store interaction information
var AICC_INTERACTIONS_ID                    = 0;
var AICC_INTERACTIONS_RESPONSE              = 1;
var AICC_INTERACTIONS_CORRECT               = 2;
var AICC_INTERACTIONS_CORRECT_RESPONSE      = 3;
var AICC_INTERACTIONS_TIME_STAMP            = 4;
var AICC_INTERACTIONS_TYPE                  = 5;
var AICC_INTERACTIONS_WEIGHTING             = 6;
var AICC_INTERACTIONS_LATENCY               = 7;
var AICC_INTERACTIONS_RESPONSE_LONG         = 8;
var AICC_INTERACTIONS_CORRECT_RESPONSE_LONG = 9;

var AICC_INTERACTION_TYPE_TRUE_FALSE    = "T";
var AICC_INTERACTION_TYPE_CHOICE        = "C";
var AICC_INTERACTION_TYPE_FILL_IN       = "F";
var AICC_INTERACTION_TYPE_MATCHING      = "M";
var AICC_INTERACTION_TYPE_PERFORMANCE   = "P";
var AICC_INTERACTION_TYPE_SEQUENCING    = "S";
var AICC_INTERACTION_TYPE_LIKERT        = "L";
var AICC_INTERACTION_TYPE_NUMERIC       = "N";

var AICC_RESULT_CORRECT         = "C";
var AICC_RESULT_WRONG           = "W";
var AICC_RESULT_UNANTICIPATED   = "U";
var AICC_RESULT_NEUTRAL         = "N";


//error constants
var AICC_NO_ERROR = "0";
var AICC_ERROR_INVALID_PREFERENCE = "-1";
var AICC_ERROR_INVALID_STATUS = "-2";
var AICC_ERROR_INVALID_SPEED = "-3";
var AICC_ERROR_INVALID_TIMESPAN = "-4";
var AICC_ERROR_INVALID_TIME_LIMIT_ACTION = "-5";
var AICC_ERROR_INVALID_DECIMAL = "-6";
var AICC_ERROR_INVALID_CREDIT = "-7";
var AICC_ERROR_INVALID_LESSON_MODE = "-8";
var AICC_ERROR_INVALID_ENTRY = "-9";

var blnReviewModeSoReadOnly = false;

function AICC_Initialize(){

    WriteToDebug("In AICC_Initialize");

    //make getparam request, this will parse the data into local variables then begin loading the content

    window.AICCComm.MakeGetParamRequest();

    return;
}

function AICC_InitializeExecuted(){
    WriteToDebug("In AICC_InitializeExecuted");
    //only reset status and such if we are not reviewing
    if (AICC_GetLessonMode() != MODE_REVIEW){
        //only set the status to incomplete if it's not attempted yet
        if (AICC_GetStatus() == LESSON_STATUS_NOT_ATTEMPTED){
            WriteToDebug("Setting Status to Incomplete");
            AICC_Status = LESSON_STATUS_INCOMPLETE;
        }
    }
    else{
        //mode is review, check if we should go to read only mode
        if (!(typeof(REVIEW_MODE_IS_READ_ONLY) == "undefined") && REVIEW_MODE_IS_READ_ONLY === true){
            blnReviewModeSoReadOnly = true;
        }
    }
}


function AICC_Finish(strExitType, blnStatusWasSet){

    WriteToDebug("In AICC_Finish, strExitType=" + strExitType + ", blnStatusWasSet=" + blnStatusWasSet);

    if (! blnStatusWasSet){
        if ( (strExitType == EXIT_TYPE_FINISH)){

            WriteToDebug("Setting status to complete");

            AICC_Status = LESSON_STATUS_COMPLETED;
        }
        else{
            WriteToDebug("Setting status to incomplete");

            AICC_Status = LESSON_STATUS_INCOMPLETE;
        }
    }

    window.AICCComm.PrepareCourseExit(this.actionConceded);

    //Commit Data
    AICC_CommitData();

    //Make ExitAU Request
    window.AICCComm.MakeExitAURequest();

    return true;
}


function AICC_CommitData(){

    var strAICCData;

    WriteToDebug("In AICC_CommitData");

    if (blnReviewModeSoReadOnly === true){
        WriteToDebug("Mode is Review and configuration setting dictates this should be read only so exiting.");
        return true;
    }

    //if there is data to save, make a putparam request
    blnCommitSavedData = false;
    if (IsThereDirtyAICCData()){
        AICC_SaveTime(GetSessionAccumulatedTime());

        blnCommitSavedData = true;

        WriteToDebug("Found Dirty Data");

        strAICCData = FormAICCPostData();

        window.AICCComm.MakePutParamRequest(strAICCData);

        //if there is no course id, then the LMS doesn't support interactions
        //MR - 5/24/05 - removed the course id check to allow aicc courses to be imported into an LMS without using the descriptor files
        //also, Saba doesn't send the Course Id field even when imported with the descriptor files
        if ( /*AICC_CourseID != "" && */ AICC_aryInteractions.length > 0){

            WriteToDebug("Saving Interactions");

            AICC_SendInteractions();
        }

        ClearDirtyAICCData();   //if the PutParam request fails, the data will be set back to dirty via a call to AICC_PutParamFailed()

    }

    return true;
}


function AICC_SendInteractions(){

    WriteToDebug("In AICC_SendInteractions.");

    if (blnReviewModeSoReadOnly === true){
        WriteToDebug("Mode is Review and configuration setting dictates this should be read only so exiting.");
        return true;
    }

    var strAICCData = FormAICCInteractionsData();
    window.AICCComm.MakePutInteractionsRequest(strAICCData);
    //AICC interactions are additive, so we need to remove them once they are sent
    //now that we've sent this batch of interactions, clear out the AICC_aryInteractions array for new ones
    AICC_aryInteractions = new Array();
}

function AICC_GetStudentID(){
    WriteToDebug("In AICC_GetStudentID, Returning " + AICC_Student_ID);

    return AICC_Student_ID ;
}

function AICC_GetStudentName(){
    WriteToDebug("In AICC_GetStudentName, Returning " + AICC_Student_Name);

    return AICC_Student_Name;
}

function AICC_GetBookmark(){
    WriteToDebug("In AICC_GetBookmark, Returning " + AICC_Lesson_Location);

    return AICC_Lesson_Location;
}

function AICC_SetBookmark(strBookmark){
    WriteToDebug("In AICC_SetBookmark, strBookmark=" + strBookmark);

    SetDirtyAICCData();

    AICC_Lesson_Location = strBookmark;

    return true;
}

function AICC_GetDataChunk(){
    WriteToDebug("In AICC_GetDataChunk, Returning " + AICC_Data_Chunk);
    return AICC_Data_Chunk ;
}

function AICC_SetDataChunk(strData){
    //need to check for character limits here 4096 characters
    WriteToDebug("In AICC_SetDataChunk, strData=" + strData );
    if(USE_STRICT_SUSPEND_DATA_LIMITS==true) {
        if(strData.length > 4096) {
            WriteToDebug("SCORM_SetDataChunk - suspend_data too large (4096 character limit for AICC)");
            return false;
        }else{
            SetDirtyAICCData();
            AICC_Data_Chunk = strData;
            AICC_CommitData();
            return true;
        }
    }else{
        SetDirtyAICCData();
        AICC_Data_Chunk = strData;
        AICC_CommitData();
        return true;
    }
}


function AICC_GetLaunchData(){
    WriteToDebug("In AICC_GetLaunchData, Returning " + AICC_Launch_Data );

    return AICC_Launch_Data ;
}

function AICC_GetComments(){
    WriteToDebug("In AICC_GetComments, Returning " + AICC_aryCommentsFromLearner.join(" | "));

    //not available in AICC - return cached comments from this session
    return AICC_aryCommentsFromLearner.join(" | ");
}

function AICC_WriteComment(strComment){
    WriteToDebug("In AICC_WriteComment, strComment=" + strComment);

    var intNextIndex;

    //remove the "|" since AICC has its own delimiters
    if (strComment.search(/ \| /) == 0){
        strComment = strComment.substr(3);
    }

    //remove encoding of "|"
    strComment.replace(/\|\|/g, "|")

    //add the comment to an array of comments
    intNextIndex = AICC_aryCommentsFromLearner.length;

    WriteToDebug("Adding comment to array");

    AICC_aryCommentsFromLearner[intNextIndex] = strComment;

    SetDirtyAICCData();

    return true;
}

function AICC_GetLMSComments(){
    WriteToDebug("In AICC_GetLMSComments, Returning " + AICC_Comments );

    return AICC_Comments;
}


function AICC_GetAudioPlayPreference(){
    WriteToDebug("In AICC_GetAudioPlayPreference, Returning " + AICC_AudioPlayPreference);

    return AICC_AudioPlayPreference;
}



function AICC_GetAudioVolumePreference(){
    WriteToDebug("In AICC_GetAudioVolumePreference, Returning " + AICC_intAudioVolume);

    return AICC_intAudioVolume;
}


function AICC_SetAudioPreference(PlayPreference, intPercentOfMaxVolume){
    WriteToDebug("In AICC_SetAudioPreference, Returning true");

    AICC_AudioPlayPreference = PlayPreference;
    AICC_intAudioVolume = intPercentOfMaxVolume;

    SetDirtyAICCData();
    return true;
}




function AICC_SetLanguagePreference(strLanguage){
    WriteToDebug("In AICC_SetLanguagePreference, Returning true");

    SetDirtyAICCData();

    AICC_Language = strLanguage;

    return true;
}


function AICC_GetLanguagePreference(){
    WriteToDebug("In AICC_GetLanguagePreference, Returning " + AICC_Language);

    return AICC_Language;
}


function AICC_SetSpeedPreference(intPercentOfMax){
    WriteToDebug("In AICC_SetSpeedPreference, Returning true");

    AICC_intPercentOfMaxSpeed = intPercentOfMax;

    SetDirtyAICCData();

    return true;
}


function AICC_GetSpeedPreference(){
    WriteToDebug("In AICC_GetSpeedPreference, Returning " + AICC_intPercentOfMaxSpeed);

    return AICC_intPercentOfMaxSpeed;
}

function AICC_SetTextPreference(intPreference){
    WriteToDebug("In AICC_SetTextPreference, Returning true");

    AICC_TextPreference = intPreference;

    SetDirtyAICCData();

    return true;
}


function AICC_GetTextPreference(){
    WriteToDebug("In AICC_GetTextPreference, Returning " + AICC_TextPreference);

    return AICC_TextPreference;
}

function AICC_GetPreviouslyAccumulatedTime(){
    WriteToDebug("In AICC_GetPreviouslyAccumulatedTime, Returning " + AICC_intPreviouslyAccumulatedMilliseconds);

    return AICC_intPreviouslyAccumulatedMilliseconds;
}


function AICC_SaveTime(intMilliSeconds){
    WriteToDebug("In intMilliSeconds, Returning true");

    AICC_intSessionTimeMilliseconds = intMilliSeconds;

    return true;
}

function AICC_GetMaxTimeAllowed(){
    WriteToDebug("In AICC_GetMaxTimeAllowed, Returning " + AICC_intMaxTimeAllowedMilliseconds);

    return AICC_intMaxTimeAllowedMilliseconds;
}



function AICC_DisplayMessageOnTimeout(){
    WriteToDebug("In AICC_DisplayMessageOnTimeout, Returning " + AICC_blnShowMessageOnTimeout);

    return AICC_blnShowMessageOnTimeout;
}

function AICC_ExitOnTimeout(){
    WriteToDebug("In AICC_ExitOnTimeout, Returning " + AICC_blnExitOnTimeout);

    return AICC_blnExitOnTimeout;
}


function AICC_GetPassingScore(){
    WriteToDebug("In AICC_GetPassingScore, Returning " + AICC_Mastery_Score);

    return AICC_Mastery_Score;

}



function AICC_GetScore(){
    WriteToDebug("In AICC_GetScore, Returning " + AICC_fltScoreRaw);

    return AICC_fltScoreRaw;
}

function AICC_SetScore(fltScore, fltMaxScore, fltMinScore){
    WriteToDebug("In AICC_SetScore, fltScore=" + fltScore + ", fltMaxScore=" + fltMaxScore + ", fltMinScore=" + fltMinScore);

    AICC_fltScoreRaw = fltScore;
    AICC_fltScoreMax = fltMaxScore;
    AICC_fltScoreMin = fltMinScore;

    SetDirtyAICCData();

    return true;
}


function AICC_RecordTrueFalseInteraction(strID, blnResponse, blnCorrect, blnCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In AICC_RecordTrueFalseInteraction strID=" + strID + ", blnResponse=" + blnResponse +
                    ", blnCorrect=" + blnCorrect + ", blnCorrectResponse=" + blnCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);


    var intTotalInteractions;
    var aryData = new Array(10);

    intTotalInteractions = AICC_aryInteractions.length;

    if (intWeighting == null || intWeighting == undefined ){intWeighting="";}
    if (intLatency == null || intLatency == undefined){intLatency="";}
    if (blnCorrect == null || blnCorrect == undefined){blnCorrect="";}

    var strResponse = "";
    var strCorrectResponse = "";

    if (blnResponse !== null) {
        if (blnResponse) {
            strResponse = "t";
        }
        else {
            strResponse = "f";
        }
    }

    if (blnCorrectResponse == true){
        strCorrectResponse = "t";
    }
    else if(blnCorrectResponse == false){       //test for false b/c it could be null in which case we want to leave it as ""
        strCorrectResponse = "f";
    }

    aryData[AICC_INTERACTIONS_ID]               = strID;
    aryData[AICC_INTERACTIONS_RESPONSE]         = strResponse;
    aryData[AICC_INTERACTIONS_CORRECT]          = blnCorrect;
    aryData[AICC_INTERACTIONS_CORRECT_RESPONSE] = strCorrectResponse;
    aryData[AICC_INTERACTIONS_TIME_STAMP]       = dtmTime;
    aryData[AICC_INTERACTIONS_TYPE]             = AICC_INTERACTION_TYPE_TRUE_FALSE;
    aryData[AICC_INTERACTIONS_WEIGHTING]        = intWeighting;
    aryData[AICC_INTERACTIONS_LATENCY]          = intLatency;
    aryData[AICC_INTERACTIONS_RESPONSE_LONG]        = strResponse;
    aryData[AICC_INTERACTIONS_CORRECT_RESPONSE_LONG]= strCorrectResponse;


    //in AICC ignore the description field since there is nothing we can do with it
    //in AICC, don't try to set the interaction objective because these need to specified in the descriptor files

    AICC_aryInteractions[intTotalInteractions] = aryData;

    WriteToDebug("Added to interactions array, index=" + intTotalInteractions);

    SetDirtyAICCData();
    return true;
}

function AICC_RecordMultipleChoiceInteraction(strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In AICC_RecordMultipleChoiceInteraction strID=" + strID + ", aryResponse=" + aryResponse +
                    ", blnCorrect=" + blnCorrect + ", aryCorrectResponse=" + aryCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);


    var intTotalInteractions;
    var aryData = new Array(10);

    intTotalInteractions = AICC_aryInteractions.length;

    if (intWeighting == null || intWeighting == undefined){intWeighting="";}
    if (intLatency == null || intLatency == undefined){intLatency="";}
    if (blnCorrect == null || blnCorrect == undefined){blnCorrect="";}


    var strResponse = "";
    var strResponseLong = "";

    var strCorrectResponse = "";
    var strCorrectResponseLong = "";

    if (aryResponse !== null) {
        for (var i=0; i < aryResponse.length; i++){

            if (strResponse.length > 0) {strResponse += ",";}
            if (strResponseLong.length > 0) {strResponseLong += ",";}

            strResponse += aryResponse[i].Short.replace(",", "");
            strResponseLong += aryResponse[i].Long.replace(",", "");
        }
    }

    for (var i=0; i < aryCorrectResponse.length; i++){

        if (strCorrectResponse.length > 0) {strCorrectResponse += ",";}
        if (strCorrectResponseLong.length > 0) {strCorrectResponseLong += ",";}

        strCorrectResponse += aryCorrectResponse[i].Short.replace(",", "");
        strCorrectResponseLong += aryCorrectResponse[i].Long.replace(",", "");
    }

    aryData[AICC_INTERACTIONS_ID]               = strID;
    aryData[AICC_INTERACTIONS_RESPONSE]         = strResponse;
    aryData[AICC_INTERACTIONS_CORRECT]          = blnCorrect;
    aryData[AICC_INTERACTIONS_CORRECT_RESPONSE] = strCorrectResponse;
    aryData[AICC_INTERACTIONS_TIME_STAMP]       = dtmTime;
    aryData[AICC_INTERACTIONS_TYPE]             = AICC_INTERACTION_TYPE_CHOICE;
    aryData[AICC_INTERACTIONS_WEIGHTING]        = intWeighting;
    aryData[AICC_INTERACTIONS_LATENCY]          = intLatency;
    aryData[AICC_INTERACTIONS_RESPONSE_LONG]        = strResponseLong;
    aryData[AICC_INTERACTIONS_CORRECT_RESPONSE_LONG]= strCorrectResponseLong;

    //in AICC ignore the description field since there is nothing we can do with it
    //in AICC, don't try to set the interaction objective because these need to specified in the descriptor files

    AICC_aryInteractions[intTotalInteractions] = aryData;

    WriteToDebug("Added to interactions array, index=" + intTotalInteractions);

    SetDirtyAICCData();
    return true;
}

function AICC_RecordFillInInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In AICC_RecordFillInInteraction strID=" + strID + ", strResponse=" + strResponse +
                    ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);


    var intTotalInteractions;
    var aryData = new Array(10);

    intTotalInteractions = AICC_aryInteractions.length;

    if (intWeighting == null || intWeighting == undefined ){intWeighting="";}
    if (intLatency == null || intLatency == undefined){intLatency="";}
    if (blnCorrect == null || blnCorrect == undefined){blnCorrect="";}
    if (strResponse === null) {strResponse = "";}
    if (strCorrectResponse == null || strCorrectResponse == undefined){strCorrectResponse="";}

    strResponse = new String(strResponse);
    if (strResponse.length > 255) {strResponse = strResponse.substr(0, 255);}

    strCorrectResponse = new String(strCorrectResponse);
    if (strCorrectResponse.length > 255) {strCorrectResponse = strCorrectResponse.substr(0, 255);}

    aryData[AICC_INTERACTIONS_ID]               = strID;
    aryData[AICC_INTERACTIONS_RESPONSE]         = strResponse;
    aryData[AICC_INTERACTIONS_CORRECT]          = blnCorrect;
    aryData[AICC_INTERACTIONS_CORRECT_RESPONSE] = strCorrectResponse;
    aryData[AICC_INTERACTIONS_TIME_STAMP]       = dtmTime;
    aryData[AICC_INTERACTIONS_TYPE]             = AICC_INTERACTION_TYPE_FILL_IN;
    aryData[AICC_INTERACTIONS_WEIGHTING]        = intWeighting;
    aryData[AICC_INTERACTIONS_LATENCY]          = intLatency;
    aryData[AICC_INTERACTIONS_RESPONSE_LONG]        = strResponse;
    aryData[AICC_INTERACTIONS_CORRECT_RESPONSE_LONG]= strCorrectResponse;

    //in AICC ignore the description field since there is nothing we can do with it
    //in AICC, don't try to set the interaction objective because these need to specified in the descriptor files

    AICC_aryInteractions[intTotalInteractions] = aryData;

    WriteToDebug("Added to interactions array, index=" + intTotalInteractions);

    SetDirtyAICCData();
    return true;
}

function AICC_RecordMatchingInteraction(strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In AICC_RecordMatchingInteraction strID=" + strID + ", aryResponse=" + aryResponse +
                    ", blnCorrect=" + blnCorrect + ", aryCorrectResponse=" + aryCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);


    var intTotalInteractions;
    var aryData = new Array(10);

    intTotalInteractions = AICC_aryInteractions.length;

    if (intWeighting == null || intWeighting == undefined ){intWeighting="";}
    if (intLatency == null || intLatency == undefined){intLatency="";}
    if (blnCorrect == null || blnCorrect == undefined){blnCorrect="";}


    var strResponse = "";
    var strResponseLong = "";

    var strCorrectResponse = "";
    var strCorrectResponseLong = "";

    if (aryResponse !== null) {
        for (var i=0; i < aryResponse.length; i++){

            if (strResponse.length > 0) {strResponse += ",";}
            if (strResponseLong.length > 0) {strResponseLong += ",";}

            strResponse += aryResponse[i].Source.Short.replace(",", "").replace(".", "") + "." + aryResponse[i].Target.Short.replace(",", "").replace(".", "");
            strResponseLong += aryResponse[i].Source.Long.replace(",", "").replace(".", "") + "." + aryResponse[i].Target.Long.replace(",", "").replace(".", "");
        }
    }

    for (var i=0; i < aryCorrectResponse.length; i++){

        if (strCorrectResponse.length > 0) {strCorrectResponse += ",";}
        if (strCorrectResponseLong.length > 0) {strCorrectResponseLong += ",";}

        if (aryCorrectResponse[i].Source.Short != "" && aryCorrectResponse[i].Source.Long != ""){
            strCorrectResponse += aryCorrectResponse[i].Source.Short.replace(",", "").replace(".", "") + "." + aryCorrectResponse[i].Target.Short.replace(",", "").replace(".", "");
            strCorrectResponseLong += aryCorrectResponse[i].Source.Long.replace(",", "").replace(".", "") + "." + aryCorrectResponse[i].Target.Long.replace(",", "").replace(".", "");
        }
    }

    aryData[AICC_INTERACTIONS_ID]               = strID;
    aryData[AICC_INTERACTIONS_RESPONSE]         = strResponse;
    aryData[AICC_INTERACTIONS_CORRECT]          = blnCorrect;
    aryData[AICC_INTERACTIONS_CORRECT_RESPONSE] = strCorrectResponse;
    aryData[AICC_INTERACTIONS_TIME_STAMP]       = dtmTime;
    aryData[AICC_INTERACTIONS_TYPE]             = AICC_INTERACTION_TYPE_MATCHING;
    aryData[AICC_INTERACTIONS_WEIGHTING]        = intWeighting;
    aryData[AICC_INTERACTIONS_LATENCY]          = intLatency;
    aryData[AICC_INTERACTIONS_RESPONSE_LONG]        = strResponseLong;
    aryData[AICC_INTERACTIONS_CORRECT_RESPONSE_LONG]= strCorrectResponseLong;

    //in AICC ignore the description field since there is nothing we can do with it
    //in AICC, don't try to set the interaction objective because these need to specified in the descriptor files

    AICC_aryInteractions[intTotalInteractions] = aryData;

    WriteToDebug("Added to interactions array, index=" + intTotalInteractions);

    SetDirtyAICCData();
    return true;
}

function AICC_RecordPerformanceInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In AICC_RecordPerformanceInteraction strID=" + strID + ", strResponse=" + strResponse +
                    ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);


    var intTotalInteractions;
    var aryData = new Array(10);

    intTotalInteractions = AICC_aryInteractions.length;

    if (intWeighting == null || intWeighting == undefined ){intWeighting="";}
    if (intLatency == null || intLatency == undefined){intLatency="";}
    if (blnCorrect == null || blnCorrect == undefined){blnCorrect="";}
    if (strResponse === null) {strResponse = "";}
    if (strCorrectResponse == null || strCorrectResponse == undefined){strCorrectResponse="";}

    strResponse = new String(strResponse);
    if (strResponse.length > 255) {strResponse = strResponse.substr(0, 255);}

    strCorrectResponse = new String(strCorrectResponse);
    if (strCorrectResponse.length > 255) {strCorrectResponse = strCorrectResponse.substr(0, 255);}

    aryData[AICC_INTERACTIONS_ID]               = strID;
    aryData[AICC_INTERACTIONS_RESPONSE]         = strResponse;
    aryData[AICC_INTERACTIONS_CORRECT]          = blnCorrect;
    aryData[AICC_INTERACTIONS_CORRECT_RESPONSE] = strCorrectResponse;
    aryData[AICC_INTERACTIONS_TIME_STAMP]       = dtmTime;
    aryData[AICC_INTERACTIONS_TYPE]             = AICC_INTERACTION_TYPE_PERFORMANCE;
    aryData[AICC_INTERACTIONS_WEIGHTING]        = intWeighting;
    aryData[AICC_INTERACTIONS_LATENCY]          = intLatency;
    aryData[AICC_INTERACTIONS_RESPONSE_LONG]        = strResponse;
    aryData[AICC_INTERACTIONS_CORRECT_RESPONSE_LONG]= strCorrectResponse;

    //in AICC ignore the description field since there is nothing we can do with it
    //in AICC, don't try to set the interaction objective because these need to specified in the descriptor files

    AICC_aryInteractions[intTotalInteractions] = aryData;

    WriteToDebug("Added to interactions array, index=" + intTotalInteractions);

    SetDirtyAICCData();
    return true;
}

function AICC_RecordSequencingInteraction(strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In AICC_RecordSequencingInteraction strID=" + strID + ", aryResponse=" + aryResponse +
                    ", blnCorrect=" + blnCorrect + ", aryCorrectResponse=" + aryCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);


    var intTotalInteractions;
    var aryData = new Array(10);

    intTotalInteractions = AICC_aryInteractions.length;

    if (intWeighting == null || intWeighting == undefined ){intWeighting="";}
    if (intLatency == null || intLatency == undefined){intLatency="";}
    if (blnCorrect == null || blnCorrect == undefined){blnCorrect="";}

    var strResponse = "";
    var strResponseLong = "";

    var strCorrectResponse = "";
    var strCorrectResponseLong = "";

    if (aryResponse !== null) {
        for (var i=0; i < aryResponse.length; i++){

            if (strResponse.length > 0) {strResponse += ",";}
            if (strResponseLong.length > 0) {strResponseLong += ",";}

            strResponse += aryResponse[i].Short.replace(",", "");
            strResponseLong += aryResponse[i].Long.replace(",", "");
        }
    }

    for (var i=0; i < aryCorrectResponse.length; i++){

        if (strCorrectResponse.length > 0) {strCorrectResponse += ",";}
        if (strCorrectResponseLong.length > 0) {strCorrectResponseLong += ",";}

        strCorrectResponse += aryCorrectResponse[i].Short.replace(",", "");
        strCorrectResponseLong += aryCorrectResponse[i].Long.replace(",", "");
    }

    aryData[AICC_INTERACTIONS_ID]               = strID;
    aryData[AICC_INTERACTIONS_RESPONSE]         = strResponse;
    aryData[AICC_INTERACTIONS_CORRECT]          = blnCorrect;
    aryData[AICC_INTERACTIONS_CORRECT_RESPONSE] = strCorrectResponse;
    aryData[AICC_INTERACTIONS_TIME_STAMP]       = dtmTime;
    aryData[AICC_INTERACTIONS_TYPE]             = AICC_INTERACTION_TYPE_SEQUENCING;
    aryData[AICC_INTERACTIONS_WEIGHTING]        = intWeighting;
    aryData[AICC_INTERACTIONS_LATENCY]          = intLatency;
    aryData[AICC_INTERACTIONS_RESPONSE_LONG]        = strResponseLong;
    aryData[AICC_INTERACTIONS_CORRECT_RESPONSE_LONG]= strCorrectResponseLong;

    //in AICC ignore the description field since there is nothing we can do with it
    //in AICC, don't try to set the interaction objective because these need to specified in the descriptor files

    AICC_aryInteractions[intTotalInteractions] = aryData;

    WriteToDebug("Added to interactions array, index=" + intTotalInteractions);

    SetDirtyAICCData();
    return true;
}

function AICC_RecordLikertInteraction(strID, response, blnCorrect, correctResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In RecordLikertInteraction strID=" + strID + ", response=" + response +
                    ", blnCorrect=" + blnCorrect + ", correctResponse=" + correctResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);


    var intTotalInteractions;
    var aryData = new Array(10);

    intTotalInteractions = AICC_aryInteractions.length;

    if (intWeighting == null || intWeighting == undefined ){intWeighting="";}
    if (intLatency == null || intLatency == undefined){intLatency="";}
    if (blnCorrect == null || blnCorrect == undefined){blnCorrect="";}

    var strResponse = "";
    var strResponseLong = "";
    if (response !== null) {
        strResponse = response.Short;
        strResponseLong = response.Long;
    }

    var strCorrectResponse = "";
    var strCorrectResponseLong = "";

    if (correctResponse != null){
        strCorrectResponse = correctResponse.Short;
        strCorrectResponseLong = correctResponse.Long;
    }

    aryData[AICC_INTERACTIONS_ID]               = strID;
    aryData[AICC_INTERACTIONS_RESPONSE]         = strResponse;
    aryData[AICC_INTERACTIONS_CORRECT]          = blnCorrect;
    aryData[AICC_INTERACTIONS_CORRECT_RESPONSE] = strCorrectResponse;
    aryData[AICC_INTERACTIONS_TIME_STAMP]       = dtmTime;
    aryData[AICC_INTERACTIONS_TYPE]             = AICC_INTERACTION_TYPE_LIKERT;
    aryData[AICC_INTERACTIONS_WEIGHTING]        = intWeighting;
    aryData[AICC_INTERACTIONS_LATENCY]          = intLatency;
    aryData[AICC_INTERACTIONS_RESPONSE_LONG]        = strResponseLong;
    aryData[AICC_INTERACTIONS_CORRECT_RESPONSE_LONG]= strCorrectResponseLong;


    //in AICC ignore the description field since there is nothing we can do with it
    //in AICC, don't try to set the interaction objective because these need to specified in the descriptor files

    AICC_aryInteractions[intTotalInteractions] = aryData;

    WriteToDebug("Added to interactions array, index=" + intTotalInteractions);

    SetDirtyAICCData();
    return true;
}

function AICC_RecordNumericInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime){

    WriteToDebug("In AICC_RecordNumericInteraction strID=" + strID + ", strResponse=" + strResponse +
                    ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
                    ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime);


    var intTotalInteractions;
    var aryData = new Array(10);

    intTotalInteractions = AICC_aryInteractions.length;

    if (intWeighting == null || intWeighting == undefined ){intWeighting="";}
    if (intLatency == null || intLatency == undefined){intLatency="";}
    if (blnCorrect == null || blnCorrect == undefined){blnCorrect="";}
    if (strResponse === null) { strResponse = ""; }

    if (strCorrectResponse != undefined && strCorrectResponse != null) {
        if (IsValidDecimalRange(strCorrectResponse))
        {
            //we could receive a decimal range correct response from SCORM 2004/Tin Can/Cmi5. If we do we need to convert that
            //to a decimal for use in AICC.
            strCorrectResponse = ConvertDecimalRangeToDecimalBasedOnLearnerResponse(strCorrectResponse, strResponse, blnCorrect);
        }
        //AICC does not accept decimal range correct answers, so if it's not a decimal by this point we fail
        if (!IsValidDecimal(strCorrectResponse)){
            WriteToDebug("Returning False - AICC_RecordNumericInteraction received invalid correct response (not a decimal), strCorrectResponse=" +strCorrectResponse);
            return false;
        }
    } else {
        strCorrectResponse = "";
    }

    aryData[AICC_INTERACTIONS_ID]               = strID;
    aryData[AICC_INTERACTIONS_RESPONSE]         = strResponse;
    aryData[AICC_INTERACTIONS_CORRECT]          = blnCorrect;
    aryData[AICC_INTERACTIONS_CORRECT_RESPONSE] = strCorrectResponse;
    aryData[AICC_INTERACTIONS_TIME_STAMP]       = dtmTime;
    aryData[AICC_INTERACTIONS_TYPE]             = AICC_INTERACTION_TYPE_NUMERIC;
    aryData[AICC_INTERACTIONS_WEIGHTING]        = intWeighting;
    aryData[AICC_INTERACTIONS_LATENCY]          = intLatency;
    aryData[AICC_INTERACTIONS_RESPONSE_LONG]        = strResponse;
    aryData[AICC_INTERACTIONS_CORRECT_RESPONSE_LONG]= strCorrectResponse;

    //in AICC ignore the description field since there is nothing we can do with it
    //in AICC, don't try to set the interaction objective because these need to specified in the descriptor files

    AICC_aryInteractions[intTotalInteractions] = aryData;

    WriteToDebug("Added to interactions array, index=" + intTotalInteractions);

    SetDirtyAICCData();
    return true;
}

function AICC_GetEntryMode(){
    WriteToDebug("In AICC_GetEntryMode, Returning " + AICC_Entry);

    return AICC_Entry;
}

function AICC_GetLessonMode(){
    WriteToDebug("In AICC_GetLessonMode, Returning " + AICC_strLessonMode);

    return AICC_strLessonMode;
}

function AICC_GetTakingForCredit(){
    WriteToDebug("In AICC_GetTakingForCredit, Returning " + AICC_blnCredit);

    return AICC_blnCredit;
}



function AICC_SetObjectiveScore(strObjectiveID, intScore, intMaxScore, intMinScore){

    WriteToDebug("In AICC_SetObjectiveScore, strObjectiveID=" + strObjectiveID + ", intScore=" + intScore + ", intMaxScore=" + intMaxScore + ", intMinScore=" + intMinScore);

    var intNextID;
    var intObjIndex;
    var strAICCScore = "";

    intObjIndex = FindObjectiveById(strObjectiveID, AICC_aryObjectivesRead);

    if (intObjIndex != null){
        WriteToDebug("Found read objective");
        AICC_aryObjectivesRead[intObjIndex][AICC_OBJ_ARRAY_SCORE] = intScore;
    }
    else{
        WriteToDebug("Adding new read objective");

        intNextID = AICC_aryObjectivesRead.length;

        AICC_aryObjectivesRead[parseInt(intNextID, 10)] = new Array(3);
        AICC_aryObjectivesRead[parseInt(intNextID, 10)][AICC_OBJ_ARRAY_ID] = strObjectiveID;
        AICC_aryObjectivesRead[parseInt(intNextID, 10)][AICC_OBJ_ARRAY_SCORE] = intScore;
        AICC_aryObjectivesRead[parseInt(intNextID, 10)][AICC_OBJ_ARRAY_STATUS] = "";
    }


    intObjIndex = FindObjectiveById(strObjectiveID, AICC_aryObjectivesWrite);

    if (intObjIndex != null){
        WriteToDebug("Found write objective");
        AICC_aryObjectivesWrite[intObjIndex][AICC_OBJ_ARRAY_SCORE] = intScore;
    }
    else{
        WriteToDebug("Adding new write objective");

        intNextID = AICC_aryObjectivesWrite.length;

        AICC_aryObjectivesWrite[parseInt(intNextID, 10)] = new Array(3);

        strAICCScore = intScore;

        //prior to version 3, AICC scores cannot contain a decimal
        if (AICC_LMS_Version < 3 && strAICCScore != ""){
            strAICCScore = parseInt(strAICCScore, 10);
        }

        if (    (AICC_REPORT_MIN_MAX_SCORE === undefined ||
                 AICC_REPORT_MIN_MAX_SCORE === null ||
                 AICC_REPORT_MIN_MAX_SCORE === true) &&
            (AICC_LMS_Version >= 3 )        //min and max scores are only allowed after version 3
        ){

            if ((intMaxScore != "") || (intMinScore != "")) {
                WriteToDebug("Appending Max and Min scores");
                strAICCScore += "," + intMaxScore + "," + intMinScore;
            }

        }

        AICC_aryObjectivesWrite[parseInt(intNextID, 10)][AICC_OBJ_ARRAY_ID] = strObjectiveID;
        AICC_aryObjectivesWrite[parseInt(intNextID, 10)][AICC_OBJ_ARRAY_SCORE] = strAICCScore;
        AICC_aryObjectivesWrite[parseInt(intNextID, 10)][AICC_OBJ_ARRAY_STATUS] = "";
    }

    SetDirtyAICCData();

    return true;
}


function AICC_SetObjectiveStatus(strObjectiveID, Lesson_Status){

    WriteToDebug("In AICC_SetObjectiveStatus, strObjectiveID=" + strObjectiveID + ", Lesson_Status=" + Lesson_Status);

    var intNextID;
    var intObjIdex;

    intObjIdex = FindObjectiveById(strObjectiveID, AICC_aryObjectivesRead);

    if (intObjIdex != null){
        WriteToDebug("Found read objective");
        AICC_aryObjectivesRead[intObjIdex][AICC_OBJ_ARRAY_STATUS] = Lesson_Status;
    }
    else{
        WriteToDebug("Adding new read objective");
        intNextID = AICC_aryObjectivesRead.length;

        AICC_aryObjectivesRead[parseInt(intNextID, 10)] = new Array(3);

        AICC_aryObjectivesRead[parseInt(intNextID, 10)][AICC_OBJ_ARRAY_ID] = strObjectiveID;
        AICC_aryObjectivesRead[parseInt(intNextID, 10)][AICC_OBJ_ARRAY_STATUS] = Lesson_Status;
        AICC_aryObjectivesRead[parseInt(intNextID, 10)][AICC_OBJ_ARRAY_SCORE] = "";
    }


    intObjIdex = FindObjectiveById(strObjectiveID, AICC_aryObjectivesWrite);

    if (intObjIdex != null){
        WriteToDebug("Found write objective");
        AICC_aryObjectivesWrite[intObjIdex][AICC_OBJ_ARRAY_STATUS] = Lesson_Status;
    }
    else{
        WriteToDebug("Adding new write objective");
        intNextID = AICC_aryObjectivesWrite.length;

        AICC_aryObjectivesWrite[parseInt(intNextID, 10)] = new Array(3);

        AICC_aryObjectivesWrite[parseInt(intNextID, 10)][AICC_OBJ_ARRAY_ID] = strObjectiveID;
        AICC_aryObjectivesWrite[parseInt(intNextID, 10)][AICC_OBJ_ARRAY_STATUS] = Lesson_Status;
        AICC_aryObjectivesWrite[parseInt(intNextID, 10)][AICC_OBJ_ARRAY_SCORE] = "";
    }

    SetDirtyAICCData();

    return true;
}


function AICC_SetObjectiveDescription(strObjectiveID, strObjectiveDescription){

    WriteToDebug("In AICC_SetObjectiveDescription, strObjectiveID=" + strObjectiveID + ", strObjectiveDescription=" + strObjectiveDescription);
    WriteToDebug("Objective descriptions are not supported prior to SCORM 2004");

    return true;

}


function AICC_GetObjectiveScore(strObjectiveID){
    WriteToDebug("In AICC_SetObjectiveScore, strObjectiveID=" + strObjectiveID);

    var intObjIndex = FindObjectiveById(strObjectiveID, AICC_aryObjectivesRead)

    if (intObjIndex != null){
        WriteToDebug("Found objective, returning " + AICC_aryObjectivesRead[intObjIndex][AICC_OBJ_ARRAY_SCORE]);
        return AICC_aryObjectivesRead[intObjIndex][AICC_OBJ_ARRAY_SCORE];
    }
    else{
        WriteToDebug("Did not find objective, returning ''");
        return "";
    }
}

function AICC_GetObjectiveDescription(strObjectiveID){
    WriteToDebug("In AICC_GetObjectiveDescription, strObjectiveID=" + strObjectiveID);
    WriteToDebug("Objective descriptions are not supported prior to SCORM 2004");

    return "";
}

function AICC_GetObjectiveStatus(strObjectiveID){

    WriteToDebug("In AICC_SetObjectiveStatus, strObjectiveID=" + strObjectiveID);

    var intObjIndex = FindObjectiveById(strObjectiveID, AICC_aryObjectivesRead)

    if (intObjIndex != null){
        WriteToDebug("Found objective, returning " + AICC_aryObjectivesRead[intObjIndex][AICC_OBJ_ARRAY_STATUS]);
        return AICC_aryObjectivesRead[intObjIndex][AICC_OBJ_ARRAY_STATUS];
    }
    else{
        WriteToDebug("Did not find objective, returning " + LESSON_STATUS_NOT_ATTEMPTED);
        return LESSON_STATUS_NOT_ATTEMPTED;
    }
}

function AICC_SetFailed(){
    WriteToDebug("In AICC_SetFailed, Returning true");

    AICC_Status = LESSON_STATUS_FAILED;

    SetDirtyAICCData();

    return true;
}

function AICC_SetPassed(){
    WriteToDebug("In AICC_SetPassed, Returning true");

    AICC_Status = LESSON_STATUS_PASSED;

    SetDirtyAICCData();

    return true;
}

function AICC_SetCompleted(){
    WriteToDebug("In AICC_SetCompleted, Returning true");

    AICC_Status = LESSON_STATUS_COMPLETED;

    SetDirtyAICCData();

    return true;
}

function AICC_ResetStatus(){
    WriteToDebug("In AICC_ResetStatus, Returning true");

    AICC_Status = LESSON_STATUS_INCOMPLETE;

    SetDirtyAICCData();

    return true;
}

function AICC_GetStatus(){
    WriteToDebug("In AICC_GetStatus, Returning " + AICC_Status);

    return AICC_Status;
}

//public
function AICC_GetProgressMeasure(){
    WriteToDebug("AICC_GetProgressMeasure - AICC does not support progress_measure, returning false");
    return false;
}
//public
function AICC_SetProgressMeasure(){
    WriteToDebug("AICC_SetProgressMeasure - AICC does not support progress_measure, returning false");
    return false;
}

//public
function AICC_GetObjectiveProgressMeasure(){
    WriteToDebug("AICC_GetObjectiveProgressMeasure - AICC does not support progress_measure, returning false");
    return false;
}
//public
function AICC_SetObjectiveProgressMeasure(){
    WriteToDebug("AICC_SetObjectiveProgressMeasure - AICC does not support progress_measure, returning false");
    return false;
}

function AICC_SetPointBasedScore(intScore, intMaxScore, intMinScore){
    WriteToDebug("AICC_SetPointBasedScore - AICC does not support SetPointBasedScore, falling back to SetScore");
    return AICC_SetScore(intScore, intMaxScore, intMinScore);
}

function AICC_GetScaledScore(intScore, intMaxScore, intMinScore){
    WriteToDebug("AICC_GetScaledScore - AICC does not support GetScaledScore, returning false");
    return false;
}

function AICC_GetLastError(){
    WriteToDebug("In AICC_GetLastError, Returning " + intAICCErrorNum);

    return intAICCErrorNum;
}

function AICC_GetLastErrorDesc(){
    WriteToDebug("In AICC_GetLastErrorDesc, Returning '" + strAICCErrorDesc + "'");

    return strAICCErrorDesc;
}



//==============================================================================


function AICC_PutParamFailed(){
    WriteToDebug("ERROR: In AICC_PutParamFailed");

    //set dirty data
    SetDirtyAICCData();

    //currently, just leaves the data as dirty, could also re-try or display an error as desired

    return;
}

function AICC_PutInteractionsFailed(){
    WriteToDebug("ERROR: In AICC_PutInteractionsFailed");

    //set dirty data
    SetDirtyAICCData();

    //the LMS probably only accepts strictly conformant interaction result values, so retry one more time
    //with the short values which will be used from now on

    //this call originates from the child frame so be sure to reference the parent
    if (parent.blnUseLongInteractionResultValues == true){

        parent.blnUseLongInteractionResultValues = false;

        parent.AICC_CommitData();
    }

    return;
}

function AICC_SetErrorInfo(strErrorNumLine, strErrorDescLine){
    WriteToDebug("ERROR: In AICC_SetErrorInfo, strErrorNumLine=" + strErrorNumLine + ", strErrorDescLine=" + strErrorDescLine);

    if (strErrorNumLine.toLowerCase().search(/error\s*=\s*0/) == -1){
        WriteToDebug("Detected No Error");
        intAICCErrorNum = NO_ERROR;
        strAICCErrorDesc = "";
    }
    else{
        WriteToDebug("Setting Error Info");
        AICC_SetError(GetValueFromAICCLine(strAICCErrorLine), GetValueFromAICCLine(strAICCErrorDesc))
    }
}

function AICC_SetError(intErrorNum, strErrorDesc){
    WriteToDebug("ERROR: In AICC_SetError, intErrorNum=" + intErrorNum + ", strErrorDesc=" + strErrorDesc);

    intAICCErrorNum = intErrorNum;
    strAICCErrorDesc = strAICCErrorDesc;
}

function SetDirtyAICCData(){
    WriteToDebug("In SetDirtyAICCData");
    blnDirtyAICCData = true;
}

function ClearDirtyAICCData(){
    WriteToDebug("In ClearDirtyAICCData");
    blnDirtyAICCData = false;
}

function IsThereDirtyAICCData(){
    WriteToDebug("In IsThereDirtyAICCData, returning " + blnDirtyAICCData);
    return blnDirtyAICCData;
}


function GetValueFromAICCLine(strLine){

    WriteToDebug("In GetValueFromAICCLine, strLine=" + strLine);

    //find equal sign
    //if found equal sign
        //get all characters after equal sign
        //remove leading and trailing white space (cr, lf, tab, space, etc)

    var intPos;
    var strValue = "";
    var strTemp;

    strLine = new String(strLine);

    intPos = strLine.indexOf("=");

    WriteToDebug("intPos=" + intPos);

    if (intPos > -1 && ((intPos + 1) < strLine.length)){

        WriteToDebug("Grabbing value");

        strTemp = strLine.substring(intPos+1);

        WriteToDebug("strTemp=" + strTemp);

        strTemp = strTemp.replace(/^\s*/, "");      //replace leading whitespace
        strTemp = strTemp.replace(/\s*$/, "");      //replace trailing whitespace

        strValue = strTemp;
    }

    WriteToDebug("returning " + strValue);

    return strValue;

}

function GetNameFromAICCLine(strLine){
    //find equal sign
    //if found equal sign
        //get all characters after equal sign
        //remove leading and trailing white space (cr, lf, tab, space, etc)
    //else, look for brackets and return contents of brackets

    WriteToDebug("In GetNameFromAICCLine, strLine=" + strLine);

    var intPos;
    var strTemp;

    var strName = "";

    strLine = new String(strLine);

    intPos = strLine.indexOf("=");

    WriteToDebug("intPos=" + intPos);

    if (intPos > -1 && intPos < strLine.length){

        WriteToDebug("Grabbing name from name/value pair");

        strTemp = strLine.substring(0, intPos);

        WriteToDebug("strTemp=" + strTemp);

        strTemp = strTemp.replace(/^\s*/, "");
        strTemp = strTemp.replace(/\s*$/, "");

        strName = strTemp;
    }

    else{

        WriteToDebug("Grabbing name from group / section heading");

        intPos = strLine.indexOf("[");

        WriteToDebug("intPos=" + intPos);

        if (intPos > -1){

            WriteToDebug("Replacing []");

            strTemp = strLine.replace(/[\[|\]]/g, "");  //replace the square brackets

            WriteToDebug("strTemp=" + strTemp);

            strTemp = strTemp.replace(/^\s*/, "");      //replace leading whitespace
            strTemp = strTemp.replace(/\s*$/, "");      //replace trailing whitespace

            strName = strTemp;
        }

    }


    WriteToDebug("returning " + strName);

    return strName;
}



function GetIndexFromAICCName(strLineName){

    WriteToDebug("In GetIndexFromAICCName, strLineName=" + strLineName);

    //find period
        //if found period
        //get all characters after preiod
        //if there is an equal sign (we got an entire line instead of a line name
            //remove all characters after the equal sign
        //remove leading and trailing white space (cr, lf, tab, space, etc)

    var intPos;
    var strIndex = "";
    var strTemp = "";

    strLine = new String(strLineName);

    intPos = strLine.indexOf(".");

    WriteToDebug("intPos=" + intPos);

    if (intPos > -1 && (intPos+1) < strLine.length){

        WriteToDebug("Grabbing index");

        strTemp = strLine.substring(intPos + 1);


        WriteToDebug("strTemp=" + strTemp);

        WriteToDebug("Checking for equal sign");

        intPos = strTemp.indexOf("=");

        if (intPos > -1 && intPos < strTemp.length){
            WriteToDebug("Found and removing equal sign");
            strTemp = strLine.substring(0, intPos);
        }

        WriteToDebug("Removing white space");

        strTemp = strTemp.replace(/^\s*/, "");      //replace leading whitespace
        strTemp = strTemp.replace(/\s*$/, "");      //replace trailing whitespace

        strIndex = strTemp;
    }

    WriteToDebug("returning " + strIndex);

    return strIndex;
}


//==============================================================================

function ParseGetParamData(strLMSResult){

    WriteToDebug("In ParseGetParamData");

    var aryAICCResponseLines;
    var strLine;
    var strLineName;
    var strLineValue;
    var i, j;           //loop line counters

    //parse LMS Result into local variables

    strLMSResult = new String(strLMSResult);
    aryAICCResponseLines = strLMSResult.split("\n");    //only use \n instead of \r\n b/c some LMS's will only use one character

    WriteToDebug("Split String");

    for (i=0; i < aryAICCResponseLines.length; i++){

        WriteToDebug("Processing Line #" + i + ": " + aryAICCResponseLines[i]);

        strLine = aryAICCResponseLines[i];

        strLineName = "";
        strLineValue = "";

        if (strLine.length > 0){

            WriteToDebug("Found non-zero length string");

            //remove \r from the beginning or end of the string in case we missed it in the original array split
            if (strLine.charAt(0) == "\r"){
                WriteToDebug("Detected leading \\r");
                strLine = strLine.substr(1);
            }
            if (strLine.charAt(strLine.length - 1) == "\r"){
                WriteToDebug("Detected trailing \\r");
                strLine = strLine.substr(0, strLine.length - 1);
            }

            if (strLine.charAt(0) != ";") {             //semi-colon indicates a comment line, ignore these
                WriteToDebug("Found non-comment line");

                strLineName  = GetNameFromAICCLine(strLine);
                strLineValue = GetValueFromAICCLine(strLine);

                WriteToDebug("strLineName=" + strLineName + ", strLineValue=" + strLineValue);
            }
        }

        strLineName = strLineName.toLowerCase();

        if (! AICC_HasItemBeenFound(strLineName)){      //only process an item the first time it is found since only the first instance is significant

            WriteToDebug("Detected an un-found item");

            AICC_FoundItem(strLineName);

            switch (strLineName){

                case "version":             //version
                    WriteToDebug("Item is version");
                    var tempVersion = parseFloat(strLineValue);

                    if (isNaN(tempVersion)){tempVersion=0;}

                    AICC_LMS_Version = tempVersion;

                break;

                case "student_id":              //student id
                    WriteToDebug("Item is student_id");
                    AICC_Student_ID = strLineValue;
                break;

                case "student_name":            //student name
                    WriteToDebug("Item is student_name");
                    AICC_Student_Name = strLineValue;
                break;

                case "lesson_location":         //bookmark
                    WriteToDebug("Item is lesson_location");
                    AICC_Lesson_Location = strLineValue;
                break;

                case "score":                   //score
                    WriteToDebug("Item is score");
                    AICC_Score = strLineValue;

                    AICC_SeperateScoreValues(AICC_Score);

                break;

                case "credit":                  //credit
                    WriteToDebug("Item is credit");
                    AICC_Credit = strLineValue;

                    AICC_TranslateCredit(AICC_Credit);

                break;

                case "lesson_status":           //status, flag for entry mode
                    WriteToDebug("Item is lesson_status");
                    AICC_Lesson_Status = strLineValue;

                    AICC_TranslateLessonStatus(AICC_Lesson_Status);

                break;

                case "time":                    //previous time
                    WriteToDebug("Item is time");
                    AICC_Time = strLineValue;

                    AICC_TranslateTimeToMilliseconds(AICC_Time);

                break;

                case "mastery_score":           //passing score
                    WriteToDebug("Item is mastery_score");
                    AICC_Mastery_Score = strLineValue;

                    AICC_ValidateMasteryScore(AICC_Mastery_Score);
                break;

                case "lesson_mode":             //lesson mode (browse, normal, review)
                    WriteToDebug("Item is lesson_mode");
                    AICC_Lesson_Mode = strLineValue;

                    AICC_TranslateLessonMode(AICC_Lesson_Mode);

                break;

                case "max_time_allowed":        //max time allowed
                    WriteToDebug("Item is max_time_allowed");
                    AICC_Max_Time_Allowed = strLineValue;

                    AICC_TranslateMaxTimeToMilliseconds(AICC_Max_Time_Allowed);

                break;

                case "time_limit_action":       //display message on timeout, exit on timeout
                    WriteToDebug("Item is time_limit_action");
                    AICC_Time_Limit_Action = strLineValue;

                    AICC_TranslateTimeLimitAction(AICC_Time_Limit_Action);

                break;

                case "audio":                   //audio play, audio speed
                    WriteToDebug("Item is audio");
                    AICC_Audio = strLineValue;

                    AICC_TranslateAudio(AICC_Audio);

                break;

                case "speed":                   //content speed
                    WriteToDebug("Item is speed");
                    AICC_Speed = strLineValue;

                    AICC_TranslateSpeed(AICC_Speed);

                break;

                case "language":                //language
                    WriteToDebug("Item is language");
                    AICC_Language = strLineValue;

                break;

                case "text":                    //text
                    WriteToDebug("Item is text");
                    AICC_Text = strLineValue;

                    AICC_TranslateTextPreference(AICC_Text);

                break;

                case "course_id":               //course id
                    WriteToDebug("Item is course id");
                    AICC_CourseID = strLineValue;

                break;

                case "core_vendor":             //launch data
                    WriteToDebug("Item is core_vendor");

                    AICC_Launch_Data = "";

                    strLine = "";
                    j=1;

                    if ((i+j) < aryAICCResponseLines.length){
                        strLine = aryAICCResponseLines[i+j];
                    }

                    //loop to the end of the file or current group
                    while ( ((i+j) < aryAICCResponseLines.length) && (! IsGroupIdentifier(strLine))){

                        if (strLine.charAt(0) != ";"){
                            AICC_Launch_Data += strLine + "\n";     //add \n to make up for the one we dropped when splitting the string into the array
                        }

                        j = j + 1;

                        if ((i+j) < aryAICCResponseLines.length){
                            strLine = aryAICCResponseLines[i+j];
                        }
                    }

                    i = i + j - 1

                    AICC_Launch_Data = AICC_Launch_Data.replace(/\s*$/, "");        //replace trailing whitespace (we've added an extra \n to the end of the string)

                break;

                case "core_lesson":             //data chunk
                    WriteToDebug("Item is core_lesson");

                    AICC_Data_Chunk = "";

                    strLine = "";
                    j=1;

                    if ((i+j) < aryAICCResponseLines.length){
                        strLine = aryAICCResponseLines[i+j];
                    }

                    //loop to the end of the file or current group
                    while ( ((i+j) < aryAICCResponseLines.length) && (! IsGroupIdentifier(strLine))){

                        if (strLine.charAt(0) != ";"){
                            AICC_Data_Chunk += strLine + "\n";      //add \n to make up for the one we dropped when splitting the string into the array
                        }

                        j = j + 1;

                        if ((i+j) < aryAICCResponseLines.length){
                            strLine = aryAICCResponseLines[i+j];
                        }
                    }

                    i = i + j - 1

                    AICC_Data_Chunk = AICC_Data_Chunk.replace(/\s*$/, "");      //replace trailing whitespace (we've added an extra \n to the end of the string)
                break;

                case "comments":                //comments from LMS
                    WriteToDebug("Item is comments");

                    AICC_Comments = "";

                    strLine = "";
                    j=1;

                    if ((i+j) < aryAICCResponseLines.length){
                        strLine = aryAICCResponseLines[i+j];
                    }

                    //loop to the end of the file or current group
                    while ( ((i+j) < aryAICCResponseLines.length) && (! IsGroupIdentifier(strLine))){

                        if (strLine.charAt(0) != ";"){
                            AICC_Comments += strLine + "\n";        //add \n to make up for the one we dropped when splitting the string into the array
                        }

                        j = j + 1;

                        if ((i+j) < aryAICCResponseLines.length){
                            strLine = aryAICCResponseLines[i+j];
                        }
                    }

                    i = i + j - 1

                    AICC_Comments = AICC_Comments.replace(/\s*$/, "");      //replace trailing whitespace (we've added an extra \n to the end of the string)

                break;

                case "objectives_status":       //objectives
                    WriteToDebug("Item is objectives_status");

                    AICC_Objectives = "";

                    strLine = "";
                    j=1;

                    if ((i+j) < aryAICCResponseLines.length){
                        strLine = aryAICCResponseLines[i+j];
                    }

                    //loop to the end of the file or current group
                    while ( ((i+j) < aryAICCResponseLines.length) && (! IsGroupIdentifier(strLine))){

                        if (strLine.charAt(0) != ";"){
                            AICC_Objectives += strLine + "\n";      //add \n to make up for the one we dropped when splitting the string into the array
                        }

                        j = j + 1;

                        if ((i+j) < aryAICCResponseLines.length){
                            strLine = aryAICCResponseLines[i+j];
                        }
                    }

                    i = i + j - 1
                    AICC_Objectives = AICC_Objectives.replace(/\s*$/, "");      //replace trailing whitespace (we've added an extra \r\n to the end of the string)

                    AICC_FormatObjectives(AICC_Objectives);

                break;

                default:
                    //comment or empty line so do nothing
                    WriteToDebug("Unknown Item Found");
                break;

            }   //end switch
        }   //end if AICC_HasItemBeenFound
    }   //end for


    return true;

}


function IsGroupIdentifier(strLine){
    WriteToDebug("In IsGroupIdentifier, strLine=" + strLine);

    var intPos;

    //remove leading white space
    strLine = strLine.replace(/^\s*/, "");

    intPos = strLine.search(/\[[\w]+\]/);

    WriteToDebug("intPos=" + intPos);

    if (intPos == 0){
        WriteToDebug("Returning True");
        return true;
    }
    else{
        WriteToDebug("Returning False");
        return false;
    }
}

function AICC_FoundItem(strItem){
    WriteToDebug("In AICC_FoundItem, strItem=" + strItem);
    aryAICCFoundItems[strItem] = true;
}

function AICC_HasItemBeenFound(strItem){
    WriteToDebug("In AICC_HasItemBeenFound, strItem=" + strItem);

    if (aryAICCFoundItems[strItem] == true){
        WriteToDebug("Returning True");
        return true;
    }
    else{
        WriteToDebug("Returning False");
        return false;
    }
}



//==================================================================
//functions for dealing with specific data elements
//==================================================================
function AICC_SeperateScoreValues(AICC_Score){
    WriteToDebug("In AICC_SeperateScoreValues, AICC_Score=" + AICC_Score);

    var aryScores;
    aryScore = AICC_Score.split(",");

    AICC_fltScoreRaw = aryScore[0];

    if (IsValidDecimal(AICC_fltScoreRaw)){
        WriteToDebug("Found a valid decimal");
        AICC_fltScoreRaw = parseFloat(AICC_fltScoreRaw);
    }
    else{
        WriteToDebug("ERROR - score from LMS is not a valid decimal");
        AICC_SetError(AICC_ERROR_INVALID_DECIMAL, "score is not a valid decimal")
    }

    if (aryScore.length > 1){
        WriteToDebug("Max score found");
        AICC_fltScoreMax = aryScore[1];
        if ( IsValidDecimal(AICC_fltScoreMax)){
            WriteToDebug("Found a valid decimal");
            AICC_fltScoreMax = parseFloat(AICC_fltScoreMax);
        }
        else{
            WriteToDebug("ERROR - max score from LMS is not a valid decimal");
            AICC_SetError(AICC_ERROR_INVALID_DECIMAL, "max score is not a valid decimal")
        }
    }

    if (aryScore.length > 2){
        WriteToDebug("Max score found");
        AICC_fltScoreMin = aryScore[2];
        if (IsValidDecimal(AICC_fltScoreMin)){
            WriteToDebug("Found a valid decimal");
            AICC_fltScoreMin = parseFloat(AICC_fltScoreMin);
        }
        else{
            WriteToDebug("ERROR - min score from LMS is not a valid decimal");
            AICC_SetError(AICC_ERROR_INVALID_DECIMAL, "min score is not a valid decimal")
        }

    }

}


function AICC_ValidateMasteryScore(strScore){
    WriteToDebug("In AICC_ValidateMasteryScore, strScore=" + strScore);

    if (IsValidDecimal(strScore)){
        AICC_Mastery_Score = parseFloat(strScore);
    }
    else{
        WriteToDebug("ERROR - mastery score from LMS is not a valid decimal");
        AICC_SetError(AICC_ERROR_INVALID_DECIMAL, "mastery score is not a valid decimal")
    }

}

function AICC_TranslateCredit(strCredit){
    WriteToDebug("In AICC_TranslateCredit, strCredit=" + strCredit);

    var strFirstChar;

    strFirstChar = strCredit.toLowerCase().charAt(0);

    if (strFirstChar == "c"){
        WriteToDebug("Credit = true");
        AICC_blnCredit = true;
    }
    else if (strFirstChar == "n"){
        WriteToDebug("Credit = false");
        AICC_blnCredit = false
    }
    else{
        WriteToDebug("ERROR - credit value from LMS is not a valid");
        AICC_SetError(AICC_ERROR_INVALID_CREDIT, "credit value from LMS is not a valid")
    }

}

function AICC_TranslateLessonMode(strMode){
    WriteToDebug("In AICC_TranslateLessonMode, strMode=" + strMode);
    var strFirstChar;

    strFirstChar = strMode.toLowerCase().charAt(0);

    if (strFirstChar == "b"){
        WriteToDebug("Lesson Mode = Browse");
        AICC_strLessonMode = MODE_BROWSE;
    }
    else if (strFirstChar == "n"){
        WriteToDebug("Lesson Mode = normal");
        AICC_strLessonMode = MODE_NORMAL;
    }
    else if (strFirstChar == "r"){
        WriteToDebug("Lesson Mode = review");
        AICC_strLessonMode = MODE_REVIEW;

        if (!(typeof(REVIEW_MODE_IS_READ_ONLY) == "undefined") && REVIEW_MODE_IS_READ_ONLY === true){
            blnReviewModeSoReadOnly = true;
        }

    }
    else{
        WriteToDebug("ERROR - lesson_mode value from LMS is not a valid");
        AICC_SetError(AICC_ERROR_INVALID_LESSON_MODE, "lesson_mode value from LMS is not a valid")

    }
}


function AICC_TranslateTimeToMilliseconds(strCMITime){
    WriteToDebug("In AICC_TranslateTimeToMilliseconds, strCMITime=" + strCMITime);

    if (IsValidCMITimeSpan(strCMITime)){
        AICC_intPreviouslyAccumulatedMilliseconds = ConvertCMITimeSpanToMS(strCMITime);
    }
    else{
        WriteToDebug("ERROR - Invalid CMITimeSpan");
        AICC_SetError(AICC_ERROR_INVALID_TIMESPAN, "Invalid timespan (previously accumulated time) received from LMS");
    }

}

function AICC_TranslateMaxTimeToMilliseconds(strCMITime){
    WriteToDebug("In AICC_TranslateMaxTimeToMilliseconds, strCMITime=" + strCMITime);

    if (IsValidCMITimeSpan(strCMITime)){
        AICC_intMaxTimeAllowedMilliseconds = ConvertCMITimeSpanToMS(strCMITime);
    }
    else{
        WriteToDebug("ERROR - Invalid CMITimeSpan");
        AICC_SetError(AICC_ERROR_INVALID_TIMESPAN, "Invalid timespan (max time allowed) received from LMS");
    }

}

function AICC_TranslateTimeLimitAction(strTimeLimitAction){
    WriteToDebug("In AICC_TranslateTimeLimitAction, strTimeLimitAction=" + strTimeLimitAction);

    var arySplit;
    var blnError = false;
    var strChar1 = "";
    var strChar2 = "";

    //note - order of characters is not significant

    arySplit = strTimeLimitAction.split(",");

    if (arySplit.length == 2){

        WriteToDebug("Found 2 elements");

        strChar1 = arySplit[0].charAt(0).toLowerCase();
        strChar2 = arySplit[1].charAt(0).toLowerCase();

        WriteToDebug("Got characters, strChar1=" + strChar1 + ", strChar2=" + strChar2);

        if ( (strChar1 != "e" && strChar1 != "c" && strChar1 != "m" && strChar1 != "n") ||
             (strChar2 != "e" && strChar2 != "c" && strChar2 != "m" && strChar2 != "n") ||
             (strChar1 == strChar2)
           ) {
                blnError = true
                WriteToDebug("Found an invalid character, or 2 identical characters");
           }

        if (strChar1 == "e" || strChar2 == "e") {AICC_blnExitOnTimeout = true;}
        if (strChar1 == "c" || strChar2 == "c") {AICC_blnExitOnTimeout = false;}

        if (strChar1 == "n" || strChar2 == "n") {AICC_blnShowMessageOnTimeout = false;}
        if (strChar1 == "m" || strChar2 == "m") {AICC_blnShowMessageOnTimeout = true;}

        WriteToDebug("AICC_blnExitOnTimeout=" + AICC_blnExitOnTimeout + ", AICC_blnShowMessageOnTimeout" + AICC_blnShowMessageOnTimeout);
    }
    else{
        WriteToDebug("Line does not contain two comma-delimited elements");
        blnError = true;
    }

    if (blnError){
        WriteToDebug("ERROR - Invalid Time Limit Action");
        AICC_SetError(AICC_ERROR_INVALID_TIME_LIMIT_ACTION, "Invalid time limit action received from LMS");

    }

}


function AICC_TranslateTextPreference(strPreference){
    WriteToDebug("In AICC_TranslateTextPreference, strPreference=" + strPreference);

    if (strPreference == -1){
        WriteToDebug("Text Preference = off");
        AICC_TextPreference = PREFERENCE_OFF;
    }
    else if (strPreference == 0){
        WriteToDebug("Text Preference = default");
        AICC_TextPreference = PREFERENCE_DEFAULT;
    }
    else if (strPreference == 1){
        WriteToDebug("Text Preference = on");
        AICC_TextPreference = PREFERENCE_ON;
    }
    else{
        WriteToDebug("ERROR - Invalid Text Preference");
        AICC_SetError(AICC_ERROR_INVALID_PREFERENCE, "Invalid Text Preference received from LMS");

    }
}


function AICC_TranslateLessonStatus(strStatus){
    WriteToDebug("In AICC_TranslateLessonStatus, strStatus=" + strStatus);

    var strFirstChar;
    var intPos;
    var strEntry;

    strFirstChar = strStatus.charAt(0).toLowerCase();

    AICC_Status = AICC_ConvertAICCStatusIntoLocalStatus(strFirstChar);

    WriteToDebug("AICC_Status=" + AICC_Status);

    //check for an entry flag
    intPos = strStatus.indexOf(",");
    if (intPos > 0){
        strEntry = strStatus.substr(intPos);
        strEntry = strEntry.replace(/,/, "");

        strFirstChar = strEntry.charAt(0).toLowerCase();

        if (strFirstChar == "a"){
            WriteToDebug("Entry is Ab initio");
            AICC_Entry = ENTRY_FIRST_TIME;
        }
        else if (strFirstChar == "r"){
            WriteToDebug("Entry is Resume");
            AICC_Entry = ENTRY_RESUME;
        }
        else{
            WriteToDebug("ERROR - entry not found");
            AICC_SetError(AICC_ERROR_INVALID_ENTRY, "Invalid lesson status received from LMS");
        }
    }

}


function AICC_ConvertAICCStatusIntoLocalStatus(strFirstCharOfAICCStatus){
    WriteToDebug("In AICC_ConvertAICCStatusIntoLocalStatus, strFirstCharOfAICCStatus=" + strFirstCharOfAICCStatus);

    if (strFirstCharOfAICCStatus == "p"){
        WriteToDebug("Status is Passed");
        return LESSON_STATUS_PASSED;
    }
    else if (strFirstCharOfAICCStatus == "f"){
        WriteToDebug("Status is Failed");
        return LESSON_STATUS_FAILED;
    }
    else if (strFirstCharOfAICCStatus == "c"){
        WriteToDebug("Status is Completed");
        return LESSON_STATUS_COMPLETED;
    }
    else if (strFirstCharOfAICCStatus == "b"){
        WriteToDebug("Status is Browsed");
        return LESSON_STATUS_BROWSED;
    }
    else if (strFirstCharOfAICCStatus == "i"){
        WriteToDebug("Status is Incomplete");
        return LESSON_STATUS_INCOMPLETE;
    }
    else if (strFirstCharOfAICCStatus == "n"){
        WriteToDebug("Status is Not Attempted");
        return LESSON_STATUS_NOT_ATTEMPTED;
    }
    else{
        WriteToDebug("ERROR - status not found");
        AICC_SetError(SCORM_ERROR_INVALID_STATUS, "Invalid status");
        return LESSON_STATUS_NOT_ATTEMPTED;
    }
}


function AICC_TranslateAudio(strAudio){

    WriteToDebug("In AICC_TranslateAudio, strAudio=" + strAudio);

    var intTempPreference = parseInt(strAudio, 10);

    WriteToDebug("intTempPreference=" + intTempPreference);

    if (intTempPreference > 0 && intTempPreference <= 100){
        WriteToDebug("Returning On");
        AICC_AudioPlayPreference = PREFERENCE_ON;
        AICC_intAudioVolume = intTempPreference;
    }
    else if (intTempPreference == 0){
        WriteToDebug("Returning Default");
        AICC_AudioPlayPreference = PREFERENCE_DEFAULT;
    }
    else if (intTempPreference < 0) {
        WriteToDebug("returning Off");
        AICC_AudioPlayPreference = PREFERENCE_OFF;
    }
    else{
        //preference is not a number
        WriteToDebug("Error: Invalid preference");
        AICC_SetError(AICC_ERROR_INVALID_PREFERENCE, "Invalid audio preference received from LMS");
    }
}


function AICC_TranslateSpeed(intAICCSpeed){

    WriteToDebug("In AICC_TranslateSpeed, intAICCSpeed=" + intAICCSpeed);

    var intPercentOfMax;

    if ( ! ValidInteger(intAICCSpeed) ){
        WriteToDebug("ERROR - invalid integer");
        AICC_SetError(AICC_ERROR_INVALID_SPEED, "Invalid speed preference received from LMS - not an integer");
        return;
    }

    intAICCSpeed = parseInt(intAICCSpeed, 10)

    if (intAICCSpeed < -100 || intAICCSpeed > 100){
        WriteToDebug("ERROR - out of range");
        AICC_SetError(AICC_ERROR_INVALID_SPEED, "Invalid speed preference received from LMS - out of range");
        return;

    }

    AICC_Speed = intAICCSpeed;

    intPercentOfMax = (intAICCSpeed + 100) / 2;
    intPercentOfMax = parseInt(intPercentOfMax, 10);

    WriteToDebug("Returning " + intPercentOfMax);

    AICC_intPercentOfMaxSpeed = intPercentOfMax;

}



function AICC_FormatObjectives(strObjectivesFromLMS){

    WriteToDebug("In AICC_FormatObjectives, strObjectivesFromLMS=" + strObjectivesFromLMS);

    var aryLines;
    var i;
    var strLineName;
    var strLineValue;
    var strLineType;
    var strIndex;

    aryLines = strObjectivesFromLMS.split("\n");

    //establish the read array with empty elements for all the IDs we'll encounter
    for (i=0; i < aryLines.length; i++){
        WriteToDebug("Extracting Index From Line: " + aryLines[i]);

        strLineName  = GetNameFromAICCLine(aryLines[i]);
        strIndex = GetIndexFromAICCName(strLineName);

        strIndex = parseInt(strIndex, 10);

        WriteToDebug("strIndex: " + strIndex);

        AICC_aryObjectivesRead[parseInt(strIndex, 10)] = new Array(3);
    }


    //loop through the line again and now populate the actual data
    for (i=0; i < aryLines.length; i++){
        WriteToDebug("Populating Line " + aryLines[i]);

        strLineName  = GetNameFromAICCLine(aryLines[i]);
        strLineValue = GetValueFromAICCLine(aryLines[i]);

        strIndex = GetIndexFromAICCName(strLineName);

        strIndex = strIndex;

        WriteToDebug("strLineName: " + strLineName);
        WriteToDebug("strLineValue: " + strLineValue);
        WriteToDebug("strIndex: " + strIndex);

        strLineType = strLineName.substr(0,4).toLowerCase();

        if (strLineType == "j_id"){
            WriteToDebug("Found ID");
            AICC_aryObjectivesRead[parseInt(strIndex, 10)][AICC_OBJ_ARRAY_ID] = strLineValue;
        }
        else if (strLineType =="j_st"){
            WriteToDebug("Found Status");
            AICC_aryObjectivesRead[parseInt(strIndex, 10)][AICC_OBJ_ARRAY_STATUS] = AICC_ConvertAICCStatusIntoLocalStatus(strLineValue.charAt(0).toLowerCase());
        }
        else if (strLineType == "j_sc"){
            WriteToDebug("Found Score");
            AICC_aryObjectivesRead[parseInt(strIndex, 10)][AICC_OBJ_ARRAY_SCORE] = AICC_ExtractSingleScoreFromObjective(strLineValue);
        }
        else{
            WriteToDebug("WARNING - unidentified objective data found - " + aryLines[i]);
        }

    }

}


function AICC_ExtractSingleScoreFromObjective(strLineValue){
    WriteToDebug("In AICC_ExtractSingleScoreFromObjective, strLineValue=" + strLineValue);

    //AICC objectives can have multiple score sets (raw,max,min) seperated by semi-colons,they represent the various attempts at an objective
    //we're just concerned with the most recent attempt

    var aryParts;

    aryParts = strLineValue.split(";");     //drop the previous instances if any
    aryParts = aryParts[0].split(",");      //drop the max and min scores if any


    WriteToDebug("returning " + aryParts[0]);

    return aryParts[0];

}

function FindObjectiveById(strID, aryObjectives){

    WriteToDebug("In FindObjectiveById, strID=" + strID);

    for (var i=0; i <= aryObjectives.length; i++){

        WriteToDebug("Searching element " + i);

        if (aryObjectives[i]){
            WriteToDebug("Element Exists");

            if (aryObjectives[i][AICC_OBJ_ARRAY_ID].toString() == strID.toString()){
                WriteToDebug("Element matches");
                return i;
            }
        }
    }

    return null;
}

function AICC_CreateValidIdentifier(str) {
    return CreateValidIdentifierLegacy(str);
}

//___________________________________________________________
//Interaction Retrieval Functionality
//NOTE ON INTERACTION RETRIEVAL
//A.  It is only available in certain standards, standards where it is unavailable will return nothing
//B.  The interaction records are currently reported using "journaling", whereby each entry is appended
//      Retrieval methods will retrieve only the most recent value


//___________________________________________________________
//Helper Methods
function AICC_FindInteractionIndexFromID(strInteractionID){

    WriteToDebug("AICC_FindInteractionIndexFromID - AICC does not support interaction retrieval, returning null");

    return null;
}
//___________________________________________________________


function AICC_GetInteractionType(strInteractionID)
{

    WriteToDebug("AICC_GetInteractionType - AICC does not support interaction retrieval, returning empty string");
    return '';


}

//public
function AICC_GetInteractionTimestamp(strInteractionID)
{
    WriteToDebug("AICC_GetInteractionTimestamp - AICC does not support interaction retrieval, returning empty string");
    return '';
}



//public
function AICC_GetInteractionCorrectResponses(strInteractionID)
{

    WriteToDebug("AICC_GetInteractionCorrectResponses - AICC does not support interaction retrieval, returning empty array");
    return new Array();


}



//public
function AICC_GetInteractionWeighting(strInteractionID)
{
    WriteToDebug("AICC_GetInteractionWeighting - AICC does not support interaction retrieval, returning empty string");
    return '';

}



//public
function AICC_GetInteractionLearnerResponses(strInteractionID)
{
    WriteToDebug("AICC_GetInteractionLearnerResponses - AICC does not support interaction retrieval, returning empty array");
    return new Array();


}


//public
function AICC_GetInteractionResult(strInteractionID)
{
    WriteToDebug("AICC_GetInteractionResult - AICC does not support interaction retrieval, returning empty string");
    return '';

}



//public
function AICC_GetInteractionLatency(strInteractionID)
{
    WriteToDebug("AICC_GetInteractionDescription - AICC does not support interaction retrieval, returning empty string");
    return '';

}



//public
function AICC_GetInteractionDescription(strInteractionID)
{
    WriteToDebug("AICC_GetInteractionDescription - AICC does not support interaction retrieval, returning empty string");
    return '';
}

//________________________________________________

//public
function AICC_CreateDataBucket(strBucketId, intMinSize, intMaxSize){
    WriteToDebug("AICC_CreateDataBucket - AICC does not support SSP, returning false");
    return false;
}

//public
function AICC_GetDataFromBucket(strBucketId){
    WriteToDebug("AICC_GetDataFromBucket - AICC does not support SSP, returning empty string");
    return "";
}

//public
function AICC_PutDataInBucket(strBucketId, strData, blnAppendToEnd){
    WriteToDebug("AICC_PutDataInBucket - AICC does not support SSP, returning false");
    return false;
}

//public
function AICC_DetectSSPSupport(){
    WriteToDebug("AICC_DetectSSPSupport - AICC does not support SSP, returning false");
    return false;
}

//public
function AICC_GetBucketInfo(strBucketId){
    WriteToDebug("AICC_DetectSSPSupport - AICC does not support SSP, returning empty SSPBucketSize");
    return new SSPBucketSize(0, 0);
}


function AICC_SetNavigationRequest(strNavRequest){
    WriteToDebug("AICC_SetNavigationRequest - AICC does not support navigation requests, returning false");
    return false;
}

function AICC_GetNavigationRequest(){
    WriteToDebug("AICC_SetNavigationRequest - AICC does not support navigation requests, returning false");
    return false;
}

//==================================================================
//==================================================================

function FormAICCPostData(){
    WriteToDebug("In FormAICCPostData");

    var strAICCData = "";


    strAICCData += "[Core]\r\n";

    strAICCData += "Lesson_Location=" + AICC_Lesson_Location + "\r\n";
    strAICCData += "Lesson_Status=" + AICC_TranslateLessonStatusToAICC(AICC_Status) + "\r\n";
    strAICCData += "Score=" + AICC_TranslateScoreToAICC() + "\r\n";
    strAICCData += "Time=" + AICC_TranslateTimeToAICC() + "\r\n";

    strAICCData += "[Comments]\r\n" + AICC_TranslateCommentsToAICC() + "\r\n";

    strAICCData += "[Objectives_Status]\r\n" + AICC_TranslateObjectivesToAICC() + "\r\n";

    strAICCData += "[Student_Preferences]\r\n";

    strAICCData += "Audio=" + AICC_TranslateAudioToAICC() + "\r\n";
    strAICCData += "Language=" + AICC_Language + "\r\n";
    strAICCData += "Speed=" + AICC_TranslateSpeedToAICC() + "\r\n";
    strAICCData += "Text=" + AICC_TranslateTextToAICC() + "\r\n";

    strAICCData += "[Core_Lesson]\r\n";
    strAICCData += AICC_Data_Chunk;

    WriteToDebug("FormAICCPostData returning: " + strAICCData);

    return strAICCData;
}

function AICC_TranslateLessonStatusToAICC(intStatus){

    WriteToDebug("In AICC_TranslateLessonStatusToAICC");

    switch (intStatus){

        case LESSON_STATUS_PASSED:
            WriteToDebug("Status is passed");
            AICC_Lesson_Status = "P";
        break;

        case LESSON_STATUS_COMPLETED:
            WriteToDebug("Status is completed");
            AICC_Lesson_Status = "C";
        break;

        case LESSON_STATUS_FAILED:
            WriteToDebug("Status is failed");
            AICC_Lesson_Status = "F";
        break;

        case LESSON_STATUS_INCOMPLETE:
            WriteToDebug("Status is incomplete");
            AICC_Lesson_Status = "I";
        break;

        case LESSON_STATUS_BROWSED:
            WriteToDebug("Status is browsed");
            AICC_Lesson_Status = "B";
        break;

        case LESSON_STATUS_NOT_ATTEMPTED:
            WriteToDebug("Status is not attempted");
            AICC_Lesson_Status = "N";
        break;
    }

    return AICC_Lesson_Status;
}

function AICC_TranslateScoreToAICC(){

    WriteToDebug("In AICC_TranslateScoreToAICC");

    AICC_Score = AICC_fltScoreRaw;

    //prior to version 3, AICC scores cannot contain a decimal
    if (AICC_LMS_Version < 3 && AICC_fltScoreRaw != ""){
        AICC_Score = parseInt(AICC_Score, 10);
    }

    //some older versions may not have this setting available
    if (    (AICC_REPORT_MIN_MAX_SCORE === undefined ||
             AICC_REPORT_MIN_MAX_SCORE === null ||
             AICC_REPORT_MIN_MAX_SCORE === true) &&
            (AICC_LMS_Version >= 3 )        //min and max scores are only allowed after version 3
        ){

        WriteToDebug("Using max and min values if available.");

        if ((AICC_fltScoreMax != "") || (AICC_fltScoreMin != "")) {
            WriteToDebug("Appending Max and Min scores");
            AICC_Score += "," + AICC_fltScoreMax + "," + AICC_fltScoreMin;
        }

    }

    WriteToDebug("AICC_Score=" + AICC_Score);

    return AICC_Score;
}

function AICC_TranslateTimeToAICC(){

    WriteToDebug("In AICC_TranslateTimeToAICC");

    var strTime;

    strTime = ConvertMilliSecondsToSCORMTime(AICC_intSessionTimeMilliseconds, false);

    return strTime;
}

function AICC_TranslateCommentsToAICC(){

    WriteToDebug("In AICC_TranslateCommentsToAICC");

    var strComments = "";
    for (var i=0; i < AICC_aryCommentsFromLearner.length; i++){
        strComments += "<" + (i+1) + ">" + AICC_aryCommentsFromLearner[i] + "<e." + (i+1) + ">";
    }
    return strComments;
}

function AICC_TranslateObjectivesToAICC(){

    WriteToDebug("In AICC_TranslateObjectivesToAICC");

    var strObjectives = "";

    for (var i=0; i<AICC_aryObjectivesWrite.length; i++){

        WriteToDebug("Looking at index: " + i);

        if (AICC_aryObjectivesWrite[i]){
            WriteToDebug("Element " + i + " exists, id=" + AICC_aryObjectivesWrite[i][AICC_OBJ_ARRAY_ID] + ", score=" + AICC_aryObjectivesWrite[i][AICC_OBJ_ARRAY_SCORE] + ", status=" + AICC_aryObjectivesWrite[i][AICC_OBJ_ARRAY_STATUS]);

            strObjectives += "J_ID." + (i+1) + "=" + AICC_aryObjectivesWrite[i][AICC_OBJ_ARRAY_ID] + "\r\n";

            if (AICC_aryObjectivesWrite[i][AICC_OBJ_ARRAY_SCORE] != ""){
                strObjectives += "J_Score." + (i+1) + "=" + AICC_aryObjectivesWrite[i][AICC_OBJ_ARRAY_SCORE] + "\r\n";
            }

            if (AICC_aryObjectivesWrite[i][AICC_OBJ_ARRAY_STATUS] != ""){
                strObjectives += "J_Status." + (i+1) + "=" + AICC_TranslateLessonStatusToAICC(AICC_aryObjectivesWrite[i][AICC_OBJ_ARRAY_STATUS]) + "\r\n";
            }
        }
    }

    return strObjectives;
}

function AICC_TranslateAudioToAICC(){

    WriteToDebug("In AICC_TranslateAudioToAICC");

    var strReturn;

    switch (AICC_AudioPlayPreference){

        case PREFERENCE_ON:
            WriteToDebug("Preference is ON");
            strReturn = AICC_intAudioVolume;
        break;

        case PREFERENCE_DEFAULT:
            WriteToDebug("Preference is DEFAULT");
            strReturn = 0;
        break;

        case PREFERENCE_OFF:
            WriteToDebug("Preference is OFF");
            strReturn = -1;
        break;
    }

    return strReturn;
}



function AICC_TranslateSpeedToAICC(){
    WriteToDebug("In AICC_TranslateSpeedToAICC");

    var intAICCSpeed;

    intAICCSpeed = (AICC_intPercentOfMaxSpeed * 2) - 100;

    return intAICCSpeed;
}

function AICC_TranslateTextToAICC(){
    WriteToDebug("In AICC_TranslateTextToAICC");

    var strPreference = 0;

    if (AICC_TextPreference == PREFERENCE_OFF){
        strPreference = -1;
    }
    else if (AICC_TextPreference == PREFERENCE_DEFAULT){
        strPreference = 0;
    }
    else if (AICC_TextPreference == PREFERENCE_ON){
        strPreference = 1;
    }

    return strPreference;
}



//==================================================================
//interactions
//==================================================================


function FormAICCInteractionsData(){

    WriteToDebug("In FormAICCInteractionsData");

    var strInteractions;
    var strDate;
    var strTime;
    var strResult = "";

    strInteractions = '"course_id","student_id","lesson_id","date","time","interaction_id",' +
                      '"objective_id","type_interaction","correct_response","student_response",' +
                      '"result","weighting","latency"\r\n';

    var blnCorrect = "";
    var strResponse = "";
    var strCorrectResponse = "";
    var strLatency = "";

    for (var i=0; i < AICC_aryInteractions.length; i++){

        blnCorrect = AICC_aryInteractions[i][AICC_INTERACTIONS_CORRECT];
        strResult = "";

        //need to leave support for blnCorrect=t/f for legacy implementations of RSECA
        //if it's null, leave it as "" (not every interaction is correct/incorrect

        if (blnCorrect == true || blnCorrect == INTERACTION_RESULT_CORRECT){
            strResult = AICC_RESULT_CORRECT;
        }
        else if (blnCorrect == "false" || blnCorrect == INTERACTION_RESULT_WRONG){          //compare against the string "false" because ("" == false) evaluates to true
            strResult = AICC_RESULT_WRONG;
        }
        else if (blnCorrect == INTERACTION_RESULT_UNANTICIPATED){
            strResult = AICC_RESULT_UNANTICIPATED;
        }
        else if (blnCorrect == INTERACTION_RESULT_NEUTRAL){
            strResult = AICC_RESULT_NEUTRAL;
        }

        strDate = ConvertDateToCMIDate(AICC_aryInteractions[i][AICC_INTERACTIONS_TIME_STAMP]);
        strTime = ConvertDateToCMITime(AICC_aryInteractions[i][AICC_INTERACTIONS_TIME_STAMP]);


        if (blnUseLongInteractionResultValues == true){
            strResponse = AICC_aryInteractions[i][AICC_INTERACTIONS_RESPONSE_LONG];
            strCorrectResponse = AICC_aryInteractions[i][AICC_INTERACTIONS_CORRECT_RESPONSE_LONG];
        }
        else{
            strResponse = AICC_aryInteractions[i][AICC_INTERACTIONS_RESPONSE];
            strCorrectResponse = AICC_aryInteractions[i][AICC_INTERACTIONS_CORRECT_RESPONSE];
        }

        strResponse = new String(strResponse);
        strCorrectResponse = new String(strCorrectResponse);

        var tempLatency = AICC_aryInteractions[i][AICC_INTERACTIONS_LATENCY];

        if (tempLatency !== null && tempLatency !== undefined && tempLatency != ""){
            strLatency = ConvertMilliSecondsToSCORMTime(tempLatency, false);
        }

        strInteractions += '"' + AICC_CourseID.replace("\"", "") + '","' + AICC_Student_ID.replace("\"", "") + '","' + AICC_LESSON_ID.replace("\"", "") + '","' +
                           strDate + '","' + strTime + '","' + AICC_aryInteractions[i][AICC_INTERACTIONS_ID].replace("\"", "") + '",' +
                           '""' + ',"' + AICC_aryInteractions[i][AICC_INTERACTIONS_TYPE] + '","' + strCorrectResponse.replace("\"", "") + '","' +
                           strResponse.replace("\"", "") + '","' + strResult + '","' +
                           AICC_aryInteractions[i][AICC_INTERACTIONS_WEIGHTING] + '","' + strLatency + '"\r\n';
    }

    return strInteractions;
}


/*
moved to UtilityFunctions.js ConvertToCMIDate
function ConvertToCMIDate(dtmDate){

    WriteToDebug("In ConvertToCMIDate");

    var strYear;
    var strMonth;
    var strDay;
    var strReturn;

    dtmDate = new Date(dtmDate);

    strYear = dtmDate.getFullYear()
    strMonth = dtmDate.getMonth();
    strDay = dtmDate.getDate();

    strReturn = ZeroPad(strYear, 4) + "/" + ZeroPad(strMonth, 2) + "/" + ZeroPad(strDay, 2);

    return strReturn;
}
*/
/*
moved to UtilityFunctions.js ConvertDateToCMITime
function ConvertToCMITime(dtmDate){

    WriteToDebug("In ConvertToCMITime");

    var strHours;
    var strMinutes;
    var strSeconds;
    var strReturn;

    dtmDate = new Date(dtmDate);

    strHours = dtmDate.getHours();
    strMinutes = dtmDate.getMinutes();
    strSeconds = dtmDate.getSeconds();

    strReturn = ZeroPad(strHours, 2) + ":" + ZeroPad(strMinutes) + ":" + ZeroPad(strSeconds, 2);

    return strReturn;
}
*/

//==================================================================
//debug stuff
//==================================================================

//used for debug only
function DisplayAICCVariables(){

    var strAlert = "";

    strAlert += "AICC_Student_ID = " + AICC_Student_ID + "\n";
    strAlert += "AICC_Student_Name = " + AICC_Student_Name + "\n";
    strAlert += "AICC_Lesson_Location = " + AICC_Lesson_Location + "\n";
    strAlert += "AICC_Score = " + AICC_Score + "\n";
    strAlert += "AICC_Credit = " + AICC_Credit + "\n";
    strAlert += "AICC_Lesson_Status = " + AICC_Lesson_Status + "\n";
    strAlert += "AICC_Time = " + AICC_Time + "\n";
    strAlert += "AICC_Mastery_Score = " + AICC_Mastery_Score + "\n";
    strAlert += "AICC_Lesson_Mode = " + AICC_Lesson_Mode + "\n";
    strAlert += "AICC_Max_Time_Allowed = " + AICC_Max_Time_Allowed + "\n";
    strAlert += "AICC_Time_Limit_Action = " + AICC_Time_Limit_Action + "\n";
    strAlert += "AICC_Audio = " + AICC_Audio + "\n";
    strAlert += "AICC_Speed = " + AICC_Speed + "\n";
    strAlert += "AICC_Language = " + AICC_Language + "\n";
    strAlert += "AICC_Text = " + AICC_Text + "\n";
    strAlert += "AICC_Launch_Data = " + AICC_Launch_Data + "\n";
    strAlert += "AICC_Data_Chunk = " + AICC_Data_Chunk + "\n";
    strAlert += "AICC_Comments = " + AICC_Comments + "\n";
    strAlert += "AICC_Objectives = " + AICC_Objectives + "\n";

    alert(strAlert)
}




function NONE_Initialize(){
    WriteToDebug("In NONE_Initialize, Returning true");

    InitializeExecuted(true, "");

    return true;
}


function NONE_Finish(strExitType, blnStatusWasSet){
    WriteToDebug("In NONE_Finish, Returning true");
    return true;
}


function NONE_CommitData(){
    WriteToDebug("In NONE_CommitData, Returning true");
    return true;
}

function NONE_GetStudentID(){
    WriteToDebug("In NONE_GetStudentID, Returning ''");
    return "";
}

function NONE_GetStudentName(){
    WriteToDebug("In NONE_GetStudentName, Returning ''");
    return "";
}

function NONE_GetBookmark(){
    WriteToDebug("In NONE_GetBookmark, Returning ''");
    return "";
}

function NONE_SetBookmark(strBookmark){
    WriteToDebug("In NONE_SetBookmark, Returning true");
    return true;
}

function NONE_GetDataChunk(){
    WriteToDebug("In NONE_GetDataChunk, Returning ''");
    return "";
}

function NONE_SetDataChunk(strData){
    WriteToDebug("In NONE_SetDataChunk, Returning true");
    return true;
}


function NONE_GetLaunchData(){
    WriteToDebug("In NONE_GetLaunchData, Returning ''");
    return "";
}

function NONE_GetComments(){
    WriteToDebug("In NONE_GetComments, Returning ''");
    return "";
}

function NONE_WriteComment(strComment){
    WriteToDebug("In NONE_WriteComment, Returning true");
    return true;
}

function NONE_GetLMSComments(){
    WriteToDebug("In NONE_GetLMSComments, Returning ''");
    return "";
}


function NONE_GetAudioPlayPreference(){
    WriteToDebug("In NONE_GetAudioPlayPreference, Returning " + PREFERENCE_DEFAULT);
    return PREFERENCE_DEFAULT;
}



function NONE_GetAudioVolumePreference(){
    WriteToDebug("In NONE_GetAudioVolumePreference, Returning 100");
    return 100;
}


function NONE_SetAudioPreference(PlayPreference, intPercentOfMaxSpeed){
    WriteToDebug("In NONE_SetAudioPreference, Returning true");
    return true;
}



function NONE_SetLanguagePreference(strLanguage){
    WriteToDebug("In NONE_SetLanguagePreference, Returning true");
    return true;
}


function NONE_GetLanguagePreference(){
    WriteToDebug("In NONE_GetLanguagePreference, Returning ''");
    return "";
}


function NONE_SetSpeedPreference(intPercentOfMax){
    WriteToDebug("In NONE_SetSpeedPreference, Returning true");
    return true;
}


function NONE_GetSpeedPreference(){
    WriteToDebug("In NONE_GetSpeedPreference, Returning 100");
    return 100;
}

function NONE_SetTextPreference(intPreference){
    WriteToDebug("In NONE_SetTextPreference, Returning true");
    return true;
}


function NONE_GetTextPreference(){
    WriteToDebug("In NONE_GetTextPreference, Returning " + PREFERENCE_DEFAULT);
    return PREFERENCE_DEFAULT;
}

function NONE_GetPreviouslyAccumulatedTime(){
    WriteToDebug("In NONE_GetPreviouslyAccumulatedTime, Returning 0");
    return 0;
}


function NONE_SaveTime(intMilliSeconds){
    WriteToDebug("In intMilliSeconds, Returning true");
    return true;
}

function NONE_GetMaxTimeAllowed(){
    WriteToDebug("In NONE_GetMaxTimeAllowed, Returning 36002439999");
    return MAX_CMI_TIME;
}



function NONE_DisplayMessageOnTimeout(){
    WriteToDebug("In NONE_DisplayMessageOnTimeout, Returning false");
    return false;
}

function NONE_ExitOnTimeout(){
    WriteToDebug("In NONE_ExitOnTimeout, Returning false");
    return false;
}


function NONE_GetPassingScore(){
    WriteToDebug("In NONE_GetPassingScore, Returning ''");
    return '';
}



function NONE_GetScore(){
    WriteToDebug("In NONE_GetScore, Returning 0");
    return 0;
}

function NONE_SetScore(intScore, intMaxScore, intMinScore){
    WriteToDebug("In NONE_SetScore, Returning true");
    return true;
}



function NONE_RecordTrueFalseInteraction(){
    WriteToDebug("In NONE_RecordTrueFalseInteraction, Returning true");
    return true;
}
function NONE_RecordMultipleChoiceInteraction(strID, strResponse, blnCorrect, strCorrectResponse){
    WriteToDebug("In NONE_RecordMultipleChoiceInteraction, Returning true");
    return true;
}
function NONE_RecordFillInInteraction(){
    WriteToDebug("In NONE_RecordFillInInteraction, Returning true");
    return true;
}
function NONE_RecordMatchingInteraction(){
    WriteToDebug("In NONE_RecordMatchingInteraction, Returning true");
    return true;
}
function NONE_RecordPerformanceInteraction(){
    WriteToDebug("In NONE_RecordPerformanceInteraction, Returning true");
    return true;
}
function NONE_RecordSequencingInteraction(){
    WriteToDebug("In NONE_RecordSequencingInteraction, Returning true");
    return true;
}
function NONE_RecordLikertInteraction(){
    WriteToDebug("In RecordLikertInteraction, Returning true");
    return true;
}
function NONE_RecordNumericInteraction(){
    WriteToDebug("In NONE_RecordNumericInteraction, Returning true");
    return true;
}


function NONE_GetEntryMode(){
    WriteToDebug("In NONE_GetEntryMode, Returning " + ENTRY_FIRST_TIME);
    return ENTRY_FIRST_TIME;
}

function NONE_GetLessonMode(){
    WriteToDebug("In NONE_GetLessonMode, Returning " + MODE_NORMAL);
    return MODE_NORMAL;
}

function NONE_GetTakingForCredit(){
    WriteToDebug("In NONE_GetTakingForCredit, Returning true");
    return true;
}



function NONE_SetObjectiveScore(strObjectiveID, intScore, intMaxScore, intMinScore){
    WriteToDebug("In NONE_SetObjectiveScore, Returning true");
    return true;
}


function NONE_SetObjectiveStatus(strObjectiveID, Lesson_Status){
    WriteToDebug("In NONE_SetObjectiveStatus, Returning true");
    return true;
}

function NONE_SetObjectiveDescription(strObjectiveID, strObjectiveDescription){
    WriteToDebug("In NONE_SetObjectiveDescription, Returning true");
    return true;
}


function NONE_GetObjectiveScore(strObjectiveID){
    WriteToDebug("In NONE_SetObjectiveScore, Returning ''");
    return '';
}


function NONE_GetObjectiveStatus(strObjectiveID){
    WriteToDebug("In NONE_SetObjectiveStatus, Returning Not Attempted");
    return LESSON_STATUS_NOT_ATTEMPTED;
}

function NONE_GetObjectiveDescription(strObjectiveID){
    WriteToDebug("In NONE_GetObjectiveDescription, ''");
    return "";
}

function NONE_CreateValidIdentifier(str) {
    return CreateUriIdentifier(str);
}

//___________________________________________________________
//Interaction Retrieval Functionality
//NOTE ON INTERACTION RETRIEVAL
//A.  It is only available in certain standards, standards where it is unavailable will return nothing
//B.  The interaction records are currently reported using "journaling", whereby each entry is appended
//      Retrieval methods will retrieve only the most recent value


//___________________________________________________________
//Helper Methods
function NONE_FindInteractionIndexFromID(strInteractionID){

    WriteToDebug("NONE_FindInteractionIndexFromID - NONE does not support interaction retrieval, returning null");

    return null;
}
//___________________________________________________________


function NONE_GetInteractionType(strInteractionID)
{

    WriteToDebug("NONE_GetInteractionType - NONE does not support interaction retrieval, returning empty string");
    return '';


}

//public
function NONE_GetInteractionTimestamp(strInteractionID)
{
    WriteToDebug("NONE_GetInteractionTimestamp - NONE does not support interaction retrieval, returning empty string");
    return '';
}



//public
function NONE_GetInteractionCorrectResponses(strInteractionID)
{

    WriteToDebug("NONE_GetInteractionCorrectResponses - NONE does not support interaction retrieval, returning empty array");
    return new Array();


}



//public
function NONE_GetInteractionWeighting(strInteractionID)
{
    WriteToDebug("NONE_GetInteractionWeighting - NONE does not support interaction retrieval, returning empty string");
    return '';

}



//public
function NONE_GetInteractionLearnerResponses(strInteractionID)
{
    WriteToDebug("NONE_GetInteractionLearnerResponses - NONE does not support interaction retrieval, returning empty array");
    return new Array();


}


//public
function NONE_GetInteractionResult(strInteractionID)
{
    WriteToDebug("NONE_GetInteractionResult - NONE does not support interaction retrieval, returning empty string");
    return '';

}



//public
function NONE_GetInteractionLatency(strInteractionID)
{
    WriteToDebug("NONE_GetInteractionDescription - NONE does not support interaction retrieval, returning empty string");
    return '';

}



//public
function NONE_GetInteractionDescription(strInteractionID)
{
    WriteToDebug("NONE_GetInteractionDescription - NONE does not support interaction retrieval, returning empty string");
    return '';


}

//public
function NONE_CreateDataBucket(strBucketId, intMinSize, intMaxSize){
    WriteToDebug("NONE_CreateDataBucket - NONE does not support SSP, returning false");
    return false;
}

//public
function NONE_GetDataFromBucket(strBucketId){
    WriteToDebug("NONE_GetDataFromBucket - NONE does not support SSP, returning empty string");
    return "";
}

//public
function NONE_PutDataInBucket(strBucketId, strData, blnAppendToEnd){
    WriteToDebug("NONE_PutDataInBucket - NONE does not support SSP, returning false");
    return false;
}

//public
function NONE_DetectSSPSupport(){
    WriteToDebug("NONE_DetectSSPSupport - NONE does not support SSP, returning false");
    return false;
}

//public
function NONE_GetBucketInfo(strBucketId){
    WriteToDebug("NONE_DetectSSPSupport - NONE does not support SSP, returning empty SSPBucketSize");
    return new SSPBucketSize(0, 0);
}


//________________________________________________

function NONE_SetNavigationRequest(strNavRequest){
    WriteToDebug("NONE_SetNavigationRequest - NONE does not support navigation requests, returning false");
    return false;
}

function NONE_GetNavigationRequest(){
    WriteToDebug("NONE_GetNavigationRequest - NONE does not support navigation requests, returning false");
    return false;
}


function NONE_SetFailed(){
    WriteToDebug("In NONE_SetFailed, Returning true");
    return true;
}

function NONE_SetPassed(){
    WriteToDebug("In NONE_SetPassed, Returning true");
    return true;
}

function NONE_SetCompleted(){
    WriteToDebug("In NONE_SetCompleted, Returning true");
    return true;
}

function NONE_ResetStatus(){
    WriteToDebug("In NONE_ResetStatus, Returning true");
    return true;
}

function NONE_GetStatus(){
    WriteToDebug("In NONE_GetStatus, Returning " + LESSON_STATUS_INCOMPLETE);
    return LESSON_STATUS_INCOMPLETE;
}

//public
function NONE_GetProgressMeasure(){
    WriteToDebug("NONE_GetProgressMeasure - NONE does not support progress_measure, returning false");
    return false;
}
//public
function NONE_SetProgressMeasure(){
    WriteToDebug("NONE_SetProgressMeasure - NONE does not support progress_measure, returning false");
    return false;
}

//public
function NONE_GetObjectiveProgressMeasure(){
    WriteToDebug("NONE_GetObjectiveProgressMeasure - NONE does not support progress_measure, returning false");
    return false;
}
//public
function NONE_SetObjectiveProgressMeasure(){
    WriteToDebug("NONE_SetObjectiveProgressMeasure - NONE does not support progress_measure, returning false");
    return false;
}

function NONE_SetPointBasedScore(intScore, intMaxScore, intMinScore){
    WriteToDebug("NONE_SetPointBasedScore - NONE does not support SetPointBasedScore, returning false");
    return false;
}

function NONE_GetScaledScore(intScore, intMaxScore, intMinScore){
    WriteToDebug("NONE_GetScaledScore - NONE does not support GetScaledScore, returning false");
    return false;
}

function NONE_GetLastError(){
    WriteToDebug("In NONE_GetLastError, Returning " + NO_ERROR);
    return NO_ERROR;
}

function NONE_GetLastErrorDesc(){
    WriteToDebug("In NONE_GetLastErrorDesc, Returning ''");
    return "";
}

"0.50.0";
/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
var CryptoJS=CryptoJS||function(e,m){var p={},j=p.lib={},l=function(){},f=j.Base={extend:function(a){l.prototype=this;var c=new l;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
n=j.WordArray=f.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=m?c:4*a.length},toString:function(a){return(a||h).stringify(this)},concat:function(a){var c=this.words,q=a.words,d=this.sigBytes;a=a.sigBytes;this.clamp();if(d%4)for(var b=0;b<a;b++)c[d+b>>>2]|=(q[b>>>2]>>>24-8*(b%4)&255)<<24-8*((d+b)%4);else if(65535<q.length)for(b=0;b<a;b+=4)c[d+b>>>2]=q[b>>>2];else c.push.apply(c,q);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<
32-8*(c%4);a.length=e.ceil(c/4)},clone:function(){var a=f.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],b=0;b<a;b+=4)c.push(4294967296*e.random()|0);return new n.init(c,a)}}),b=p.enc={},h=b.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var b=[],d=0;d<a;d++){var f=c[d>>>2]>>>24-8*(d%4)&255;b.push((f>>>4).toString(16));b.push((f&15).toString(16))}return b.join("")},parse:function(a){for(var c=a.length,b=[],d=0;d<c;d+=2)b[d>>>3]|=parseInt(a.substr(d,
2),16)<<24-4*(d%8);return new n.init(b,c/2)}},g=b.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var b=[],d=0;d<a;d++)b.push(String.fromCharCode(c[d>>>2]>>>24-8*(d%4)&255));return b.join("")},parse:function(a){for(var c=a.length,b=[],d=0;d<c;d++)b[d>>>2]|=(a.charCodeAt(d)&255)<<24-8*(d%4);return new n.init(b,c)}},r=b.Utf8={stringify:function(a){try{return decodeURIComponent(escape(g.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return g.parse(unescape(encodeURIComponent(a)))}},
k=j.BufferedBlockAlgorithm=f.extend({reset:function(){this._data=new n.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=r.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,b=c.words,d=c.sigBytes,f=this.blockSize,h=d/(4*f),h=a?e.ceil(h):e.max((h|0)-this._minBufferSize,0);a=h*f;d=e.min(4*a,d);if(a){for(var g=0;g<a;g+=f)this._doProcessBlock(b,g);g=b.splice(0,a);c.sigBytes-=d}return new n.init(g,d)},clone:function(){var a=f.clone.call(this);
a._data=this._data.clone();return a},_minBufferSize:0});j.Hasher=k.extend({cfg:f.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){k.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(c,b){return(new a.init(b)).finalize(c)}},_createHmacHelper:function(a){return function(b,f){return(new s.HMAC.init(a,
f)).finalize(b)}}});var s=p.algo={};return p}(Math);
(function(){var e=CryptoJS,m=e.lib,p=m.WordArray,j=m.Hasher,l=[],m=e.algo.SHA1=j.extend({_doReset:function(){this._hash=new p.init([1732584193,4023233417,2562383102,271733878,3285377520])},_doProcessBlock:function(f,n){for(var b=this._hash.words,h=b[0],g=b[1],e=b[2],k=b[3],j=b[4],a=0;80>a;a++){if(16>a)l[a]=f[n+a]|0;else{var c=l[a-3]^l[a-8]^l[a-14]^l[a-16];l[a]=c<<1|c>>>31}c=(h<<5|h>>>27)+j+l[a];c=20>a?c+((g&e|~g&k)+1518500249):40>a?c+((g^e^k)+1859775393):60>a?c+((g&e|g&k|e&k)-1894007588):c+((g^e^
k)-899497514);j=k;k=e;e=g<<30|g>>>2;g=h;h=c}b[0]=b[0]+h|0;b[1]=b[1]+g|0;b[2]=b[2]+e|0;b[3]=b[3]+k|0;b[4]=b[4]+j|0},_doFinalize:function(){var f=this._data,e=f.words,b=8*this._nDataBytes,h=8*f.sigBytes;e[h>>>5]|=128<<24-h%32;e[(h+64>>>9<<4)+14]=Math.floor(b/4294967296);e[(h+64>>>9<<4)+15]=b;f.sigBytes=4*e.length;this._process();return this._hash},clone:function(){var e=j.clone.call(this);e._hash=this._hash.clone();return e}});e.SHA1=j._createHelper(m);e.HmacSHA1=j._createHmacHelper(m)})();

/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
var CryptoJS=CryptoJS||function(h,s){var f={},t=f.lib={},g=function(){},j=t.Base={extend:function(a){g.prototype=this;var c=new g;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
q=t.WordArray=j.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=s?c:4*a.length},toString:function(a){return(a||u).stringify(this)},concat:function(a){var c=this.words,d=a.words,b=this.sigBytes;a=a.sigBytes;this.clamp();if(b%4)for(var e=0;e<a;e++)c[b+e>>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((b+e)%4);else if(65535<d.length)for(e=0;e<a;e+=4)c[b+e>>>2]=d[e>>>2];else c.push.apply(c,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<
32-8*(c%4);a.length=h.ceil(c/4)},clone:function(){var a=j.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],d=0;d<a;d+=4)c.push(4294967296*h.random()|0);return new q.init(c,a)}}),v=f.enc={},u=v.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++){var e=c[b>>>2]>>>24-8*(b%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b+=2)d[b>>>3]|=parseInt(a.substr(b,
2),16)<<24-4*(b%8);return new q.init(d,c/2)}},k=v.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++)d.push(String.fromCharCode(c[b>>>2]>>>24-8*(b%4)&255));return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b++)d[b>>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new q.init(d,c)}},l=v.Utf8={stringify:function(a){try{return decodeURIComponent(escape(k.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return k.parse(unescape(encodeURIComponent(a)))}},
x=t.BufferedBlockAlgorithm=j.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=l.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,d=c.words,b=c.sigBytes,e=this.blockSize,f=b/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;b=h.min(4*a,b);if(a){for(var m=0;m<a;m+=e)this._doProcessBlock(d,m);m=d.splice(0,a);c.sigBytes-=b}return new q.init(m,b)},clone:function(){var a=j.clone.call(this);
a._data=this._data.clone();return a},_minBufferSize:0});t.Hasher=x.extend({cfg:j.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){x.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(c,d){return(new a.init(d)).finalize(c)}},_createHmacHelper:function(a){return function(c,d){return(new w.HMAC.init(a,
d)).finalize(c)}}});var w=f.algo={};return f}(Math);
(function(h){for(var s=CryptoJS,f=s.lib,t=f.WordArray,g=f.Hasher,f=s.algo,j=[],q=[],v=function(a){return 4294967296*(a-(a|0))|0},u=2,k=0;64>k;){var l;a:{l=u;for(var x=h.sqrt(l),w=2;w<=x;w++)if(!(l%w)){l=!1;break a}l=!0}l&&(8>k&&(j[k]=v(h.pow(u,0.5))),q[k]=v(h.pow(u,1/3)),k++);u++}var a=[],f=f.SHA256=g.extend({_doReset:function(){this._hash=new t.init(j.slice(0))},_doProcessBlock:function(c,d){for(var b=this._hash.words,e=b[0],f=b[1],m=b[2],h=b[3],p=b[4],j=b[5],k=b[6],l=b[7],n=0;64>n;n++){if(16>n)a[n]=
c[d+n]|0;else{var r=a[n-15],g=a[n-2];a[n]=((r<<25|r>>>7)^(r<<14|r>>>18)^r>>>3)+a[n-7]+((g<<15|g>>>17)^(g<<13|g>>>19)^g>>>10)+a[n-16]}r=l+((p<<26|p>>>6)^(p<<21|p>>>11)^(p<<7|p>>>25))+(p&j^~p&k)+q[n]+a[n];g=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&f^e&m^f&m);l=k;k=j;j=p;p=h+r|0;h=m;m=f;f=e;e=r+g|0}b[0]=b[0]+e|0;b[1]=b[1]+f|0;b[2]=b[2]+m|0;b[3]=b[3]+h|0;b[4]=b[4]+p|0;b[5]=b[5]+j|0;b[6]=b[6]+k|0;b[7]=b[7]+l|0},_doFinalize:function(){var a=this._data,d=a.words,b=8*this._nDataBytes,e=8*a.sigBytes;
d[e>>>5]|=128<<24-e%32;d[(e+64>>>9<<4)+14]=h.floor(b/4294967296);d[(e+64>>>9<<4)+15]=b;a.sigBytes=4*d.length;this._process();return this._hash},clone:function(){var a=g.clone.call(this);a._hash=this._hash.clone();return a}});s.SHA256=g._createHelper(f);s.HmacSHA256=g._createHmacHelper(f)})(Math);

/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
(function () {
    // Shortcuts
    var C = CryptoJS;
    var C_lib = C.lib;
    var WordArray = C_lib.WordArray;
    var C_enc = C.enc;

    /**
     * Base64 encoding strategy.
     */
    var Base64 = C_enc.Base64 = {
        /**
         * Converts a word array to a Base64 string.
         *
         * @param {WordArray} wordArray The word array.
         *
         * @return {string} The Base64 string.
         *
         * @static
         *
         * @example
         *
         *     var base64String = CryptoJS.enc.Base64.stringify(wordArray);
         */
        stringify: function (wordArray) {
            // Shortcuts
            var words = wordArray.words;
            var sigBytes = wordArray.sigBytes;
            var map = this._map;

            // Clamp excess bits
            wordArray.clamp();

            // Convert
            var base64Chars = [];
            for (var i = 0; i < sigBytes; i += 3) {
                var byte1 = (words[i >>> 2]       >>> (24 - (i % 4) * 8))       & 0xff;
                var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff;
                var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff;

                var triplet = (byte1 << 16) | (byte2 << 8) | byte3;

                for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) {
                    base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f));
                }
            }

            // Add padding
            var paddingChar = map.charAt(64);
            if (paddingChar) {
                while (base64Chars.length % 4) {
                    base64Chars.push(paddingChar);
                }
            }

            return base64Chars.join('');
        },

        /**
         * Converts a Base64 string to a word array.
         *
         * @param {string} base64Str The Base64 string.
         *
         * @return {WordArray} The word array.
         *
         * @static
         *
         * @example
         *
         *     var wordArray = CryptoJS.enc.Base64.parse(base64String);
         */
        parse: function (base64Str) {
            // Shortcuts
            var base64StrLength = base64Str.length;
            var map = this._map;

            // Ignore padding
            var paddingChar = map.charAt(64);
            if (paddingChar) {
                var paddingIndex = base64Str.indexOf(paddingChar);
                if (paddingIndex != -1) {
                    base64StrLength = paddingIndex;
                }
            }

            // Convert
            var words = [];
            var nBytes = 0;
            for (var i = 0; i < base64StrLength; i++) {
                if (i % 4) {
                    var bits1 = map.indexOf(base64Str.charAt(i - 1)) << ((i % 4) * 2);
                    var bits2 = map.indexOf(base64Str.charAt(i)) >>> (6 - (i % 4) * 2);
                    words[nBytes >>> 2] |= (bits1 | bits2) << (24 - (nBytes % 4) * 8);
                    nBytes++;
                }
            }

            return WordArray.create(words, nBytes);
        },

        _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
    };
}());

/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
(function () {
    // Check if typed arrays are supported
    if (typeof ArrayBuffer != 'function') {
        return;
    }

    // Shortcuts
    var C = CryptoJS;
    var C_lib = C.lib;
    var WordArray = C_lib.WordArray;

    // Reference original init
    var superInit = WordArray.init;

    // Augment WordArray.init to handle typed arrays
    var subInit = WordArray.init = function (typedArray) {
        // Convert buffers to uint8
        if (typedArray instanceof ArrayBuffer) {
            typedArray = new Uint8Array(typedArray);
        }

        // Convert other array views to uint8
        if (
            typedArray instanceof Int8Array ||
            typedArray instanceof Uint8ClampedArray ||
            typedArray instanceof Int16Array ||
            typedArray instanceof Uint16Array ||
            typedArray instanceof Int32Array ||
            typedArray instanceof Uint32Array ||
            typedArray instanceof Float32Array ||
            typedArray instanceof Float64Array
        ) {
            typedArray = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength);
        }

        // Handle Uint8Array
        if (typedArray instanceof Uint8Array) {
            // Shortcut
            var typedArrayByteLength = typedArray.byteLength;

            // Extract bytes
            var words = [];
            for (var i = 0; i < typedArrayByteLength; i++) {
                words[i >>> 2] |= typedArray[i] << (24 - (i % 4) * 8);
            }

            // Initialize this word array
            superInit.call(this, words, typedArrayByteLength);
        } else {
            // Else call normal init
            superInit.apply(this, arguments);
        }
    };

    subInit.prototype = WordArray;
}());

/*!
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

TODO:

* Add statement queueing

@module TinCan
**/
var TinCan;

(function () {
    "use strict";
    var _reservedQSParams = {
        //
        // these are TC spec reserved words that may end up in queries to the endpoint
        //
        statementId:       true,
        voidedStatementId: true,
        verb:              true,
        object:            true,
        registration:      true,
        context:           true,
        actor:             true,
        since:             true,
        until:             true,
        limit:             true,
        authoritative:     true,
        sparse:            true,
        instructor:        true,
        ascending:         true,
        continueToken:     true,
        agent:             true,
        activityId:        true,
        stateId:           true,
        profileId:         true,

        //
        // these are suggested by the LMS launch spec addition that TinCanJS consumes
        //
        activity_platform: true,
        grouping:          true,
        "Accept-Language": true
    };

    /**
    @class TinCan
    @constructor
    @param {Object} [options] Configuration used to initialize.
        @param {String} [options.url] URL for determining launch provided
            configuration options
        @param {Array} [options.recordStores] list of pre-configured LRSes
        @param {Object|TinCan.Agent} [options.actor] default actor
        @param {Object|TinCan.Activity} [options.activity] default activity
        @param {String} [options.registration] default registration
        @param {Object|TinCan.Context} [options.context] default context
    **/
    TinCan = function (cfg) {
        this.log("constructor");

        /**
        @property recordStores
        @type Array
        */
        this.recordStores = [];

        /**
        Default actor used when preparing statements that
        don't yet have an actor set, and for saving state, etc.

        @property actor
        @type Object
        */
        this.actor = null;

        /**
        Default activity, may be used as a statement 'target'
        or incorporated into 'context'

        @property activity
        @type Object
        */
        this.activity = null;

        /**
        Default registration, included in default context when
        provided, otherwise used in statement queries

        @property registration
        @type String
        */
        this.registration = null;

        /**
        Default context used when preparing statements that
        don't yet have a context set, or mixed in when one
        has been provided, properties do NOT override on mixing

        @property context
        @type Object
        */
        this.context = null;

        this.init(cfg);
    };

    TinCan.prototype = {
        LOG_SRC: "TinCan",

        /**
        Safe version of logging, only displays when .DEBUG is true, and console.log
        is available

        @method log
        @param {String} msg Message to output
        */
        log: function (msg, src) {
            /* globals console */
            if (TinCan.DEBUG && typeof console !== "undefined" && console.log) {
                src = src || this.LOG_SRC || "TinCan";

                console.log("TinCan." + src + ": " + msg);
            }
        },

        /**
        @method init
        @param {Object} [options] Configuration used to initialize (see TinCan constructor).
        */
        init: function (cfg) {
            this.log("init");
            var i;

            cfg = cfg || {};

            if (cfg.hasOwnProperty("url") && cfg.url !== "") {
                this._initFromQueryString(cfg.url);
            }

            if (cfg.hasOwnProperty("recordStores") && cfg.recordStores !== undefined) {
                for (i = 0; i < cfg.recordStores.length; i += 1) {
                    this.addRecordStore(cfg.recordStores[i]);
                }
            }
            if (cfg.hasOwnProperty("activity")) {
                if (cfg.activity instanceof TinCan.Activity) {
                    this.activity = cfg.activity;
                }
                else {
                    this.activity = new TinCan.Activity (cfg.activity);
                }
            }
            if (cfg.hasOwnProperty("actor")) {
                if (cfg.actor instanceof TinCan.Agent) {
                    this.actor = cfg.actor;
                }
                else {
                    this.actor = new TinCan.Agent (cfg.actor);
                }
            }
            if (cfg.hasOwnProperty("context")) {
                if (cfg.context instanceof TinCan.Context) {
                    this.context = cfg.context;
                }
                else {
                    this.context = new TinCan.Context (cfg.context);
                }
            }
            if (cfg.hasOwnProperty("registration")) {
                this.registration = cfg.registration;
            }
        },

        /**
        @method _initFromQueryString
        @param {String} url
        @private
        */
        _initFromQueryString: function (url) {
            this.log("_initFromQueryString");

            var i,
                prop,
                qsParams = TinCan.Utils.parseURL(url).params,
                lrsProps = ["endpoint", "auth"],
                lrsCfg = {},
                contextCfg,
                extended = null
            ;

            if (qsParams.hasOwnProperty("actor")) {
                this.log("_initFromQueryString - found actor: " + qsParams.actor);
                try {
                    this.actor = TinCan.Agent.fromJSON(qsParams.actor);
                    delete qsParams.actor;
                }
                catch (ex) {
                    this.log("_initFromQueryString - failed to set actor: " + ex);
                }
            }

            if (qsParams.hasOwnProperty("activity_id")) {
                this.activity = new TinCan.Activity (
                    {
                        id: qsParams.activity_id
                    }
                );
                delete qsParams.activity_id;
            }

            if (
                qsParams.hasOwnProperty("activity_platform") ||
                qsParams.hasOwnProperty("registration") ||
                qsParams.hasOwnProperty("grouping")
            ) {
                contextCfg = {};

                if (qsParams.hasOwnProperty("activity_platform")) {
                    contextCfg.platform = qsParams.activity_platform;
                    delete qsParams.activity_platform;
                }
                if (qsParams.hasOwnProperty("registration")) {
                    //
                    // stored in two locations cause we always want it in the default
                    // context, but we also want to be able to get to it for Statement
                    // queries
                    //
                    contextCfg.registration = this.registration = qsParams.registration;
                    delete qsParams.registration;
                }
                if (qsParams.hasOwnProperty("grouping")) {
                    contextCfg.contextActivities = {};
                    contextCfg.contextActivities.grouping = qsParams.grouping;
                    delete qsParams.grouping;
                }

                this.context = new TinCan.Context (contextCfg);
            }

            //
            // order matters here, process the URL provided LRS last because it gets
            // all the remaining parameters so that they get passed through
            //
            if (qsParams.hasOwnProperty("endpoint")) {
                for (i = 0; i < lrsProps.length; i += 1) {
                    prop = lrsProps[i];
                    if (qsParams.hasOwnProperty(prop)) {
                        lrsCfg[prop] = qsParams[prop];
                        delete qsParams[prop];
                    }
                }

                // remove our reserved params so they don't end up  in the extended object
                for (i in qsParams) {
                    if (qsParams.hasOwnProperty(i)) {
                        if (_reservedQSParams.hasOwnProperty(i)) {
                            delete qsParams[i];
                        } else {
                            extended = extended || {};
                            extended[i] = qsParams[i];
                        }
                    }
                }
                if (extended !== null) {
                    lrsCfg.extended = extended;
                }

                lrsCfg.allowFail = false;

                this.addRecordStore(lrsCfg);
            }
        },

        /**
        @method addRecordStore
        @param {Object} Configuration data

         * TODO:
         * check endpoint for trailing '/'
         * check for unique endpoints
        */
        addRecordStore: function (cfg) {
            this.log("addRecordStore");
            var lrs;
            if (cfg instanceof TinCan.LRS) {
                lrs = cfg;
            }
            else {
                lrs = new TinCan.LRS (cfg);
            }
            this.recordStores.push(lrs);
        },

        /**
        @method prepareStatement
        @param {Object|TinCan.Statement} Base statement properties or
            pre-created TinCan.Statement instance
        @return {TinCan.Statement}
        */
        prepareStatement: function (stmt) {
            this.log("prepareStatement");
            if (! (stmt instanceof TinCan.Statement)) {
                stmt = new TinCan.Statement (stmt);
            }

            if (stmt.actor === null && this.actor !== null) {
                stmt.actor = this.actor;
            }
            if (stmt.target === null && this.activity !== null) {
                stmt.target = this.activity;
            }

            if (this.context !== null) {
                if (stmt.context === null) {
                    stmt.context = this.context;
                }
                else {
                    if (stmt.context.registration === null) {
                        stmt.context.registration = this.context.registration;
                    }
                    if (stmt.context.platform === null) {
                        stmt.context.platform = this.context.platform;
                    }

                    if (this.context.contextActivities !== null) {
                        if (stmt.context.contextActivities === null) {
                            stmt.context.contextActivities = this.context.contextActivities;
                        }
                        else {
                            if (this.context.contextActivities.grouping !== null && stmt.context.contextActivities.grouping === null) {
                                stmt.context.contextActivities.grouping = this.context.contextActivities.grouping;
                            }
                            if (this.context.contextActivities.parent !== null && stmt.context.contextActivities.parent === null) {
                                stmt.context.contextActivities.parent = this.context.contextActivities.parent;
                            }
                            if (this.context.contextActivities.other !== null && stmt.context.contextActivities.other === null) {
                                stmt.context.contextActivities.other = this.context.contextActivities.other;
                            }
                        }
                    }
                }
            }

            return stmt;
        },

        /**
        Calls saveStatement on each configured LRS, provide callback to make it asynchronous

        @method sendStatement
        @param {TinCan.Statement|Object} statement Send statement to LRS
        @param {Function} [callback] Callback function to execute on completion
        */
        sendStatement: function (stmt, callback) {
            this.log("sendStatement");

            // would prefer to use .bind instead of 'self'
            var self = this,
                lrs,
                statement = this.prepareStatement(stmt),
                rsCount = this.recordStores.length,
                i,
                results = [],
                callbackWrapper,
                callbackResults = []
            ;

            if (rsCount > 0) {
                /*
                   if there is a callback that is a function then we need
                   to wrap that function with a function that becomes
                   the new callback that reduces a closure count of the
                   requests that don't have allowFail set to true and
                   when that number hits zero then the original callback
                   is executed
                */
                if (typeof callback === "function") {
                    callbackWrapper = function (err, xhr) {
                        var args;

                        self.log("sendStatement - callbackWrapper: " + rsCount);
                        if (rsCount > 1) {
                            rsCount -= 1;
                            callbackResults.push(
                                {
                                    err: err,
                                    xhr: xhr
                                }
                            );
                        }
                        else if (rsCount === 1) {
                            callbackResults.push(
                                {
                                    err: err,
                                    xhr: xhr
                                }
                            );
                            args = [
                                callbackResults,
                                statement
                            ];
                            callback.apply(this, args);
                        }
                        else {
                            self.log("sendStatement - unexpected record store count: " + rsCount);
                        }
                    };
                }

                for (i = 0; i < rsCount; i += 1) {
                    lrs = this.recordStores[i];

                    results.push(
                        lrs.saveStatement(statement, { callback: callbackWrapper })
                    );
                }
            }
            else {
                this.log("[warning] sendStatement: No LRSs added yet (statement not sent)");
                if (typeof callback === "function") {
                    callback.apply(this, [ null, statement ]);
                }
            }

            return {
                statement: statement,
                results: results
            };
        },

        /**
        Calls retrieveStatement on the first LRS, provide callback to make it asynchronous

        @method getStatement
        @param {String} [stmtId] Statement ID to get
        @param {Function} [callback] Callback function to execute on completion
        @param {Object} [cfg] Configuration data
            @param {Object} [params] Query parameters
                @param {Boolean} [attachments] Include attachments in multipart response or don't (defualt: false)
        @return {Array|Result} Array of results, or single result

        TODO: make TinCan track statements it has seen in a local cache to be returned easily
        */
        getStatement: function (stmtId, callback, cfg) {
            this.log("getStatement");

            var lrs;

            cfg = cfg || {};
            cfg.params = cfg.params || {};

            if (this.recordStores.length > 0) {
                //
                // for statements (for now) we only need to read from the first LRS
                // in the future it may make sense to get all from all LRSes and
                // compare to remove duplicates or allow inspection of them for differences?
                //
                // TODO: make this the first non-allowFail LRS but for now it should
                // be good enough to make it the first since we know the LMS provided
                // LRS is the first
                //
                lrs = this.recordStores[0];

                return lrs.retrieveStatement(stmtId, { callback: callback, params: cfg.params });
            }

            this.log("[warning] getStatement: No LRSs added yet (statement not retrieved)");
        },

        /**
        Creates a statement used for voiding the passed statement/statement ID and calls
        send statement with the voiding statement.

        @method voidStatement
        @param {TinCan.Statement|String} statement Statement or statement ID to void
        @param {Function} [callback] Callback function to execute on completion
        @param {Object} [options] Options used to build voiding statement
            @param {TinCan.Agent} [options.actor] Agent to be used as 'actor' in voiding statement
        */
        voidStatement: function (stmt, callback, options) {
            this.log("voidStatement");

            // would prefer to use .bind instead of 'self'
            var self = this,
                lrs,
                actor,
                voidingStatement,
                rsCount = this.recordStores.length,
                i,
                results = [],
                callbackWrapper,
                callbackResults = []
            ;

            if (stmt instanceof TinCan.Statement) {
                stmt = stmt.id;
            }

            if (typeof options.actor !== "undefined") {
                actor = options.actor;
            }
            else if (this.actor !== null) {
                actor = this.actor;
            }

            voidingStatement = new TinCan.Statement(
                {
                    actor: actor,
                    verb: {
                        id: "http://adlnet.gov/expapi/verbs/voided"
                    },
                    target: {
                        objectType: "StatementRef",
                        id: stmt
                    }
                }
            );

            if (rsCount > 0) {
                /*
                   if there is a callback that is a function then we need
                   to wrap that function with a function that becomes
                   the new callback that reduces a closure count of the
                   requests that don't have allowFail set to true and
                   when that number hits zero then the original callback
                   is executed
                */
                if (typeof callback === "function") {
                    callbackWrapper = function (err, xhr) {
                        var args;

                        self.log("voidStatement - callbackWrapper: " + rsCount);
                        if (rsCount > 1) {
                            rsCount -= 1;
                            callbackResults.push(
                                {
                                    err: err,
                                    xhr: xhr
                                }
                            );
                        }
                        else if (rsCount === 1) {
                            callbackResults.push(
                                {
                                    err: err,
                                    xhr: xhr
                                }
                            );
                            args = [
                                callbackResults,
                                voidingStatement
                            ];
                            callback.apply(this, args);
                        }
                        else {
                            self.log("voidStatement - unexpected record store count: " + rsCount);
                        }
                    };
                }

                for (i = 0; i < rsCount; i += 1) {
                    lrs = this.recordStores[i];

                    results.push(
                        lrs.saveStatement(voidingStatement, { callback: callbackWrapper })
                    );
                }
            }
            else {
                this.log("[warning] voidStatement: No LRSs added yet (statement not sent)");
                if (typeof callback === "function") {
                    callback.apply(this, [ null, voidingStatement ]);
                }
            }

            return {
                statement: voidingStatement,
                results: results
            };
        },

        /**
        Calls retrieveVoidedStatement on the first LRS, provide callback to make it asynchronous

        @method getVoidedStatement
        @param {String} statement Statement ID to get
        @param {Function} [callback] Callback function to execute on completion
        @return {Array|Result} Array of results, or single result

        TODO: make TinCan track voided statements it has seen in a local cache to be returned easily
        */
        getVoidedStatement: function (stmtId, callback) {
            this.log("getVoidedStatement");

            var lrs;

            if (this.recordStores.length > 0) {
                //
                // for statements (for now) we only need to read from the first LRS
                // in the future it may make sense to get all from all LRSes and
                // compare to remove duplicates or allow inspection of them for differences?
                //
                // TODO: make this the first non-allowFail LRS but for now it should
                // be good enough to make it the first since we know the LMS provided
                // LRS is the first
                //
                lrs = this.recordStores[0];

                return lrs.retrieveVoidedStatement(stmtId, { callback: callback });
            }

            this.log("[warning] getVoidedStatement: No LRSs added yet (statement not retrieved)");
        },

        /**
        Calls saveStatements with list of prepared statements

        @method sendStatements
        @param {Array} Array of statements to send
        @param {Function} Callback function to execute on completion
        */
        sendStatements: function (stmts, callback) {
            this.log("sendStatements");
            var self = this,
                lrs,
                statements = [],
                rsCount = this.recordStores.length,
                i,
                results = [],
                callbackWrapper,
                callbackResults = []
            ;
            if (stmts.length === 0) {
                if (typeof callback === "function") {
                    callback.apply(this, [ null, statements ]);
                }
            }
            else {
                for (i = 0; i < stmts.length; i += 1) {
                    statements.push(
                        this.prepareStatement(stmts[i])
                    );
                }

                if (rsCount > 0) {
                    /*
                       if there is a callback that is a function then we need
                       to wrap that function with a function that becomes
                       the new callback that reduces a closure count of the
                       requests that don't have allowFail set to true and
                       when that number hits zero then the original callback
                       is executed
                    */

                    if (typeof callback === "function") {
                        callbackWrapper = function (err, xhr) {
                            var args;

                            self.log("sendStatements - callbackWrapper: " + rsCount);
                            if (rsCount > 1) {
                                rsCount -= 1;
                                callbackResults.push(
                                    {
                                        err: err,
                                        xhr: xhr
                                    }
                                );
                            }
                            else if (rsCount === 1) {
                                callbackResults.push(
                                    {
                                        err: err,
                                        xhr: xhr
                                    }
                                );
                                args = [
                                    callbackResults,
                                    statements
                                ];
                                callback.apply(this, args);
                            }
                            else {
                                self.log("sendStatements - unexpected record store count: " + rsCount);
                            }
                        };
                    }

                    for (i = 0; i < rsCount; i += 1) {
                        lrs = this.recordStores[i];

                        results.push(
                            lrs.saveStatements(statements, { callback: callbackWrapper })
                        );
                    }
                }
                else {
                    this.log("[warning] sendStatements: No LRSs added yet (statements not sent)");
                    if (typeof callback === "function") {
                        callback.apply(this, [ null, statements ]);
                    }
                }
            }

            return {
                statements: statements,
                results: results
            };
        },

        /**
        @method getStatements
        @param {Object} [cfg] Configuration for request
            @param {Boolean} [cfg.sendActor] Include default actor in query params
            @param {Boolean} [cfg.sendActivity] Include default activity in query params
            @param {Object} [cfg.params] Parameters used to filter.
                            These are the same as those accepted by the
                            <a href="TinCan.LRS.html#method_queryStatements">LRS.queryStatements</a>
                            method.

            @param {Function} [cfg.callback] Function to run at completion

        TODO: support multiple LRSs and flag to use single
        */
        getStatements: function (cfg) {
            this.log("getStatements");
            var queryCfg = {},
                lrs,
                params
            ;
            if (this.recordStores.length > 0) {
                //
                // for get (for now) we only get from one (as they should be the same)
                // but it may make sense to long term try to merge statements, perhaps
                // by using statementId as unique
                //
                // TODO: make this the first non-allowFail LRS but for now it should
                // be good enough to make it the first since we know the LMS provided
                // LRS is the first
                //
                lrs = this.recordStores[0];

                cfg = cfg || {};

                // TODO: need a clone function?
                params = cfg.params || {};

                if (cfg.sendActor && this.actor !== null) {
                    if (lrs.version === "0.9" || lrs.version === "0.95") {
                        params.actor = this.actor;
                    }
                    else {
                        params.agent = this.actor;
                    }
                }
                if (cfg.sendActivity && this.activity !== null) {
                    if (lrs.version === "0.9" || lrs.version === "0.95") {
                        params.target = this.activity;
                    }
                    else {
                        params.activity = this.activity;
                    }
                }
                if (typeof params.registration === "undefined" && this.registration !== null) {
                    params.registration = this.registration;
                }

                queryCfg = {
                    params: params
                };
                if (typeof cfg.callback !== "undefined") {
                    queryCfg.callback = cfg.callback;
                }

                return lrs.queryStatements(queryCfg);
            }

            this.log("[warning] getStatements: No LRSs added yet (statements not read)");
        },

        /**
        @method getState
        @param {String} key Key to retrieve from the state
        @param {Object} [cfg] Configuration for request
            @param {Object} [cfg.agent] Agent used in query,
                defaults to 'actor' property if empty
            @param {Object} [cfg.activity] Activity used in query,
                defaults to 'activity' property if empty
            @param {Object} [cfg.registration] Registration used in query,
                defaults to 'registration' property if empty
            @param {Function} [cfg.callback] Function to run with state
        */
        getState: function (key, cfg) {
            this.log("getState");
            var queryCfg,
                lrs
            ;

            if (this.recordStores.length > 0) {
                //
                // for state (for now) we are only going to store to the first LRS
                // so only get from there too
                //
                // TODO: make this the first non-allowFail LRS but for now it should
                // be good enough to make it the first since we know the LMS provided
                // LRS is the first
                //
                lrs = this.recordStores[0];

                cfg = cfg || {};

                queryCfg = {
                    agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor),
                    activity: (typeof cfg.activity !== "undefined" ? cfg.activity : this.activity)
                };
                if (typeof cfg.registration !== "undefined") {
                    queryCfg.registration = cfg.registration;
                }
                else if (this.registration !== null) {
                    queryCfg.registration = this.registration;
                }
                if (typeof cfg.callback !== "undefined") {
                    queryCfg.callback = cfg.callback;
                }

                return lrs.retrieveState(key, queryCfg);
            }

            this.log("[warning] getState: No LRSs added yet (state not retrieved)");
        },

        /**
        @method setState
        @param {String} key Key to store into the state
        @param {String|Object} val Value to store into the state, objects will be stringified to JSON
        @param {Object} [cfg] Configuration for request
            @param {Object} [cfg.agent] Agent used in query,
                defaults to 'actor' property if empty
            @param {Object} [cfg.activity] Activity used in query,
                defaults to 'activity' property if empty
            @param {Object} [cfg.registration] Registration used in query,
                defaults to 'registration' property if empty
            @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing state
            @param {String} [cfg.contentType] Content-Type to specify in headers
            @param {Boolean} [cfg.overwriteJSON] If the Content-Type is JSON, should a PUT be used?
            @param {Function} [cfg.callback] Function to run with state
        */
        setState: function (key, val, cfg) {
            this.log("setState");
            var queryCfg,
                lrs
            ;

            if (this.recordStores.length > 0) {
                //
                // for state (for now) we are only going to store to the first LRS
                // so only get from there too
                //
                // TODO: make this the first non-allowFail LRS but for now it should
                // be good enough to make it the first since we know the LMS provided
                // LRS is the first
                //
                lrs = this.recordStores[0];

                cfg = cfg || {};

                queryCfg = {
                    agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor),
                    activity: (typeof cfg.activity !== "undefined" ? cfg.activity : this.activity)
                };
                if (typeof cfg.registration !== "undefined") {
                    queryCfg.registration = cfg.registration;
                }
                else if (this.registration !== null) {
                    queryCfg.registration = this.registration;
                }
                if (typeof cfg.lastSHA1 !== "undefined") {
                    queryCfg.lastSHA1 = cfg.lastSHA1;
                }
                if (typeof cfg.contentType !== "undefined") {
                    queryCfg.contentType = cfg.contentType;
                    if ((typeof cfg.overwriteJSON !== "undefined") && (! cfg.overwriteJSON) && (TinCan.Utils.isApplicationJSON(cfg.contentType))) {
                        queryCfg.method = "POST";
                    }
                }
                if (typeof cfg.callback !== "undefined") {
                    queryCfg.callback = cfg.callback;
                }

                return lrs.saveState(key, val, queryCfg);
            }

            this.log("[warning] setState: No LRSs added yet (state not saved)");
        },

        /**
        @method deleteState
        @param {String|null} key Key to remove from the state, or null to clear all
        @param {Object} [cfg] Configuration for request
            @param {Object} [cfg.agent] Agent used in query,
                defaults to 'actor' property if empty
            @param {Object} [cfg.activity] Activity used in query,
                defaults to 'activity' property if empty
            @param {Object} [cfg.registration] Registration used in query,
                defaults to 'registration' property if empty
            @param {Function} [cfg.callback] Function to run with state
        */
        deleteState: function (key, cfg) {
            this.log("deleteState");
            var queryCfg,
                lrs
            ;

            if (this.recordStores.length > 0) {
                //
                // for state (for now) we are only going to store to the first LRS
                // so only get from there too
                //
                // TODO: make this the first non-allowFail LRS but for now it should
                // be good enough to make it the first since we know the LMS provided
                // LRS is the first
                //
                lrs = this.recordStores[0];

                cfg = cfg || {};

                queryCfg = {
                    agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor),
                    activity: (typeof cfg.activity !== "undefined" ? cfg.activity : this.activity)
                };
                if (typeof cfg.registration !== "undefined") {
                    queryCfg.registration = cfg.registration;
                }
                else if (this.registration !== null) {
                    queryCfg.registration = this.registration;
                }
                if (typeof cfg.callback !== "undefined") {
                    queryCfg.callback = cfg.callback;
                }

                return lrs.dropState(key, queryCfg);
            }

            this.log("[warning] deleteState: No LRSs added yet (state not deleted)");
        },

        /**
        @method getActivityProfile
        @param {String} key Key to retrieve from the profile
        @param {Object} [cfg] Configuration for request
            @param {Object} [cfg.activity] Activity used in query,
                defaults to 'activity' property if empty
            @param {Function} [cfg.callback] Function to run with activity profile
        */
        getActivityProfile: function (key, cfg) {
            this.log("getActivityProfile");
            var queryCfg,
                lrs
            ;

            if (this.recordStores.length > 0) {
                //
                // for activity profiles (for now) we are only going to store to the first LRS
                // so only get from there too
                //
                // TODO: make this the first non-allowFail LRS but for now it should
                // be good enough to make it the first since we know the LMS provided
                // LRS is the first
                //
                lrs = this.recordStores[0];

                cfg = cfg || {};

                queryCfg = {
                    activity: (typeof cfg.activity !== "undefined" ? cfg.activity : this.activity)
                };
                if (typeof cfg.callback !== "undefined") {
                    queryCfg.callback = cfg.callback;
                }

                return lrs.retrieveActivityProfile(key, queryCfg);
            }

            this.log("[warning] getActivityProfile: No LRSs added yet (activity profile not retrieved)");
        },

        /**
        @method setActivityProfile
        @param {String} key Key to store into the activity profile
        @param {String|Object} val Value to store into the activity profile, objects will be stringified to JSON
        @param {Object} [cfg] Configuration for request
            @param {Object} [cfg.activity] Activity used in query,
                defaults to 'activity' property if empty
            @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile
            @param {String} [cfg.contentType] Content-Type to specify in headers
            @param {Boolean} [cfg.overwriteJSON] If the Content-Type is JSON, should a PUT be used?
            @param {Function} [cfg.callback] Function to run with activity profile
        */
        setActivityProfile: function (key, val, cfg) {
            this.log("setActivityProfile");
            var queryCfg,
                lrs
            ;

            if (this.recordStores.length > 0) {
                //
                // for activity profile (for now) we are only going to store to the first LRS
                // so only get from there too
                //
                // TODO: make this the first non-allowFail LRS but for now it should
                // be good enough to make it the first since we know the LMS provided
                // LRS is the first
                //
                lrs = this.recordStores[0];

                cfg = cfg || {};

                queryCfg = {
                    activity: (typeof cfg.activity !== "undefined" ? cfg.activity : this.activity)
                };
                if (typeof cfg.callback !== "undefined") {
                    queryCfg.callback = cfg.callback;
                }
                if (typeof cfg.lastSHA1 !== "undefined") {
                    queryCfg.lastSHA1 = cfg.lastSHA1;
                }
                if (typeof cfg.contentType !== "undefined") {
                    queryCfg.contentType = cfg.contentType;
                    if ((typeof cfg.overwriteJSON !== "undefined") && (! cfg.overwriteJSON) && (TinCan.Utils.isApplicationJSON(cfg.contentType))) {
                        queryCfg.method = "POST";
                    }
                }

                return lrs.saveActivityProfile(key, val, queryCfg);
            }

            this.log("[warning] setActivityProfile: No LRSs added yet (activity profile not saved)");
        },

        /**
        @method deleteActivityProfile
        @param {String|null} key Key to remove from the activity profile, or null to clear all
        @param {Object} [cfg] Configuration for request
            @param {Object} [cfg.activity] Activity used in query,
                defaults to 'activity' property if empty
            @param {Function} [cfg.callback] Function to run with activity profile
        */
        deleteActivityProfile: function (key, cfg) {
            this.log("deleteActivityProfile");
            var queryCfg,
                lrs
            ;

            if (this.recordStores.length > 0) {
                //
                // for activity profile (for now) we are only going to store to the first LRS
                // so only get from there too
                //
                // TODO: make this the first non-allowFail LRS but for now it should
                // be good enough to make it the first since we know the LMS provided
                // LRS is the first
                //
                lrs = this.recordStores[0];

                cfg = cfg || {};

                queryCfg = {
                    activity: (typeof cfg.activity !== "undefined" ? cfg.activity : this.activity)
                };
                if (typeof cfg.callback !== "undefined") {
                    queryCfg.callback = cfg.callback;
                }

                return lrs.dropActivityProfile(key, queryCfg);
            }

            this.log("[warning] deleteActivityProfile: No LRSs added yet (activity profile not deleted)");
        },

        /**
        @method getAgentProfile
        @param {String} key Key to retrieve from the profile
        @param {Object} [cfg] Configuration for request
            @param {Object} [cfg.agent] Agent used in query,
                defaults to 'actor' property if empty
            @param {Function} [cfg.callback] Function to run with agent profile
        */
        getAgentProfile: function (key, cfg) {
            this.log("getAgentProfile");
            var queryCfg,
                lrs
            ;

            if (this.recordStores.length > 0) {
                //
                // for agent profiles (for now) we are only going to store to the first LRS
                // so only get from there too
                //
                // TODO: make this the first non-allowFail LRS but for now it should
                // be good enough to make it the first since we know the LMS provided
                // LRS is the first
                //
                lrs = this.recordStores[0];

                cfg = cfg || {};

                queryCfg = {
                    agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor)
                };
                if (typeof cfg.callback !== "undefined") {
                    queryCfg.callback = cfg.callback;
                }

                return lrs.retrieveAgentProfile(key, queryCfg);
            }

            this.log("[warning] getAgentProfile: No LRSs added yet (agent profile not retrieved)");
        },

        /**
        @method setAgentProfile
        @param {String} key Key to store into the agent profile
        @param {String|Object} val Value to store into the agent profile, objects will be stringified to JSON
        @param {Object} [cfg] Configuration for request
            @param {Object} [cfg.agent] Agent used in query,
                defaults to 'actor' property if empty
            @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile
            @param {String} [cfg.contentType] Content-Type to specify in headers
            @param {Boolean} [cfg.overwriteJSON] If the Content-Type is JSON, should a PUT be used?
            @param {Function} [cfg.callback] Function to run with agent profile
        */
        setAgentProfile: function (key, val, cfg) {
            this.log("setAgentProfile");
            var queryCfg,
                lrs
            ;

            if (this.recordStores.length > 0) {
                //
                // for agent profile (for now) we are only going to store to the first LRS
                // so only get from there too
                //
                // TODO: make this the first non-allowFail LRS but for now it should
                // be good enough to make it the first since we know the LMS provided
                // LRS is the first
                //
                lrs = this.recordStores[0];

                cfg = cfg || {};

                queryCfg = {
                    agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor)
                };
                if (typeof cfg.callback !== "undefined") {
                    queryCfg.callback = cfg.callback;
                }
                if (typeof cfg.lastSHA1 !== "undefined") {
                    queryCfg.lastSHA1 = cfg.lastSHA1;
                }
                if (typeof cfg.contentType !== "undefined") {
                    queryCfg.contentType = cfg.contentType;
                    if ((typeof cfg.overwriteJSON !== "undefined") && (! cfg.overwriteJSON) && (TinCan.Utils.isApplicationJSON(cfg.contentType))) {
                        queryCfg.method = "POST";
                    }
                }

                return lrs.saveAgentProfile(key, val, queryCfg);
            }

            this.log("[warning] setAgentProfile: No LRSs added yet (agent profile not saved)");
        },

        /**
        @method deleteAgentProfile
        @param {String|null} key Key to remove from the agent profile, or null to clear all
        @param {Object} [cfg] Configuration for request
            @param {Object} [cfg.agent] Agent used in query,
                defaults to 'actor' property if empty
            @param {Function} [cfg.callback] Function to run with agent profile
        */
        deleteAgentProfile: function (key, cfg) {
            this.log("deleteAgentProfile");
            var queryCfg,
                lrs
            ;

            if (this.recordStores.length > 0) {
                //
                // for agent profile (for now) we are only going to store to the first LRS
                // so only get from there too
                //
                // TODO: make this the first non-allowFail LRS but for now it should
                // be good enough to make it the first since we know the LMS provided
                // LRS is the first
                //
                lrs = this.recordStores[0];

                cfg = cfg || {};

                queryCfg = {
                    agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor)
                };
                if (typeof cfg.callback !== "undefined") {
                    queryCfg.callback = cfg.callback;
                }

                return lrs.dropAgentProfile(key, queryCfg);
            }

            this.log("[warning] deleteAgentProfile: No LRSs added yet (agent profile not deleted)");
        }
    };

    /**
    @property DEBUG
    @static
    @default false
    */
    TinCan.DEBUG = false;

    /**
    Turn on debug logging

    @method enableDebug
    @static
    */
    TinCan.enableDebug = function () {
        TinCan.DEBUG = true;
    };

    /**
    Turn off debug logging

    @method disableDebug
    @static
    */
    TinCan.disableDebug = function () {
        TinCan.DEBUG = false;
    };

    /**
    @method versions
    @return {Array} Array of supported version numbers
    @static
    */
    TinCan.versions = function () {
        // newest first so we can use the first as the default
        return [
            "1.0.2",
            "1.0.1",
            "1.0.0",
            "0.95",
            "0.9"
        ];
    };

    /*global module*/
    // Support the CommonJS method for exporting our single global
    if (typeof module === "object") {
        module.exports = TinCan;
    }
}());

/*
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.Utils
**/
(function () {
    "use strict";

    /**
    @class TinCan.Utils
    */
    TinCan.Utils = {
        defaultEncoding: "utf8",

        /**
        Generates a UUIDv4 compliant string that should be reasonably unique

        @method getUUID
        @return {String} UUID
        @static

        Excerpt from: http://www.broofa.com/Tools/Math.uuid.js (v1.4)
        http://www.broofa.com
        mailto:robert@broofa.com
        Copyright (c) 2010 Robert Kieffer
        Dual licensed under the MIT and GPL licenses.
        */
        getUUID: function () {
            /*jslint bitwise: true, eqeq: true */
            return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
                /[xy]/g,
                function (c) {
                    var r = Math.random() * 16|0, v = c == "x" ? r : (r&0x3|0x8);
                    return v.toString(16);
                }
            );
        },

        /**
        @method getISODateString
        @static
        @param {Date} date Date to stringify
        @return {String} ISO date String
        */
        getISODateString: function (d) {
            function pad (val, n) {
                var padder,
                    tempVal;
                if (typeof val === "undefined" || val === null) {
                    val = 0;
                }
                if (typeof n === "undefined" || n === null) {
                    n = 2;
                }
                padder = Math.pow(10, n-1);
                tempVal = val.toString();

                while (val < padder && padder > 1) {
                    tempVal = "0" + tempVal;
                    padder = padder / 10;
                }

                return tempVal;
            }

            return d.getUTCFullYear() + "-" +
                pad(d.getUTCMonth() + 1) + "-" +
                pad(d.getUTCDate()) + "T" +
                pad(d.getUTCHours()) + ":" +
                pad(d.getUTCMinutes()) + ":" +
                pad(d.getUTCSeconds()) + "." +
                pad(d.getUTCMilliseconds(), 3) + "Z";
        },

        /**
        @method convertISO8601DurationToMilliseconds
        @static
        @param {String} ISO8601Duration Duration in ISO8601 format
        @return {Int} Duration in milliseconds

        Note: does not handle input strings with years, months and days
        */
        convertISO8601DurationToMilliseconds: function (ISO8601Duration) {
            var isValueNegative = (ISO8601Duration.indexOf("-") >= 0),
                indexOfT = ISO8601Duration.indexOf("T"),
                indexOfH = ISO8601Duration.indexOf("H"),
                indexOfM = ISO8601Duration.indexOf("M"),
                indexOfS = ISO8601Duration.indexOf("S"),
                hours,
                minutes,
                seconds,
                durationInMilliseconds;

            if ((indexOfT === -1) || ((indexOfM !== -1) && (indexOfM < indexOfT)) || (ISO8601Duration.indexOf("D") !== -1) || (ISO8601Duration.indexOf("Y") !== -1)) {
                throw new Error("ISO 8601 timestamps including years, months and/or days are not currently supported");
            }

            if (indexOfH === -1) {
                indexOfH = indexOfT;
                hours = 0;
            }
            else {
                hours = parseInt(ISO8601Duration.slice(indexOfT + 1, indexOfH), 10);
            }

            if (indexOfM === -1) {
                indexOfM = indexOfT;
                minutes = 0;
            }
            else {
                minutes = parseInt(ISO8601Duration.slice(indexOfH + 1, indexOfM), 10);
            }

            seconds = parseFloat(ISO8601Duration.slice(indexOfM + 1, indexOfS));

            durationInMilliseconds = parseInt((((((hours * 60) + minutes) * 60) + seconds) * 1000), 10);
            if (isNaN(durationInMilliseconds)){
                durationInMilliseconds = 0;
            }
            if (isValueNegative) {
                durationInMilliseconds = durationInMilliseconds * -1;
            }

            return durationInMilliseconds;
        },

        /**
        @method convertMillisecondsToISO8601Duration
        @static
        @param {Int} inputMilliseconds Duration in milliseconds
        @return {String} Duration in ISO8601 format
        */
        convertMillisecondsToISO8601Duration: function (inputMilliseconds) {
            var hours,
                minutes,
                seconds,
                i_inputMilliseconds = parseInt(inputMilliseconds, 10),
                i_inputCentiseconds,
                inputIsNegative = "",
                rtnStr = "";

            //round to nearest 0.01 seconds
            i_inputCentiseconds = Math.round(i_inputMilliseconds / 10);

            if (i_inputCentiseconds < 0) {
                inputIsNegative = "-";
                i_inputCentiseconds = i_inputCentiseconds * -1;
            }

            hours = parseInt(((i_inputCentiseconds) / 360000), 10);
            minutes = parseInt((((i_inputCentiseconds) % 360000) / 6000), 10);
            seconds = (((i_inputCentiseconds) % 360000) % 6000) / 100;

            rtnStr = inputIsNegative + "PT";
            if (hours > 0) {
                rtnStr += hours + "H";
            }

            if (minutes > 0) {
                rtnStr += minutes + "M";
            }

            rtnStr += seconds + "S";

            return rtnStr;
        },

        /**
        @method getSHA1String
        @static
        @param {String} str Content to hash
        @return {String} SHA1 for contents
        */
        getSHA1String: function (str) {
            /*global CryptoJS*/

            return CryptoJS.SHA1(str).toString(CryptoJS.enc.Hex);
        },

        /**
        @method getSHA256String
        @static
        @param {ArrayBuffer|String} content Content to hash
        @return {String} SHA256 for contents
        */
        getSHA256String: function (content) {
            /*global CryptoJS*/

            if (Object.prototype.toString.call(content) === "[object ArrayBuffer]") {
                content = CryptoJS.lib.WordArray.create(content);
            }
            return CryptoJS.SHA256(content).toString(CryptoJS.enc.Hex);
        },

        /**
        @method getBase64String
        @static
        @param {String} str Content to encode
        @return {String} Base64 encoded contents
        */
        getBase64String: function (str) {
            /*global CryptoJS*/

            return CryptoJS.enc.Base64.stringify(
                CryptoJS.enc.Latin1.parse(str)
            );
        },

        /**
        Intended to be inherited by objects with properties that store
        display values in a language based "dictionary"

        @method getLangDictionaryValue
        @param {String} prop Property name storing the dictionary
        @param {String} [lang] Language to return
        @return {String}
        */
        getLangDictionaryValue: function (prop, lang) {
            var langDict = this[prop],
                key;

            if (typeof lang !== "undefined" && typeof langDict[lang] !== "undefined") {
                return langDict[lang];
            }
            if (typeof langDict.und !== "undefined") {
                return langDict.und;
            }
            if (typeof langDict["en-US"] !== "undefined") {
                return langDict["en-US"];
            }
            for (key in langDict) {
                if (langDict.hasOwnProperty(key)) {
                    return langDict[key];
                }
            }

            return "";
        },

        /**
        @method parseURL
        @param {String} url
        @param {Object} [options]
            @param {Boolean} [options.allowRelative] Option to allow relative URLs
        @return {Object} Object of values
        @private
        */
        parseURL: function (url, cfg) {
            //
            // see http://stackoverflow.com/a/21553982
            // and http://stackoverflow.com/a/2880929
            //
            var isRelative = url.charAt(0) === "/",
                _reURLInformation = [
                    "(/[^?#]*)", // pathname
                    "(\\?[^#]*|)", // search
                    "(#.*|)$" // hash
                ],
                reURLInformation,
                match,
                result,
                paramMatch,
                pl     = /\+/g,  // Regex for replacing addition symbol with a space
                search = /([^&=]+)=?([^&]*)/g,
                decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); };

            cfg = cfg || {};

            //
            // this method in an earlier version supported relative URLs, mostly to provide
            // support to the `LRS.moreStatements` method, that functionality was removed and
            // subsequently restored but with the addition of the option for allowing relative
            // URLs to be accepted which is the reason for the "helpful" exception message here
            //
            if (! isRelative) {
                //
                // not relative so make sure they have a scheme, host, etc.
                //
                _reURLInformation.unshift(
                    "^(https?:)//", // scheme
                    "(([^:/?#]*)(?::([0-9]+))?)" // host (hostname and port)
                );

                //
                // our regex requires there to be a '/' for the detection of the start
                // of the path, we can detect a '/' using indexOf beyond the part of the
                // scheme, since we've restricted scheme to 'http' or 'https' and because
                // a hostname is guaranteed to be there we can detect beyond the '://'
                // based on position, then tack on a trailing '/' because it can't be
                // part of the path
                //
                if (url.indexOf("/", 8) === -1) {
                    url = url + "/";
                }
            }
            else {
                //
                // relative so make sure they allow that explicitly
                //
                if (typeof cfg.allowRelative === "undefined" || ! cfg.allowRelative) {
                    throw new Error("Refusing to parse relative URL without 'allowRelative' option");
                }
            }

            reURLInformation = new RegExp(_reURLInformation.join(""));
            match = url.match(reURLInformation);
            if (match === null) {
                throw new Error("Unable to parse URL regular expression did not match: '" + url + "'");
            }

            // 'path' is for backwards compatibility
            if (isRelative) {
                result = {
                    protocol: null,
                    host: null,
                    hostname: null,
                    port: null,
                    path: null,
                    pathname: match[1],
                    search: match[2],
                    hash: match[3],
                    params: {}
                };

                result.path = result.pathname;
            }
            else {
                result = {
                    protocol: match[1],
                    host: match[2],
                    hostname: match[3],
                    port: match[4],
                    pathname: match[5],
                    search: match[6],
                    hash: match[7],
                    params: {}
                };

                result.path = result.protocol + "//" + result.host + result.pathname;
            }

            if (result.search !== "") {
                // extra parens to let jshint know this is an expression
                while ((paramMatch = search.exec(result.search.substring(1)))) {
                    result.params[decode(paramMatch[1])] = decode(paramMatch[2]);
                }
            }

            return result;
        },

        /**
        @method getServerRoot
        @param {String} absoluteUrl
        @return {String} server root of url
        @private
        */
        getServerRoot: function (absoluteUrl) {
            var urlParts = absoluteUrl.split("/");
            return urlParts[0] + "//" + urlParts[2];
        },

        /**
        @method getContentTypeFromHeader
        @static
        @param {String} header Content-Type header value
        @return {String} Primary value from Content-Type
        */
        getContentTypeFromHeader: function (header) {
            return (String(header).split(";"))[0];
        },

        /**
        @method isApplicationJSON
        @static
        @param {String} header Content-Type header value
        @return {Boolean} whether "application/json" was matched
        */
        isApplicationJSON: function (header) {
            return TinCan.Utils.getContentTypeFromHeader(header).toLowerCase().indexOf("application/json") === 0;
        },

        /**
        @method stringToArrayBuffer
        @static
        @param {String} content String of content to convert to an ArrayBuffer
        @param {String} [encoding] Encoding to use for conversion
        @return {ArrayBuffer} Converted content
        */
        stringToArrayBuffer: function () {
            TinCan.prototype.log("stringToArrayBuffer not overloaded - no environment loaded?");
        },

        /**
        @method stringFromArrayBuffer
        @static
        @param {ArrayBuffer} content ArrayBuffer of content to convert to a String
        @param {String} [encoding] Encoding to use for conversion
        @return {String} Converted content
        */
        stringFromArrayBuffer: function () {
            TinCan.prototype.log("stringFromArrayBuffer not overloaded - no environment loaded?");
        }
    };
}());

/*
    Copyright 2012-2013 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.LRS
**/
(function () {
    "use strict";
    /**
    @class TinCan.LRS
    @constructor
    */
    var LRS = TinCan.LRS = function (cfg) {
        this.log("constructor");

        /**
        @property endpoint
        @type String
        */
        this.endpoint = null;

        /**
        @property version
        @type String
        */
        this.version = null;

        /**
        @property auth
        @type String
        */
        this.auth = null;

        /**
        @property allowFail
        @type Boolean
        @default true
        */
        this.allowFail = true;

        /**
        @property extended
        @type Object
        */
        this.extended = null;

        this.init(cfg);
    };
    LRS.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "LRS",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        */
        init: function (cfg) {
            this.log("init");

            var versions = TinCan.versions(),
                versionMatch = false,
                i
            ;

            cfg = cfg || {};

            if (cfg.hasOwnProperty("alertOnRequestFailure")) {
                this.log("'alertOnRequestFailure' is deprecated (alerts have been removed) no need to set it now");
            }

            if (! cfg.hasOwnProperty("endpoint") || cfg.endpoint === null || cfg.endpoint === "") {
                this.log("[error] LRS invalid: no endpoint");
                throw {
                    code: 3,
                    mesg: "LRS invalid: no endpoint"
                };
            }

            this.endpoint = String(cfg.endpoint);
            if (this.endpoint.slice(-1) !== "/") {
                this.log("adding trailing slash to endpoint");
                this.endpoint += "/";
            }

            if (cfg.hasOwnProperty("allowFail")) {
                this.allowFail = cfg.allowFail;
            }

            if (cfg.hasOwnProperty("auth")) {
                this.auth = cfg.auth;
            }
            else if (cfg.hasOwnProperty("username") && cfg.hasOwnProperty("password")) {
                this.auth = "Basic " + TinCan.Utils.getBase64String(cfg.username + ":" + cfg.password);
            }

            if (cfg.hasOwnProperty("extended")) {
                this.extended = cfg.extended;
            }

            //
            // provide a hook method that environments can override
            // to handle anything necessary in the initialization
            // process that is customized to them, such as cross domain
            // setup in browsers, default implementation is empty
            //
            // this hook must run prior to version detection so that
            // request handling can be set up before requesting the
            // LRS version via the /about resource
            //
            this._initByEnvironment(cfg);

            if (typeof cfg.version !== "undefined") {
                this.log("version: " + cfg.version);
                for (i = 0; i < versions.length; i += 1) {
                    if (versions[i] === cfg.version) {
                        versionMatch = true;
                        break;
                    }
                }
                if (! versionMatch) {
                    this.log("[error] LRS invalid: version not supported (" + cfg.version + ")");
                    throw {
                        code: 5,
                        mesg: "LRS invalid: version not supported (" + cfg.version + ")"
                    };
                }
                this.version = cfg.version;
            }
            else {
                //
                // assume max supported when not specified,
                // TODO: add detection of LRS from call to endpoint
                //
                this.version = versions[0];
            }
        },

        /**
        Creates and returns a boundary for separating parts in
        requests where the statement has an attachment

        @method _getBoundary
        @private
        */
        _getBoundary: function () {
            return TinCan.Utils.getUUID().replace(/-/g, "");
        },

        /**
        Method should be overloaded by an environment to do per
        environment specifics such that the LRS can make a call
        to set the version if not provided

        @method _initByEnvironment
        @private
        */
        _initByEnvironment: function () {
            this.log("_initByEnvironment not overloaded - no environment loaded?");
        },

        /**
        Method should be overloaded by an environment to do per
        environment specifics for sending requests to the LRS

        @method _makeRequest
        @private
        */
        _makeRequest: function () {
            this.log("_makeRequest not overloaded - no environment loaded?");
        },

        /**
        Method should be overloaded by an environment to do per
        environment specifics for building multipart request data

        @method _getMultipartRequestData
        @private
        */
        _getMultipartRequestData: function () {
            this.log("_getMultipartRequestData not overloaded - no environment loaded?");
        },

        /**
        Method is overloaded by the browser environment in order to test converting an
        HTTP request that is greater than a defined length

        @method _IEModeConversion
        @private
        */
        _IEModeConversion: function () {
            this.log("_IEModeConversion not overloaded - browser environment not loaded.");
        },

        _processGetStatementResult: function (xhr, params) {
            var boundary,
                parsedResponse,
                statement,
                attachmentMap = {},
                i;

            if (! params.attachments) {
                return TinCan.Statement.fromJSON(xhr.responseText);
            }

            boundary = xhr.getResponseHeader("Content-Type").split("boundary=")[1];

            parsedResponse = this._parseMultipart(boundary, xhr.response);
            statement = JSON.parse(parsedResponse[0].body);
            for (i = 1; i < parsedResponse.length; i += 1) {
                attachmentMap[parsedResponse[i].headers["X-Experience-API-Hash"]] = parsedResponse[i].body;
            }

            this._assignAttachmentContent([statement], attachmentMap);

            return new TinCan.Statement(statement);
        },

        /**
        Method used to send a request via browser objects to the LRS

        @method sendRequest
        @param {Object} cfg Configuration for request
            @param {String} cfg.url URL portion to add to endpoint
            @param {String} [cfg.method] GET, PUT, POST, etc.
            @param {Object} [cfg.params] Parameters to set on the querystring
            @param {String|ArrayBuffer} [cfg.data] Body content as a String or ArrayBuffer
            @param {Object} [cfg.headers] Additional headers to set in the request
            @param {Function} [cfg.callback] Function to run at completion
                @param {String|Null} cfg.callback.err If an error occurred, this parameter will contain the HTTP status code.
                    If the operation succeeded, err will be null.
                @param {Object} cfg.callback.xhr XHR object
            @param {Boolean} [cfg.ignore404] Whether 404 status codes should be considered an error
            @param {Boolean} [cfg.expectMultipart] Whether to expect the response to be a multipart response
        @return {Object} XHR if called in a synchronous way (in other words no callback)
        */
        sendRequest: function (cfg) {
            this.log("sendRequest");
            var fullUrl = this.endpoint + cfg.url,
                headers = {},
                prop
            ;

            // respect absolute URLs passed in
            if (cfg.url.indexOf("http") === 0) {
                fullUrl = cfg.url;
            }

            // add extended LMS-specified values to the params
            if (this.extended !== null) {
                cfg.params = cfg.params || {};

                for (prop in this.extended) {
                    if (this.extended.hasOwnProperty(prop)) {
                        // don't overwrite cfg.params values that have already been added to the request with our extended params
                        if (! cfg.params.hasOwnProperty(prop)) {
                            if (this.extended[prop] !== null) {
                                cfg.params[prop] = this.extended[prop];
                            }
                        }
                    }
                }
            }

            // consolidate headers
            headers.Authorization = this.auth;
            if (this.version !== "0.9") {
                headers["X-Experience-API-Version"] = this.version;
            }

            for (prop in cfg.headers) {
                if (cfg.headers.hasOwnProperty(prop)) {
                    headers[prop] = cfg.headers[prop];
                }
            }

            return this._makeRequest(fullUrl, headers, cfg);
        },

        /**
        Method used to determine the LRS version

        @method about
        @param {Object} cfg Configuration object for the about request
            @param {Function} [cfg.callback] Callback to execute upon receiving a response
            @param {Object} [cfg.params] this is needed, but can be empty
        @return {Object} About which holds the version, or asyncrhonously calls a specified callback
        */
        about: function (cfg) {
            this.log("about");
            var requestCfg,
                requestResult,
                callbackWrapper;

            cfg = cfg || {};

            requestCfg = {
                url: "about",
                method: "GET",
                params: {}
            };
            if (typeof cfg.callback !== "undefined") {
                callbackWrapper = function (err, xhr) {
                    var result = xhr;

                    if (err === null) {
                        result = TinCan.About.fromJSON(xhr.responseText);
                    }

                    cfg.callback(err, result);
                };
                requestCfg.callback = callbackWrapper;
            }

            requestResult = this.sendRequest(requestCfg);

            if (callbackWrapper) {
                return;
            }

            if (requestResult.err === null) {
                requestResult.xhr = TinCan.About.fromJSON(requestResult.xhr.responseText);
            }
            return requestResult;
        },

        /**
        Save a statement, when used from a browser sends to the endpoint using the RESTful interface.
        Use a callback to make the call asynchronous.

        @method saveStatement
        @param {TinCan.Statement} statement to send
        @param {Object} [cfg] Configuration used when saving
            @param {Function} [cfg.callback] Callback to execute on completion
        */
        saveStatement: function (stmt, cfg) {
            this.log("saveStatement");
            var requestCfg = {
                    url: "statements",
                    headers: {}
                },
                versionedStatement,
                requestAttachments = [],
                boundary,
                i;

            cfg = cfg || {};

            try {
                versionedStatement = stmt.asVersion( this.version );
            }
            catch (ex) {
                if (this.allowFail) {
                    this.log("[warning] statement could not be serialized in version (" + this.version + "): " + ex);
                    if (typeof cfg.callback !== "undefined") {
                        cfg.callback(null, null);
                        return;
                    }
                    return {
                        err: null,
                        xhr: null
                    };
                }

                this.log("[error] statement could not be serialized in version (" + this.version + "): " + ex);
                if (typeof cfg.callback !== "undefined") {
                    cfg.callback(ex, null);
                    return;
                }
                return {
                    err: ex,
                    xhr: null
                };
            }

            if (versionedStatement.hasOwnProperty("attachments") && stmt.hasAttachmentWithContent()) {
                boundary = this._getBoundary();

                requestCfg.headers["Content-Type"] = "multipart/mixed; boundary=" + boundary;

                for (i = 0; i < stmt.attachments.length; i += 1) {
                    if (stmt.attachments[i].content !== null) {
                        requestAttachments.push(stmt.attachments[i]);
                    }
                }

                try {
                    requestCfg.data = this._getMultipartRequestData(boundary, versionedStatement, requestAttachments);
                }
                catch (ex) {
                    if (this.allowFail) {
                        this.log("[warning] multipart request data could not be created (attachments probably not supported): " + ex);
                        if (typeof cfg.callback !== "undefined") {
                            cfg.callback(null, null);
                            return;
                        }
                        return {
                            err: null,
                            xhr: null
                        };
                    }

                    this.log("[error] multipart request data could not be created (attachments probably not supported): " + ex);
                    if (typeof cfg.callback !== "undefined") {
                        cfg.callback(ex, null);
                        return;
                    }
                    return {
                        err: ex,
                        xhr: null
                    };
                }
            }
            else {
                requestCfg.headers["Content-Type"] = "application/json";
                requestCfg.data = JSON.stringify(versionedStatement);
            }
            if (stmt.id !== null) {
                requestCfg.method = "PUT";
                requestCfg.params = {
                    statementId: stmt.id
                };
            }
            else {
                requestCfg.method = "POST";
            }

            if (typeof cfg.callback !== "undefined") {
                requestCfg.callback = cfg.callback;
            }

            return this.sendRequest(requestCfg);
        },

        /**
        Retrieve a statement, when used from a browser sends to the endpoint using the RESTful interface.

        @method retrieveStatement
        @param {String} ID of statement to retrieve
        @param {Object} [cfg] Configuration options
            @param {Object} [cfg.params] Query parameters
                @param {Boolean} [cfg.params.attachments] Include attachments in multipart response or don't (default: false)
            @param {Function} [cfg.callback] Callback to execute on completion
        @return {TinCan.Statement} Statement retrieved
        */
        retrieveStatement: function (stmtId, cfg) {
            this.log("retrieveStatement");
            var requestCfg,
                requestResult,
                callbackWrapper,
                lrs = this;

            cfg = cfg || {};
            cfg.params = cfg.params || {};

            requestCfg = {
                url: "statements",
                method: "GET",
                params: {
                    statementId: stmtId
                }
            };
            if (cfg.params.attachments) {
                requestCfg.params.attachments = true;
                requestCfg.expectMultipart = true;
            }
            if (typeof cfg.callback !== "undefined") {
                callbackWrapper = function (err, xhr) {
                    var result = xhr;

                    if (err === null) {
                        result = lrs._processGetStatementResult(xhr, cfg.params);
                    }

                    cfg.callback(err, result);
                };
                requestCfg.callback = callbackWrapper;
            }

            requestResult = this.sendRequest(requestCfg);
            if (! callbackWrapper) {
                requestResult.statement = null;
                if (requestResult.err === null) {
                    requestResult.statement = lrs._processGetStatementResult(requestResult.xhr, cfg.params);
                }
            }

            return requestResult;
        },

        /**
        Retrieve a voided statement, when used from a browser sends to the endpoint using the RESTful interface.

        @method retrieveVoidedStatement
        @param {String} ID of voided statement to retrieve
        @param {Object} [cfg] Configuration options
            @param {Object} [cfg.params] Query parameters
                @param {Boolean} [cfg.params.attachments] Include attachments in multipart response or don't (default: false)
            @param {Function} [cfg.callback] Callback to execute on completion
        @return {TinCan.Statement} Statement retrieved
        */
        retrieveVoidedStatement: function (stmtId, cfg) {
            this.log("retrieveVoidedStatement");
            var requestCfg,
                requestResult,
                callbackWrapper,
                lrs = this;

            cfg = cfg || {};
            cfg.params = cfg.params || {};

            requestCfg = {
                url: "statements",
                method: "GET",
                params: {}
            };
            if (this.version === "0.9" || this.version === "0.95") {
                requestCfg.params.statementId = stmtId;
            }
            else {
                requestCfg.params.voidedStatementId = stmtId;
                if (cfg.params.attachments) {
                    requestCfg.params.attachments = true;
                    requestCfg.expectMultipart = true;
                }
            }

            if (typeof cfg.callback !== "undefined") {
                callbackWrapper = function (err, xhr) {
                    var result = xhr;

                    if (err === null) {
                        result = lrs._processGetStatementResult(xhr, cfg.params);
                    }

                    cfg.callback(err, result);
                };
                requestCfg.callback = callbackWrapper;
            }

            requestResult = this.sendRequest(requestCfg);
            if (! callbackWrapper) {
                requestResult.statement = null;
                if (requestResult.err === null) {
                    requestResult.statement = lrs._processGetStatementResult(requestResult.xhr, cfg.params);
                }
            }

            return requestResult;
        },

        /**
        Save a set of statements, when used from a browser sends to the endpoint using the RESTful interface.
        Use a callback to make the call asynchronous.

        @method saveStatements
        @param {Array} Array of statements or objects convertable to statements
        @param {Object} [cfg] Configuration used when saving
            @param {Function} [cfg.callback] Callback to execute on completion
        */
        saveStatements: function (stmts, cfg) {
            this.log("saveStatements");
            var requestCfg = {
                    url: "statements",
                    method: "POST",
                    headers: {}
                },
                versionedStatement,
                versionedStatements = [],
                requestAttachments = [],
                boundary,
                i,
                j;

            cfg = cfg || {};

            if (stmts.length === 0) {
                if (typeof cfg.callback !== "undefined") {
                    cfg.callback(new Error("no statements"), null);
                    return;
                }
                return {
                    err: new Error("no statements"),
                    xhr: null
                };
            }

            for (i = 0; i < stmts.length; i += 1) {
                try {
                    versionedStatement = stmts[i].asVersion( this.version );
                }
                catch (ex) {
                    if (this.allowFail) {
                        this.log("[warning] statement could not be serialized in version (" + this.version + "): " + ex);
                        if (typeof cfg.callback !== "undefined") {
                            cfg.callback(null, null);
                            return;
                        }
                        return {
                            err: null,
                            xhr: null
                        };
                    }

                    this.log("[error] statement could not be serialized in version (" + this.version + "): " + ex);
                    if (typeof cfg.callback !== "undefined") {
                        cfg.callback(ex, null);
                        return;
                    }
                    return {
                        err: ex,
                        xhr: null
                    };
                }

                if (stmts[i].hasAttachmentWithContent()) {
                    for (j = 0; j < stmts[i].attachments.length; j += 1) {
                        if (stmts[i].attachments[j].content !== null) {
                            requestAttachments.push(stmts[i].attachments[j]);
                        }
                    }
                }

                versionedStatements.push(versionedStatement);
            }

            if (requestAttachments.length !== 0) {
                boundary = this._getBoundary();

                requestCfg.headers["Content-Type"] = "multipart/mixed; boundary=" + boundary;

                try {
                    requestCfg.data = this._getMultipartRequestData(boundary, versionedStatements, requestAttachments);
                }
                catch (ex) {
                    if (this.allowFail) {
                        this.log("[warning] multipart request data could not be created (attachments probably not supported): " + ex);
                        if (typeof cfg.callback !== "undefined") {
                            cfg.callback(null, null);
                            return;
                        }
                        return {
                            err: null,
                            xhr: null
                        };
                    }

                    this.log("[error] multipart request data could not be created (attachments probably not supported): " + ex);
                    if (typeof cfg.callback !== "undefined") {
                        cfg.callback(ex, null);
                        return;
                    }
                    return {
                        err: ex,
                        xhr: null
                    };
                }
            }
            else {
                requestCfg.headers["Content-Type"] = "application/json";
                requestCfg.data = JSON.stringify(versionedStatements);
            }

            if (typeof cfg.callback !== "undefined") {
                requestCfg.callback = cfg.callback;
            }

            return this.sendRequest(requestCfg);
        },

        /**
        Fetch a set of statements, when used from a browser sends to the endpoint using the
        RESTful interface.  Use a callback to make the call asynchronous.

        @method queryStatements
        @param {Object} [cfg] Configuration used to query
            @param {Object} [cfg.params] Query parameters
                @param {TinCan.Agent|TinCan.Group} [cfg.params.agent] Agent matches 'actor' or 'object'
                @param {TinCan.Verb|String} [cfg.params.verb] Verb (or verb ID) to query on
                @param {TinCan.Activity|String} [cfg.params.activity] Activity (or activity ID) to query on
                @param {String} [cfg.params.registration] Registration UUID
                @param {Boolean} [cfg.params.related_activities] Match related activities
                @param {Boolean} [cfg.params.related_agents] Match related agents
                @param {String} [cfg.params.since] Match statements stored since specified timestamp
                @param {String} [cfg.params.until] Match statements stored at or before specified timestamp
                @param {Integer} [cfg.params.limit] Number of results to retrieve
                @param {String} [cfg.params.format] One of "ids", "exact", "canonical" (default: "exact")
                @param {Boolean} [cfg.params.ascending] Return results in ascending order of stored time

                @param {TinCan.Agent} [cfg.params.actor] (Removed in 1.0.0, use 'agent' instead) Agent matches 'actor'
                @param {TinCan.Activity|TinCan.Agent|TinCan.Statement} [cfg.params.target] (Removed in 1.0.0, use 'activity' or 'agent' instead) Activity, Agent, or Statement matches 'object'
                @param {TinCan.Agent} [cfg.params.instructor] (Removed in 1.0.0, use 'agent' + 'related_agents' instead) Agent matches 'context:instructor'
                @param {Boolean} [cfg.params.context] (Removed in 1.0.0, use 'activity' instead) When filtering on target, include statements with matching context
                @param {Boolean} [cfg.params.authoritative] (Removed in 1.0.0) Get authoritative results
                @param {Boolean} [cfg.params.sparse] (Removed in 1.0.0, use 'format' instead) Get sparse results

            @param {Function} [cfg.callback] Callback to execute on completion
                @param {String|null} cfg.callback.err Error status or null if succcess
                @param {TinCan.StatementsResult|XHR} cfg.callback.response Receives a StatementsResult argument
        @return {Object} Request result
        */
        queryStatements: function (cfg) {
            this.log("queryStatements");
            var requestCfg,
                requestResult,
                callbackWrapper,
                lrs = this;

            cfg = cfg || {};
            cfg.params = cfg.params || {};

            //
            // if they misconfigured (possibly due to version mismatches) the
            // query then don't try to send a request at all, rather than give
            // them invalid results
            //
            try {
                requestCfg = this._queryStatementsRequestCfg(cfg);

                if (cfg.params.attachments) {
                    requestCfg.expectMultipart = true;
                }
            }
            catch (ex) {
                this.log("[error] Query statements failed - " + ex);
                if (typeof cfg.callback !== "undefined") {
                    cfg.callback(ex, {});
                }

                return {
                    err: ex,
                    statementsResult: null
                };
            }

            if (typeof cfg.callback !== "undefined") {
                callbackWrapper = function (err, xhr) {
                    var result = xhr,
                        parsedResponse,
                        boundary,
                        statements,
                        attachmentMap = {},
                        i;

                    if (err === null) {
                        if (! cfg.params.attachments) {
                            result = TinCan.StatementsResult.fromJSON(xhr.responseText);
                        }
                        else {
                            boundary = xhr.getResponseHeader("Content-Type").split("boundary=")[1];

                            parsedResponse = lrs._parseMultipart(boundary, xhr.response);
                            statements = JSON.parse(parsedResponse[0].body);
                            for (i = 1; i < parsedResponse.length; i += 1) {
                                attachmentMap[parsedResponse[i].headers["X-Experience-API-Hash"]] = parsedResponse[i].body;
                            }

                            lrs._assignAttachmentContent(statements.statements, attachmentMap);
                            result = new TinCan.StatementsResult({ statements: statements.statements });

                            for (i = 0; i < result.statements.length; i += 1) {
                                if (! (result.statements[i] instanceof TinCan.Statement)) {
                                    result.statements[i] = new TinCan.Statement(result.statements[i]);
                                }
                            }
                        }
                    }

                    cfg.callback(err, result);
                };
                requestCfg.callback = callbackWrapper;
            }

            requestResult = this.sendRequest(requestCfg);
            requestResult.config = requestCfg;

            if (! callbackWrapper) {
                requestResult.statementsResult = null;
                if (requestResult.err === null) {
                    requestResult.statementsResult = TinCan.StatementsResult.fromJSON(requestResult.xhr.responseText);
                }
            }

            return requestResult;
        },

        /**
        Build a request config object that can be passed to sendRequest() to make a query request

        @method _queryStatementsRequestCfg
        @private
        @param {Object} [cfg] See configuration for {{#crossLink "TinCan.LRS/queryStatements"}}{{/crossLink}}
        @return {Object} Request configuration object
        */
        _queryStatementsRequestCfg: function (cfg) {
            this.log("_queryStatementsRequestCfg");
            var params = {},
                returnCfg = {
                    url: "statements",
                    method: "GET",
                    params: params
                },
                jsonProps = [
                    "agent",
                    "actor",
                    "object",
                    "instructor"
                ],
                idProps = [
                    "verb",
                    "activity"
                ],
                valProps = [
                    "registration",
                    "context",
                    "since",
                    "until",
                    "limit",
                    "authoritative",
                    "sparse",
                    "ascending",
                    "related_activities",
                    "related_agents",
                    "format",
                    "attachments"
                ],
                i,
                prop,
                //
                // list of parameters that are supported in all versions (supported by
                // this library) of the spec
                //
                universal = {
                    verb: true,
                    registration: true,
                    since: true,
                    until: true,
                    limit: true,
                    ascending: true
                },
                //
                // future proofing here, "supported" is an object so that
                // in the future we can support a "deprecated" list to
                // throw warnings, hopefully the spec uses deprecation phases
                // for the removal of these things
                //
                compatibility = {
                    "0.9": {
                        supported: {
                            actor: true,
                            instructor: true,
                            target: true,
                            object: true,
                            context: true,
                            authoritative: true,
                            sparse: true
                        }
                    },
                    "1.0.0": {
                        supported: {
                            agent: true,
                            activity: true,
                            related_activities: true,
                            related_agents: true,
                            format: true,
                            attachments: true
                        }
                    }
                };

            compatibility["0.95"] = compatibility["0.9"];
            compatibility["1.0.1"] = compatibility["1.0.0"];
            compatibility["1.0.2"] = compatibility["1.0.0"];

            if (cfg.params.hasOwnProperty("target")) {
                cfg.params.object = cfg.params.target;
            }

            //
            // check compatibility tables, either the configured parameter is in
            // the universal list or the specific version, if not then throw an
            // error which at least for .queryStatements will prevent the request
            // and potentially alert the user
            //
            for (prop in cfg.params) {
                if (cfg.params.hasOwnProperty(prop)) {
                    if (typeof universal[prop] === "undefined" && typeof compatibility[this.version].supported[prop] === "undefined") {
                        throw "Unrecognized query parameter configured: " + prop;
                    }
                }
            }

            //
            // getting here means that all parameters are valid for this version
            // to make handling the output formats easier
            //

            for (i = 0; i < jsonProps.length; i += 1) {
                if (typeof cfg.params[jsonProps[i]] !== "undefined") {
                    params[jsonProps[i]] = JSON.stringify(cfg.params[jsonProps[i]].asVersion(this.version));
                }
            }

            for (i = 0; i < idProps.length; i += 1) {
                if (typeof cfg.params[idProps[i]] !== "undefined") {
                    if (typeof cfg.params[idProps[i]].id === "undefined") {
                        params[idProps[i]] = cfg.params[idProps[i]];
                    }
                    else {
                        params[idProps[i]] = cfg.params[idProps[i]].id;
                    }
                }
            }

            for (i = 0; i < valProps.length; i += 1) {
                if (typeof cfg.params[valProps[i]] !== "undefined" && cfg.params[valProps[i]] !== null) {
                    params[valProps[i]] = cfg.params[valProps[i]];
                }
            }

            return returnCfg;
        },

        /**
        Assigns attachment content to the correct attachment to create a StatementsResult object that is sent
        to the callback of queryStatements()

        @method _assignAttachmentContent
        @private
        @param {Array} [stmts] Array of TinCan.Statement JSON objects
        @param {Object} [attachmentMap] Map of the content to place into its attachment
        @return {Array} Array of TinCan.Statement JSON objects with correctly assigned attachment content
        */
        _assignAttachmentContent: function (stmts, attachmentMap) {
            var i,
                j;

            for (i = 0; i < stmts.length; i += 1) {
                if (stmts[i].hasOwnProperty("attachments") && stmts[i].attachments !== null) {
                    for (j = 0; j < stmts[i].attachments.length; j += 1) {
                        if (attachmentMap.hasOwnProperty(stmts[i].attachments[j].sha2)) {
                            stmts[i].attachments[j].content = attachmentMap[stmts[i].attachments[j].sha2];
                        }
                    }
                }
            }
        },

        /**
        Parses the different sections of a multipart/mixed response

        @method _parseMultipart
        @private
        @param {String} [boundary] Boundary used to mark off the sections of the response
        @param {ArrayBuffer} [response] Body of the response
        @return {Array} Array of objects containing the parsed headers and body of each part
        */
        _parseMultipart: function (boundary, response) {
            /* global Uint8Array */
            var __boundary = "--" + boundary,
                byteArray,
                bodyEncodedInString,
                fullBodyEnd,
                sliceStart,
                sliceEnd,
                headerStart,
                headerEnd,
                bodyStart,
                bodyEnd,
                headers,
                body,
                parts = [],
                CRLF = 2;

            //
            // treating the reponse as a stream of bytes and assuming that headers
            // and related mime boundaries are all US-ASCII (which is a safe assumption)
            // allows us to treat the whole response as a string when looking for offsets
            // but then slice on the raw array buffer
            //
            byteArray = new Uint8Array(response);
            bodyEncodedInString = this.__uint8ToString(byteArray);

            fullBodyEnd = bodyEncodedInString.indexOf(__boundary + "--");

            sliceStart = bodyEncodedInString.indexOf(__boundary);
            while (sliceStart !== -1) {
                sliceEnd = bodyEncodedInString.indexOf(__boundary, sliceStart + __boundary.length);

                headerStart = sliceStart + __boundary.length + CRLF;
                headerEnd = bodyEncodedInString.indexOf("\r\n\r\n", sliceStart);
                bodyStart = headerEnd + CRLF + CRLF;
                bodyEnd = sliceEnd - 2;

                headers = this._parseHeaders(
                    this.__uint8ToString(
                        new Uint8Array( response.slice(headerStart, headerEnd) )
                    )
                );
                body = response.slice(bodyStart, bodyEnd);

                //
                // we know the first slice is the statement, and we know it is a string in UTF-8 (spec requirement)
                //
                if (parts.length === 0) {
                    body = TinCan.Utils.stringFromArrayBuffer(body);
                }

                parts.push(
                    {
                        headers: headers,
                        body: body
                    }
                );

                if (sliceEnd === fullBodyEnd) {
                    sliceStart = -1;
                }
                else {
                    sliceStart = sliceEnd;
                }
            }

            return parts;
        },

        //
        // implemented as a function to avoid 'RangeError: Maximum call stack size exceeded'
        // when calling .fromCharCode on the full byteArray which results in a too long
        // argument list for large arrays
        //
        __uint8ToString: function (byteArray) {
            var result = "",
                len = byteArray.byteLength,
                i;

            for (i = 0; i < len; i += 1) {
                result += String.fromCharCode(byteArray[i]);
            }
            return result;
        },

        /**
        Parses the headers of a multipart/mixed response section

        @method _parseHeaders
        @private
        @param {String} [rawHeaders] String containing all the headers
        @return {Object} Map of the headers
        */
        _parseHeaders: function (rawHeaders) {
            var headers = {},
                headerList,
                key,
                h,
                i;

            headerList = rawHeaders.split("\n");
            for (i = 0; i < headerList.length; i += 1) {
                h = headerList[i].split(":", 2);

                if (h[1] !== null) {
                    headers[h[0]] = h[1].replace(/^\s+|\s+$/g, "");

                    key = h[0];
                }
                else {
                    if (h[0].substring(0, 1) === "\t") {
                        headers[h[0]] = h[1].replace(/^\s+|\s+$/g, "");
                    }
                }
            }

            return headers;
        },

        /**
        Fetch more statements from a previous query, when used from a browser sends to the endpoint using the
        RESTful interface.  Use a callback to make the call asynchronous.

        @method moreStatements
        @param {Object} [cfg] Configuration used to query
            @param {String} [cfg.url] More URL
            @param {Function} [cfg.callback] Callback to execute on completion
                @param {String|null} cfg.callback.err Error status or null if succcess
                @param {TinCan.StatementsResult|XHR} cfg.callback.response Receives a StatementsResult argument
        @return {Object} Request result
        */
        moreStatements: function (cfg) {
            this.log("moreStatements: " + cfg.url);
            var requestCfg,
                requestResult,
                callbackWrapper,
                parsedURL,
                serverRoot;

            cfg = cfg || {};

            // to support our interface (to support IE) we need to break apart
            // the more URL query params so that the request can be made properly later
            parsedURL = TinCan.Utils.parseURL(cfg.url, { allowRelative: true });

            // Respect a more URL that is relative to either the server root
            // or endpoint (though only the former is allowed in the spec)
            serverRoot = TinCan.Utils.getServerRoot(this.endpoint);
            if (parsedURL.path.indexOf("/statements") === 0){
                parsedURL.path = this.endpoint.replace(serverRoot, "") + parsedURL.path;
                this.log("converting non-standard more URL to " + parsedURL.path);
            }

            // The more relative URL might not start with a slash, add it if not
            if (parsedURL.path.indexOf("/") !== 0) {
                parsedURL.path = "/" + parsedURL.path;
            }

            requestCfg = {
                method: "GET",
                // For arbitrary more URLs to work, we need to make the URL absolute here
                url: serverRoot + parsedURL.path,
                params: parsedURL.params
            };
            if (typeof cfg.callback !== "undefined") {
                callbackWrapper = function (err, xhr) {
                    var result = xhr;

                    if (err === null) {
                        result = TinCan.StatementsResult.fromJSON(xhr.responseText);
                    }

                    cfg.callback(err, result);
                };
                requestCfg.callback = callbackWrapper;
            }

            requestResult = this.sendRequest(requestCfg);
            requestResult.config = requestCfg;

            if (! callbackWrapper) {
                requestResult.statementsResult = null;
                if (requestResult.err === null) {
                    requestResult.statementsResult = TinCan.StatementsResult.fromJSON(requestResult.xhr.responseText);
                }
            }

            return requestResult;
        },

        /**
        Retrieve a state value, when used from a browser sends to the endpoint using the RESTful interface.

        @method retrieveState
        @param {String} key Key of state to retrieve
        @param {Object} cfg Configuration options
            @param {TinCan.Activity} cfg.activity Activity in document identifier
            @param {TinCan.Agent} cfg.agent Agent in document identifier
            @param {String} [cfg.registration] Registration
            @param {Function} [cfg.callback] Callback to execute on completion
                @param {Object|Null} cfg.callback.error
                @param {TinCan.State|null} cfg.callback.result null if state is 404
            @param {Object} [cfg.requestHeaders] Object containing additional headers to add to request
        @return {TinCan.State|Object} TinCan.State retrieved when synchronous, or result from sendRequest
        */
        retrieveState: function (key, cfg) {
            this.log("retrieveState");
            var requestParams = {},
                requestCfg = {},
                requestResult,
                callbackWrapper,
                requestHeaders,
                self = this;

            requestHeaders = cfg.requestHeaders || {};

            requestParams = {
                stateId: key,
                activityId: cfg.activity.id
            };
            if (this.version === "0.9") {
                requestParams.actor = JSON.stringify(cfg.agent.asVersion(this.version));
            }
            else {
                requestParams.agent = JSON.stringify(cfg.agent.asVersion(this.version));
            }
            if ((typeof cfg.registration !== "undefined") && (cfg.registration !== null)) {
                if (this.version === "0.9") {
                    requestParams.registrationId = cfg.registration;
                }
                else {
                    requestParams.registration = cfg.registration;
                }
            }

            requestCfg = {
                url: "activities/state",
                method: "GET",
                params: requestParams,
                ignore404: true,
                headers: requestHeaders
            };

            if (typeof cfg.callback !== "undefined") {
                callbackWrapper = function (err, xhr) {
                    var result = xhr;

                    if (err === null) {
                        if (xhr.status === 404) {
                            result = null;
                        }
                        else {
                            result = new TinCan.State(
                                {
                                    id: key,
                                    contents: xhr.responseText
                                }
                            );
                            if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("ETag") !== null && xhr.getResponseHeader("ETag") !== "") {
                                result.etag = xhr.getResponseHeader("ETag");
                            }
                            else {
                                //
                                // either XHR didn't have getResponseHeader (probably cause it is an IE
                                // XDomainRequest object which doesn't) or not populated by LRS so create
                                // the hash ourselves
                                //
                                // the LRS is responsible for quoting the Etag value so we need to mimic
                                // that behavior here as well
                                //
                                result.etag = "\"" + TinCan.Utils.getSHA1String(xhr.responseText) + "\"";
                            }

                            if (typeof xhr.contentType !== "undefined") {
                                // most likely an XDomainRequest which has .contentType,
                                // for the ones that it supports
                                result.contentType = xhr.contentType;
                            }
                            else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") {
                                result.contentType = xhr.getResponseHeader("Content-Type");
                            }

                            if (TinCan.Utils.isApplicationJSON(result.contentType)) {
                                try {
                                    result.contents = JSON.parse(result.contents);
                                } catch (ex) {
                                    self.log("retrieveState - failed to deserialize JSON: " + ex);
                                }
                            }
                        }
                    }

                    cfg.callback(err, result);
                };
                requestCfg.callback = callbackWrapper;
            }

            requestResult = this.sendRequest(requestCfg);
            if (! callbackWrapper) {
                requestResult.state = null;
                if (requestResult.err === null && requestResult.xhr.status !== 404) {
                    requestResult.state = new TinCan.State(
                        {
                            id: key,
                            contents: requestResult.xhr.responseText
                        }
                    );
                    if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("ETag") !== null && requestResult.xhr.getResponseHeader("ETag") !== "") {
                        requestResult.state.etag = requestResult.xhr.getResponseHeader("ETag");
                    }
                    else {
                        //
                        // either XHR didn't have getResponseHeader (probably cause it is an IE
                        // XDomainRequest object which doesn't) or not populated by LRS so create
                        // the hash ourselves
                        //
                        // the LRS is responsible for quoting the Etag value so we need to mimic
                        // that behavior here as well
                        //
                        requestResult.state.etag = "\"" + TinCan.Utils.getSHA1String(requestResult.xhr.responseText) + "\"";
                    }
                    if (typeof requestResult.xhr.contentType !== "undefined") {
                        // most likely an XDomainRequest which has .contentType
                        // for the ones that it supports
                        requestResult.state.contentType = requestResult.xhr.contentType;
                    }
                    else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") {
                        requestResult.state.contentType = requestResult.xhr.getResponseHeader("Content-Type");
                    }
                    if (TinCan.Utils.isApplicationJSON(requestResult.state.contentType)) {
                        try {
                            requestResult.state.contents = JSON.parse(requestResult.state.contents);
                        } catch (ex) {
                            this.log("retrieveState - failed to deserialize JSON: " + ex);
                        }
                    }
                }
            }

            return requestResult;
        },

        /**
        Retrieve the list of IDs for a state, when used from a browser sends to the endpoint using the RESTful interface.

        @method retrieveStateIds
        @param {Object} cfg Configuration options
            @param {TinCan.Activity} cfg.activity Activity in document identifier
            @param {TinCan.Agent} cfg.agent Agent in document identifier
            @param {String} [cfg.registration] Registration
            @param {Function} [cfg.callback] Callback to execute on completion
            @param {String} [cfg.since] Match activity profiles saved since given timestamp
            @param {Object} [cfg.requestHeaders] Optional object containing additional headers to add to request
        @return {Object} requestResult Request result
        */
        retrieveStateIds: function (cfg) {
            this.log("retrieveStateIds");
            var requestParams = {},
                requestCfg,
                requestHeaders,
                requestResult,
                callbackWrapper;

            cfg = cfg || {};
            requestHeaders = cfg.requestHeaders || {};

            requestParams.activityId = cfg.activity.id;
            if (this.version === "0.9") {
                requestParams.actor = JSON.stringify(cfg.agent.asVersion(this.version));
            }
            else {
                requestParams.agent = JSON.stringify(cfg.agent.asVersion(this.version));
            }
            if ((typeof cfg.registration !== "undefined") && (cfg.registration !== null)) {
                if (this.version === "0.9") {
                    requestParams.registrationId = cfg.registration;
                }
                else {
                    requestParams.registration = cfg.registration;
                }
            }

            requestCfg = {
                url: "activities/state",
                method: "GET",
                params: requestParams,
                headers: requestHeaders,
                ignore404: true
            };
            if (typeof cfg.callback !== "undefined") {
                callbackWrapper = function (err, xhr) {
                    var result = xhr;

                    if (err !== null) {
                        cfg.callback(err, result);
                        return;
                    }

                    if (xhr.status === 404) {
                        result = [];
                    }
                    else {
                        try {
                            result = JSON.parse(xhr.responseText);
                        }
                        catch (ex) {
                            err = "Response JSON parse error: " + ex;
                        }
                    }

                    cfg.callback(err, result);
                };
                requestCfg.callback = callbackWrapper;
            }
            if (typeof cfg.since !== "undefined") {
                requestCfg.params.since = cfg.since;
            }

            requestResult = this.sendRequest(requestCfg);
            if (! callbackWrapper) {
                requestResult.profileIds = null;
                if (requestResult.err !== null) {
                    return requestResult;
                }

                if (requestResult.xhr.status === 404) {
                    requestResult.profileIds = [];
                }
                else {
                    try {
                        requestResult.profileIds = JSON.parse(requestResult.xhr.responseText);
                    }
                    catch (ex) {
                        requestResult.err = "retrieveStateIds - JSON parse error: " + ex;
                    }
                }
            }
            return requestResult;
        },

        /**
        Save a state value, when used from a browser sends to the endpoint using the RESTful interface.

        @method saveState
        @param {String} key Key of state to save
        @param val Value to be stored
        @param {Object} cfg Configuration options
            @param {TinCan.Activity} cfg.activity Activity in document identifier
            @param {TinCan.Agent} cfg.agent Agent in document identifier
            @param {String} [cfg.registration] Registration
            @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing state
            @param {String} [cfg.contentType] Content-Type to specify in headers (defaults to 'application/octet-stream')
            @param {String} [cfg.method] Method to use. Default: PUT
            @param {Function} [cfg.callback] Callback to execute on completion
            @param {Object} [cfg.requestHeaders] Optional object containing additional headers to add to request
        */
        saveState: function (key, val, cfg) {
            this.log("saveState");
            var requestParams,
                requestCfg,
                requestHeaders;

            requestHeaders = cfg.requestHeaders || {};

            if (typeof cfg.contentType === "undefined") {
                cfg.contentType = "application/octet-stream";
            }
            requestHeaders["Content-Type"] = cfg.contentType;

            if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) {
                val = JSON.stringify(val);
            }

            if (typeof cfg.method === "undefined" || cfg.method !== "POST") {
                cfg.method = "PUT";
            }

            requestParams = {
                stateId: key,
                activityId: cfg.activity.id
            };
            if (this.version === "0.9") {
                requestParams.actor = JSON.stringify(cfg.agent.asVersion(this.version));
            }
            else {
                requestParams.agent = JSON.stringify(cfg.agent.asVersion(this.version));
            }
            if ((typeof cfg.registration !== "undefined") && (cfg.registration !== null)) {
                if (this.version === "0.9") {
                    requestParams.registrationId = cfg.registration;
                }
                else {
                    requestParams.registration = cfg.registration;
                }
            }

            requestCfg = {
                url: "activities/state",
                method: cfg.method,
                params: requestParams,
                data: val,
                headers: requestHeaders
            };

            if (typeof cfg.callback !== "undefined") {
                requestCfg.callback = cfg.callback;
            }
            if (typeof cfg.lastSHA1 !== "undefined" && cfg.lastSHA1 !== null) {
                requestCfg.headers["If-Match"] = cfg.lastSHA1;
            }

            return this.sendRequest(requestCfg);
        },

        /**
        Drop a state value or all of the state, when used from a browser sends to the endpoint using the RESTful interface.

        @method dropState
        @param {String|null} key Key of state to delete, or null for all
        @param {Object} cfg Configuration options
            @param {TinCan.Activity} cfg.activity Activity in document identifier
            @param {TinCan.Agent} cfg.agent Agent in document identifier
            @param {String} [cfg.registration] Registration
            @param {Function} [cfg.callback] Callback to execute on completion
            @param {Object} [cfg.requestHeaders] Optional object containing additional headers to add to request
        */
        dropState: function (key, cfg) {
            this.log("dropState");
            var requestParams,
                requestCfg,
                requestHeaders;

            requestHeaders = cfg.requestHeaders || {};

            requestParams = {
                activityId: cfg.activity.id
            };
            if (this.version === "0.9") {
                requestParams.actor = JSON.stringify(cfg.agent.asVersion(this.version));
            }
            else {
                requestParams.agent = JSON.stringify(cfg.agent.asVersion(this.version));
            }
            if (key !== null) {
                requestParams.stateId = key;
            }
            if ((typeof cfg.registration !== "undefined") && (cfg.registration !== null)) {
                if (this.version === "0.9") {
                    requestParams.registrationId = cfg.registration;
                }
                else {
                    requestParams.registration = cfg.registration;
                }
            }

            requestCfg = {
                url: "activities/state",
                method: "DELETE",
                params: requestParams,
                headers: requestHeaders
            };

            if (typeof cfg.callback !== "undefined") {
                requestCfg.callback = cfg.callback;
            }

            return this.sendRequest(requestCfg);
        },

        /**
        Retrieve an activity, when used from a browser sends to the endpoint using the RESTful interface.

        @method retrieveActivity
        @param {String} activityId id of the Activity to retrieve
        @param {Object} cfg Configuration options
            @param {Function} [cfg.callback] Callback to execute on completion
            @param {Object} [cfg.requestHeaders] Optional object containing additional headers to add to request
        @return {Object} Value retrieved
        */
        retrieveActivity: function (activityId, cfg) {
            this.log("retrieveActivity");
            var requestCfg = {},
                requestResult,
                callbackWrapper,
                requestHeaders;

            requestHeaders = cfg.requestHeaders || {};

            requestCfg = {
                url: "activities",
                method: "GET",
                params: {
                    activityId: activityId
                },
                ignore404: true,
                headers: requestHeaders
            };

            if (typeof cfg.callback !== "undefined") {
                callbackWrapper = function (err, xhr) {
                    var result = xhr;

                    if (err === null) {
                        //
                        // a 404 really shouldn't happen because the LRS can dynamically
                        // build the response based on what has been passed to it, but
                        // don't have the client fail in the condition that it does, because
                        // we can do the same thing
                        //
                        if (xhr.status === 404) {
                            result = new TinCan.Activity(
                                {
                                    id: activityId
                                }
                            );
                        }
                        else {
                            result = TinCan.Activity.fromJSON(xhr.responseText);
                        }
                    }

                    cfg.callback(err, result);
                };
                requestCfg.callback = callbackWrapper;
            }

            requestResult = this.sendRequest(requestCfg);
            if (! callbackWrapper) {
                requestResult.activity = null;
                if (requestResult.err === null) {
                    if (requestResult.xhr.status === 404) {
                        requestResult.activity = new TinCan.Activity(
                            {
                                id: activityId
                            }
                        );
                    }
                    else {
                        requestResult.activity = TinCan.Activity.fromJSON(requestResult.xhr.responseText);
                    }
                }
            }

            return requestResult;
        },

        /**
        Retrieve an activity profile value, when used from a browser sends to the endpoint using the RESTful interface.

        @method retrieveActivityProfile
        @param {String} key Key of activity profile to retrieve
        @param {Object} cfg Configuration options
            @param {TinCan.Activity} cfg.activity Activity in document identifier
            @param {Function} [cfg.callback] Callback to execute on completion
            @param {Object} [cfg.requestHeaders] Optional object containing additional headers to add to request
        @return {Object} Value retrieved
        */
        retrieveActivityProfile: function (key, cfg) {
            this.log("retrieveActivityProfile");
            var requestCfg = {},
                requestResult,
                callbackWrapper,
                requestHeaders,
                self = this;

            requestHeaders = cfg.requestHeaders || {};

            requestCfg = {
                url: "activities/profile",
                method: "GET",
                params: {
                    profileId: key,
                    activityId: cfg.activity.id
                },
                ignore404: true,
                headers: requestHeaders
            };

            if (typeof cfg.callback !== "undefined") {
                callbackWrapper = function (err, xhr) {
                    var result = xhr;

                    if (err === null) {
                        if (xhr.status === 404) {
                            result = null;
                        }
                        else {
                            result = new TinCan.ActivityProfile(
                                {
                                    id: key,
                                    activity: cfg.activity,
                                    contents: xhr.responseText
                                }
                            );
                            if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("ETag") !== null && xhr.getResponseHeader("ETag") !== "") {
                                result.etag = xhr.getResponseHeader("ETag");
                            }
                            else {
                                //
                                // either XHR didn't have getResponseHeader (probably cause it is an IE
                                // XDomainRequest object which doesn't) or not populated by LRS so create
                                // the hash ourselves
                                //
                                // the LRS is responsible for quoting the Etag value so we need to mimic
                                // that behavior here as well
                                //
                                result.etag = "\"" + TinCan.Utils.getSHA1String(xhr.responseText) + "\"";
                            }
                            if (typeof xhr.contentType !== "undefined") {
                                // most likely an XDomainRequest which has .contentType
                                // for the ones that it supports
                                result.contentType = xhr.contentType;
                            }
                            else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") {
                                result.contentType = xhr.getResponseHeader("Content-Type");
                            }
                            if (TinCan.Utils.isApplicationJSON(result.contentType)) {
                                try {
                                    result.contents = JSON.parse(result.contents);
                                } catch (ex) {
                                    self.log("retrieveActivityProfile - failed to deserialize JSON: " + ex);
                                }
                            }
                        }
                    }

                    cfg.callback(err, result);
                };
                requestCfg.callback = callbackWrapper;
            }

            requestResult = this.sendRequest(requestCfg);
            if (! callbackWrapper) {
                requestResult.profile = null;
                if (requestResult.err === null && requestResult.xhr.status !== 404) {
                    requestResult.profile = new TinCan.ActivityProfile(
                        {
                            id: key,
                            activity: cfg.activity,
                            contents: requestResult.xhr.responseText
                        }
                    );
                    if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("ETag") !== null && requestResult.xhr.getResponseHeader("ETag") !== "") {
                        requestResult.profile.etag = requestResult.xhr.getResponseHeader("ETag");
                    }
                    else {
                        //
                        // either XHR didn't have getResponseHeader (probably cause it is an IE
                        // XDomainRequest object which doesn't) or not populated by LRS so create
                        // the hash ourselves
                        //
                        // the LRS is responsible for quoting the Etag value so we need to mimic
                        // that behavior here as well
                        //
                        requestResult.profile.etag = "\"" + TinCan.Utils.getSHA1String(requestResult.xhr.responseText) + "\"";
                    }
                    if (typeof requestResult.xhr.contentType !== "undefined") {
                        // most likely an XDomainRequest which has .contentType
                        // for the ones that it supports
                        requestResult.profile.contentType = requestResult.xhr.contentType;
                    }
                    else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") {
                        requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type");
                    }
                    if (TinCan.Utils.isApplicationJSON(requestResult.profile.contentType)) {
                        try {
                            requestResult.profile.contents = JSON.parse(requestResult.profile.contents);
                        } catch (ex) {
                            this.log("retrieveActivityProfile - failed to deserialize JSON: " + ex);
                        }
                    }
                }
            }

            return requestResult;
        },

        /**
        Retrieve the list of IDs for an activity profile, when used from a browser sends to the endpoint using the RESTful interface.

        @method retrieveActivityProfileIds
        @param {Object} cfg Configuration options
            @param {TinCan.Activity} cfg.activity Activity in document identifier
            @param {Function} [cfg.callback] Callback to execute on completion
            @param {String} [cfg.since] Match activity profiles saved since given timestamp
            @param {Object} [cfg.requestHeaders] Optional object containing additional headers to add to request
        @return {Array} List of ids for this Activity profile
        */
        retrieveActivityProfileIds: function (cfg) {
            this.log("retrieveActivityProfileIds");
            var requestCfg,
                requestHeaders,
                requestResult,
                callbackWrapper;

            cfg = cfg || {};
            requestHeaders = cfg.requestHeaders || {};

            requestCfg = {
                url: "activities/profile",
                method: "GET",
                params: {
                    activityId: cfg.activity.id
                },
                headers: requestHeaders,
                ignore404: true
            };
            if (typeof cfg.callback !== "undefined") {
                callbackWrapper = function (err, xhr) {
                    var result = xhr;

                    if (err !== null) {
                        cfg.callback(err, result);
                        return;
                    }

                    if (xhr.status === 404) {
                        result = [];
                    }
                    else {
                        try {
                            result = JSON.parse(xhr.responseText);
                        }
                        catch (ex) {
                            err = "Response JSON parse error: " + ex;
                        }
                    }

                    cfg.callback(err, result);
                };
                requestCfg.callback = callbackWrapper;
            }
            if (typeof cfg.since !== "undefined") {
                requestCfg.params.since = cfg.since;
            }

            requestResult = this.sendRequest(requestCfg);
            if (! callbackWrapper) {
                requestResult.profileIds = null;
                if (requestResult.err !== null) {
                    return requestResult;
                }

                if (requestResult.xhr.status === 404) {
                    requestResult.profileIds = [];
                }
                else {
                    try {
                        requestResult.profileIds = JSON.parse(requestResult.xhr.responseText);
                    }
                    catch (ex) {
                        requestResult.err = "retrieveActivityProfileIds - JSON parse error: " + ex;
                    }
                }
            }
            return requestResult;
        },

        /**
        Save an activity profile value, when used from a browser sends to the endpoint using the RESTful interface.

        @method saveActivityProfile
        @param {String} key Key of activity profile to retrieve
        @param val Value to be stored
        @param {Object} cfg Configuration options
            @param {TinCan.Activity} cfg.activity Activity in document identifier
            @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile
            @param {String} [cfg.contentType] Content-Type to specify in headers (defaults to 'application/octet-stream')
            @param {String} [cfg.method] Method to use. Default: PUT
            @param {Function} [cfg.callback] Callback to execute on completion
            @param {Object} [cfg.requestHeaders] Optional object containing additional headers to add to request
        */
        saveActivityProfile: function (key, val, cfg) {
            this.log("saveActivityProfile");
            var requestCfg,
                requestHeaders;

            requestHeaders = cfg.requestHeaders || {};

            if (typeof cfg.contentType === "undefined") {
                cfg.contentType = "application/octet-stream";
            }
            requestHeaders["Content-Type"] = cfg.contentType;

            if (typeof cfg.method === "undefined" || cfg.method !== "POST") {
                cfg.method = "PUT";
            }

            if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) {
                val = JSON.stringify(val);
            }

            requestCfg = {
                url: "activities/profile",
                method: cfg.method,
                params: {
                    profileId: key,
                    activityId: cfg.activity.id
                },
                data: val,
                headers: requestHeaders
            };

            if (typeof cfg.callback !== "undefined") {
                requestCfg.callback = cfg.callback;
            }
            if (typeof cfg.lastSHA1 !== "undefined" && cfg.lastSHA1 !== null) {
                requestCfg.headers["If-Match"] = cfg.lastSHA1;
            }
            else {
                requestCfg.headers["If-None-Match"] = "*";
            }

            return this.sendRequest(requestCfg);
        },

        /**
        Drop an activity profile value, when used from a browser sends to the endpoint using the RESTful interface. Full activity profile
        delete is not supported by the spec.

        @method dropActivityProfile
        @param {String|null} key Key of activity profile to delete
        @param {Object} cfg Configuration options
            @param {TinCan.Activity} cfg.activity Activity in document identifier
            @param {Function} [cfg.callback] Callback to execute on completion
            @param {Object} [cfg.requestHeaders] Optional object containing additional headers to add to request
        */
        dropActivityProfile: function (key, cfg) {
            this.log("dropActivityProfile");
            var requestParams,
                requestCfg,
                requestHeaders;

            requestHeaders = cfg.requestHeaders || {};

            requestParams = {
                profileId: key,
                activityId: cfg.activity.id
            };

            requestCfg = {
                url: "activities/profile",
                method: "DELETE",
                params: requestParams,
                headers: requestHeaders
            };

            if (typeof cfg.callback !== "undefined") {
                requestCfg.callback = cfg.callback;
            }

            return this.sendRequest(requestCfg);
        },

        /**
        Retrieve an agent profile value, when used from a browser sends to the endpoint using the RESTful interface.

        @method retrieveAgentProfile
        @param {String} key Key of agent profile to retrieve
        @param {Object} cfg Configuration options
            @param {TinCan.Agent} cfg.agent Agent in document identifier
            @param {Function} [cfg.callback] Callback to execute on completion
            @param {Object} [cfg.requestHeaders] Optional object containing additional headers to add to request
        @return {Object} Value retrieved
        */
        retrieveAgentProfile: function (key, cfg) {
            this.log("retrieveAgentProfile");
            var requestCfg = {},
                requestResult,
                callbackWrapper,
                requestHeaders,
                self = this;

            requestHeaders = cfg.requestHeaders || {};

            requestCfg = {
                method: "GET",
                params: {
                    profileId: key
                },
                ignore404: true,
                headers: requestHeaders
            };

            if (this.version === "0.9") {
                requestCfg.url = "actors/profile";
                requestCfg.params.actor = JSON.stringify(cfg.agent.asVersion(this.version));
            }
            else {
                requestCfg.url = "agents/profile";
                requestCfg.params.agent = JSON.stringify(cfg.agent.asVersion(this.version));
            }
            if (typeof cfg.callback !== "undefined") {
                callbackWrapper = function (err, xhr) {
                    var result = xhr;

                    if (err === null) {
                        if (xhr.status === 404) {
                            result = null;
                        }
                        else {
                            result = new TinCan.AgentProfile(
                                {
                                    id: key,
                                    agent: cfg.agent,
                                    contents: xhr.responseText
                                }
                            );
                            if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("ETag") !== null && xhr.getResponseHeader("ETag") !== "") {
                                result.etag = xhr.getResponseHeader("ETag");
                            }
                            else {
                                //
                                // either XHR didn't have getResponseHeader (probably cause it is an IE
                                // XDomainRequest object which doesn't) or not populated by LRS so create
                                // the hash ourselves
                                //
                                // the LRS is responsible for quoting the Etag value so we need to mimic
                                // that behavior here as well
                                //
                                result.etag = "\"" + TinCan.Utils.getSHA1String(xhr.responseText) + "\"";
                            }
                            if (typeof xhr.contentType !== "undefined") {
                                // most likely an XDomainRequest which has .contentType
                                // for the ones that it supports
                                result.contentType = xhr.contentType;
                            }
                            else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") {
                                result.contentType = xhr.getResponseHeader("Content-Type");
                            }
                            if (TinCan.Utils.isApplicationJSON(result.contentType)) {
                                try {
                                    result.contents = JSON.parse(result.contents);
                                } catch (ex) {
                                    self.log("retrieveAgentProfile - failed to deserialize JSON: " + ex);
                                }
                            }
                        }
                    }

                    cfg.callback(err, result);
                };
                requestCfg.callback = callbackWrapper;
            }

            requestResult = this.sendRequest(requestCfg);
            if (! callbackWrapper) {
                requestResult.profile = null;
                if (requestResult.err === null && requestResult.xhr.status !== 404) {
                    requestResult.profile = new TinCan.AgentProfile(
                        {
                            id: key,
                            agent: cfg.agent,
                            contents: requestResult.xhr.responseText
                        }
                    );
                    if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("ETag") !== null && requestResult.xhr.getResponseHeader("ETag") !== "") {
                        requestResult.profile.etag = requestResult.xhr.getResponseHeader("ETag");
                    }
                    else {
                        //
                        // either XHR didn't have getResponseHeader (probably cause it is an IE
                        // XDomainRequest object which doesn't) or not populated by LRS so create
                        // the hash ourselves
                        //
                        // the LRS is responsible for quoting the Etag value so we need to mimic
                        // that behavior here as well
                        //
                        requestResult.profile.etag = "\"" + TinCan.Utils.getSHA1String(requestResult.xhr.responseText) + "\"";
                    }
                    if (typeof requestResult.xhr.contentType !== "undefined") {
                        // most likely an XDomainRequest which has .contentType
                        // for the ones that it supports
                        requestResult.profile.contentType = requestResult.xhr.contentType;
                    }
                    else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") {
                        requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type");
                    }
                    if (TinCan.Utils.isApplicationJSON(requestResult.profile.contentType)) {
                        try {
                            requestResult.profile.contents = JSON.parse(requestResult.profile.contents);
                        } catch (ex) {
                            this.log("retrieveAgentProfile - failed to deserialize JSON: " + ex);
                        }
                    }
                }
            }

            return requestResult;
        },

        /**
        Retrieve the list of profileIds for an agent profile, when used from a browser sends to the endpoint using the RESTful interface.

        @method retrieveAgentProfileIds
        @param {Object} cfg Configuration options
            @param {TinCan.Agent} cfg.agent Agent in document identifier
            @param {Function} [cfg.callback] Callback to execute on completion
            @param {String} [cfg.since] Match activity profiles saved since given timestamp
            @param {Object} [cfg.requestHeaders] Optional object containing additional headers to add to request
        @return {Array} List of profileIds for this Agent

        */
        retrieveAgentProfileIds: function (cfg) {
            this.log("retrieveAgentProfileIds");
            var requestParams = {},
                requestCfg,
                requestHeaders,
                requestResult,
                callbackWrapper;

            cfg = cfg || {};
            requestHeaders = cfg.requestHeaders || {};

            requestCfg = {
                method: "GET",
                params: requestParams,
                headers: requestHeaders,
                ignore404: true
            };

            if (this.version === "0.9") {
                requestCfg.url = "actors/profile";
                requestParams.actor = JSON.stringify(cfg.agent.asVersion(this.version));
            }
            else {
                requestCfg.url = "agents/profile";
                requestParams.agent = JSON.stringify(cfg.agent.asVersion(this.version));
            }
            if (typeof cfg.callback !== "undefined") {
                callbackWrapper = function (err, xhr) {
                    var result = xhr;

                    if (err !== null) {
                        cfg.callback(err, result);
                        return;
                    }

                    if (xhr.status === 404) {
                        result = [];
                    }
                    else {
                        try {
                            result = JSON.parse(xhr.responseText);
                        }
                        catch (ex) {
                            err = "Response JSON parse error: " + ex;
                        }
                    }

                    cfg.callback(err, result);
                };
                requestCfg.callback = callbackWrapper;
            }
            if (typeof cfg.since !== "undefined") {
                requestCfg.params.since = cfg.since;
            }

            requestResult = this.sendRequest(requestCfg);
            if (! callbackWrapper) {
                requestResult.profileIds = null;
                if (requestResult.err !== null) {
                    return requestResult;
                }

                if (requestResult.xhr.status === 404) {
                    requestResult.profileIds = [];
                }
                else {
                    try {
                        requestResult.profileIds = JSON.parse(requestResult.xhr.responseText);
                    }
                    catch (ex) {
                        requestResult.err = "retrieveAgentProfileIds - JSON parse error: " + ex;
                    }
                }
            }
            return requestResult;
        },

        /**
        Save an agent profile value, when used from a browser sends to the endpoint using the RESTful interface.

        @method saveAgentProfile
        @param {String} key Key of agent profile to retrieve
        @param val Value to be stored
        @param {Object} cfg Configuration options
            @param {TinCan.Agent} cfg.agent Agent in document identifier
            @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile
            @param {String} [cfg.contentType] Content-Type to specify in headers (defaults to 'application/octet-stream')
            @param {String} [cfg.method] Method to use. Default: PUT
            @param {Function} [cfg.callback] Callback to execute on completion
            @param {Object} [cfg.requestHeaders] Optional object containing additional headers to add to request
        */
        saveAgentProfile: function (key, val, cfg) {
            this.log("saveAgentProfile");
            var requestCfg,
                requestHeaders;

            requestHeaders = cfg.requestHeaders || {};

            if (typeof cfg.contentType === "undefined") {
                cfg.contentType = "application/octet-stream";
            }
            requestHeaders["Content-Type"] = cfg.contentType;

            if (typeof cfg.method === "undefined" || cfg.method !== "POST") {
                cfg.method = "PUT";
            }

            if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) {
                val = JSON.stringify(val);
            }

            requestCfg = {
                method: cfg.method,
                params: {
                    profileId: key
                },
                data: val,
                headers: requestHeaders
            };

            if (this.version === "0.9") {
                requestCfg.url = "actors/profile";
                requestCfg.params.actor = JSON.stringify(cfg.agent.asVersion(this.version));
            }
            else {
                requestCfg.url = "agents/profile";
                requestCfg.params.agent = JSON.stringify(cfg.agent.asVersion(this.version));
            }
            if (typeof cfg.callback !== "undefined") {
                requestCfg.callback = cfg.callback;
            }
            if (typeof cfg.lastSHA1 !== "undefined" && cfg.lastSHA1 !== null) {
                requestCfg.headers["If-Match"] = cfg.lastSHA1;
            }
            else {
                requestCfg.headers["If-None-Match"] = "*";
            }

            return this.sendRequest(requestCfg);
        },

        /**
        Drop an agent profile value, when used from a browser sends to the endpoint using the RESTful interface. Full agent profile
        delete is not supported by the spec.

        @method dropAgentProfile
        @param {String|null} key Key of agent profile to delete
        @param {Object} cfg Configuration options
            @param {TinCan.Agent} cfg.agent Agent in document identifier
            @param {Function} [cfg.callback] Callback to execute on completion
            @param {Object} [cfg.requestHeaders] Optional object containing additional headers to add to request
        */
        dropAgentProfile: function (key, cfg) {
            this.log("dropAgentProfile");
            var requestParams,
                requestCfg,
                requestHeaders;

            requestHeaders = cfg.requestHeaders || {};

            requestParams = {
                profileId: key
            };
            requestCfg = {
                method: "DELETE",
                params: requestParams,
                headers: requestHeaders
            };

            if (this.version === "0.9") {
                requestCfg.url = "actors/profile";
                requestParams.actor = JSON.stringify(cfg.agent.asVersion(this.version));
            }
            else {
                requestCfg.url = "agents/profile";
                requestParams.agent = JSON.stringify(cfg.agent.asVersion(this.version));
            }
            if (typeof cfg.callback !== "undefined") {
                requestCfg.callback = cfg.callback;
            }

            return this.sendRequest(requestCfg);
        }
    };

    /**
    Allows client code to determine whether their environment supports synchronous xhr handling
    @static this is a static property, set by the environment
    */
    LRS.syncEnabled = null;
}());

/*
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.AgentAccount
**/
(function () {
    "use strict";

    /**
    @class TinCan.AgentAccount
    @constructor
    */
    var AgentAccount = TinCan.AgentAccount = function (cfg) {
        this.log("constructor");

        /**
        @property homePage
        @type String
        */
        this.homePage = null;

        /**
        @property name
        @type String
        */
        this.name = null;

        this.init(cfg);
    };
    AgentAccount.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "AgentAccount",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");
            var i,
                directProps = [
                    "name",
                    "homePage"
                ];

            cfg = cfg || {};

            // handle .9 name changes
            if (typeof cfg.accountServiceHomePage !== "undefined") {
                cfg.homePage = cfg.accountServiceHomePage;
            }
            if (typeof cfg.accountName !== "undefined") {
                cfg.name = cfg.accountName;
            }

            for (i = 0; i < directProps.length; i += 1) {
                if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) {
                    this[directProps[i]] = cfg[directProps[i]];
                }
            }
        },

        toString: function () {
            this.log("toString");
            var result = "";

            if (this.name !== null || this.homePage !== null) {
                result += this.name !== null ? this.name : "-";
                result += ":";
                result += this.homePage !== null ? this.homePage : "-";
            }
            else {
                result = "AgentAccount: unidentified";
            }

            return result;
        },

        /**
        @method asVersion
        @param {String} [version] Version to return (defaults to newest supported)
        */
        asVersion: function (version) {
            this.log("asVersion: " + version);
            var result = {};

            version = version || TinCan.versions()[0];

            if (version === "0.9") {
                result.accountName = this.name;
                result.accountServiceHomePage = this.homePage;
            } else {
                result.name = this.name;
                result.homePage = this.homePage;
            }

            return result;
        }
    };

    /**
    @method fromJSON
    @return {Object} AgentAccount
    @static
    */
    AgentAccount.fromJSON = function (acctJSON) {
        AgentAccount.prototype.log("fromJSON");
        var _acct = JSON.parse(acctJSON);

        return new AgentAccount(_acct);
    };
}());

/*
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.Agent
**/
(function () {
    "use strict";

    /**
    @class TinCan.Agent
    @constructor
    */
    var Agent = TinCan.Agent = function (cfg) {
        this.log("constructor");

        /**
        @property name
        @type String
        */
        this.name = null;

        /**
        @property mbox
        @type String
        */
        this.mbox = null;

        /**
        @property mbox_sha1sum
        @type String
        */
        this.mbox_sha1sum = null;

        /**
        @property openid
        @type String
        */
        this.openid = null;

        /**
        @property account
        @type TinCan.AgentAccount
        */
        this.account = null;

        /**
        @property degraded
        @type Boolean
        @default false
        */
        this.degraded = false;

        this.init(cfg);
    };
    Agent.prototype = {
        /**
        @property objectType
        @type String
        @default Agent
        */
        objectType: "Agent",

        /**
        @property LOG_SRC
        */
        LOG_SRC: "Agent",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");
            var i,
                directProps = [
                    "name",
                    "mbox",
                    "mbox_sha1sum",
                    "openid"
                ],
                val
            ;

            cfg = cfg || {};

            // handle .9 split names and array properties into single interface
            if (typeof cfg.lastName !== "undefined" || typeof cfg.firstName !== "undefined") {
                cfg.name = "";
                if (typeof cfg.firstName !== "undefined" && cfg.firstName.length > 0) {
                    cfg.name = cfg.firstName[0];
                    if (cfg.firstName.length > 1) {
                        this.degraded = true;
                    }
                }

                if (cfg.name !== "") {
                    cfg.name += " ";
                }

                if (typeof cfg.lastName !== "undefined" && cfg.lastName.length > 0) {
                    cfg.name += cfg.lastName[0];
                    if (cfg.lastName.length > 1) {
                        this.degraded = true;
                    }
                }
            } else if (typeof cfg.familyName !== "undefined" || typeof cfg.givenName !== "undefined") {
                cfg.name = "";
                if (typeof cfg.givenName !== "undefined" && cfg.givenName.length > 0) {
                    cfg.name = cfg.givenName[0];
                    if (cfg.givenName.length > 1) {
                        this.degraded = true;
                    }
                }

                if (cfg.name !== "") {
                    cfg.name += " ";
                }

                if (typeof cfg.familyName !== "undefined" && cfg.familyName.length > 0) {
                    cfg.name += cfg.familyName[0];
                    if (cfg.familyName.length > 1) {
                        this.degraded = true;
                    }
                }
            }

            if (typeof cfg.name === "object" && cfg.name !== null) {
                if (cfg.name.length > 1) {
                    this.degraded = true;
                }
                cfg.name = cfg.name[0];
            }
            if (typeof cfg.mbox === "object" && cfg.mbox !== null) {
                if (cfg.mbox.length > 1) {
                    this.degraded = true;
                }
                cfg.mbox = cfg.mbox[0];
            }
            if (typeof cfg.mbox_sha1sum === "object" && cfg.mbox_sha1sum !== null) {
                if (cfg.mbox_sha1sum.length > 1) {
                    this.degraded = true;
                }
                cfg.mbox_sha1sum = cfg.mbox_sha1sum[0];
            }
            if (typeof cfg.openid === "object" && cfg.openid !== null) {
                if (cfg.openid.length > 1) {
                    this.degraded = true;
                }
                cfg.openid = cfg.openid[0];
            }
            if (typeof cfg.account === "object" && cfg.account !== null && typeof cfg.account.homePage === "undefined" && typeof cfg.account.name === "undefined") {
                if (cfg.account.length === 0) {
                    delete cfg.account;
                }
                else {
                    if (cfg.account.length > 1) {
                        this.degraded = true;
                    }
                    cfg.account = cfg.account[0];
                }
            }

            if (cfg.hasOwnProperty("account")) {
                if (cfg.account instanceof TinCan.AgentAccount) {
                    this.account = cfg.account;
                }
                else {
                    this.account = new TinCan.AgentAccount (cfg.account);
                }
            }

            for (i = 0; i < directProps.length; i += 1) {
                if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) {
                    val = cfg[directProps[i]];
                    if (directProps[i] === "mbox" && val.indexOf("mailto:") === -1) {
                        val = "mailto:" + val;
                    }
                    this[directProps[i]] = val;
                }
            }
        },

        toString: function () {
            this.log("toString");

            if (this.name !== null) {
                return this.name;
            }
            if (this.mbox !== null) {
                return this.mbox.replace("mailto:", "");
            }
            if (this.mbox_sha1sum !== null) {
                return this.mbox_sha1sum;
            }
            if (this.openid !== null) {
                return this.openid;
            }
            if (this.account !== null) {
                return this.account.toString();
            }

            return this.objectType + ": unidentified";
        },

        /**
        While a TinCan.Agent instance can store more than one inverse functional identifier
        this method will always only output one to be compliant with the statement sending
        specification. Order of preference is: mbox, mbox_sha1sum, openid, account

        @method asVersion
        @param {String} [version] Version to return (defaults to newest supported)
        */
        asVersion: function (version) {
            this.log("asVersion: " + version);
            var result = {
                objectType: this.objectType
            };

            version = version || TinCan.versions()[0];

            if (version === "0.9") {
                if (this.mbox !== null) {
                    result.mbox = [ this.mbox ];
                }
                else if (this.mbox_sha1sum !== null) {
                    result.mbox_sha1sum = [ this.mbox_sha1sum ];
                }
                else if (this.openid !== null) {
                    result.openid = [ this.openid ];
                }
                else if (this.account !== null) {
                    result.account = [ this.account.asVersion(version) ];
                }

                if (this.name !== null) {
                    result.name = [ this.name ];
                }
            } else {
                if (this.mbox !== null) {
                    result.mbox = this.mbox;
                }
                else if (this.mbox_sha1sum !== null) {
                    result.mbox_sha1sum = this.mbox_sha1sum;
                }
                else if (this.openid !== null) {
                    result.openid = this.openid;
                }
                else if (this.account !== null) {
                    result.account = this.account.asVersion(version);
                }

                if (this.name !== null) {
                    result.name = this.name;
                }
            }

            return result;
        }
    };

    /**
    @method fromJSON
    @return {Object} Agent
    @static
    */
    Agent.fromJSON = function (agentJSON) {
        Agent.prototype.log("fromJSON");
        var _agent = JSON.parse(agentJSON);

        return new Agent(_agent);
    };
}());

/*
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.Group
**/
(function () {
    "use strict";

    /**
    @class TinCan.Group
    @constructor
    */
    var Group = TinCan.Group = function (cfg) {
        this.log("constructor");

        /**
        @property name
        @type String
        */
        this.name = null;

        /**
        @property mbox
        @type String
        */
        this.mbox = null;

        /**
        @property mbox_sha1sum
        @type String
        */
        this.mbox_sha1sum = null;

        /**
        @property openid
        @type String
        */
        this.openid = null;

        /**
        @property account
        @type TinCan.AgentAccount
        */
        this.account = null;

        /**
        @property member
        @type Array
        */
        this.member = [];

        this.init(cfg);
    };
    Group.prototype = {
        /**
        @property objectType
        @type String
        @default "Group"
        @static
        */
        objectType: "Group",

        /**
        @property LOG_SRC
        */
        LOG_SRC: "Group",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");
            var i;

            cfg = cfg || {};

            TinCan.Agent.prototype.init.call(this, cfg);

            if (typeof cfg.member !== "undefined") {
                for (i = 0; i < cfg.member.length; i += 1) {
                    if (cfg.member[i] instanceof TinCan.Agent) {
                        this.member.push(cfg.member[i]);
                    }
                    else {
                        this.member.push(new TinCan.Agent (cfg.member[i]));
                    }
                }
            }
        },

        toString: function (lang) {
            this.log("toString");

            var result = TinCan.Agent.prototype.toString.call(this, lang);
            if (result !== this.objectType + ": unidentified") {
                result = this.objectType + ": " + result;
            }

            return result;
        },

        /**
        @method asVersion
        @param {String} [version] Version to return (defaults to newest supported)
        */
        asVersion: function (version) {
            this.log("asVersion: " + version);
            var result,
                i
            ;

            version = version || TinCan.versions()[0];

            result = TinCan.Agent.prototype.asVersion.call(this, version);

            if (this.member.length > 0) {
                result.member = [];
                for (i = 0; i < this.member.length; i += 1) {
                    result.member.push(this.member[i].asVersion(version));
                }
            }

            return result;
        }
    };

    /**
    @method fromJSON
    @return {Object} Group
    @static
    */
    Group.fromJSON = function (groupJSON) {
        Group.prototype.log("fromJSON");
        var _group = JSON.parse(groupJSON);

        return new Group(_group);
    };
}());

/*
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.Verb
*/
(function () {
    "use strict";

    //
    // this represents the full set of verb values that were
    // allowed by the .9 spec version, if an object is created with one of
    // the short forms it will be upconverted to the matching long form,
    // for local storage and use and if an object is needed in .9 version
    // consequently down converted
    //
    // hopefully this list will never grow (or change) and only the exact
    // ADL compatible URLs should be matched
    //
    var _downConvertMap = {
        "http://adlnet.gov/expapi/verbs/experienced": "experienced",
        "http://adlnet.gov/expapi/verbs/attended":    "attended",
        "http://adlnet.gov/expapi/verbs/attempted":   "attempted",
        "http://adlnet.gov/expapi/verbs/completed":   "completed",
        "http://adlnet.gov/expapi/verbs/passed":      "passed",
        "http://adlnet.gov/expapi/verbs/failed":      "failed",
        "http://adlnet.gov/expapi/verbs/answered":    "answered",
        "http://adlnet.gov/expapi/verbs/interacted":  "interacted",
        "http://adlnet.gov/expapi/verbs/imported":    "imported",
        "http://adlnet.gov/expapi/verbs/created":     "created",
        "http://adlnet.gov/expapi/verbs/shared":      "shared",
        "http://adlnet.gov/expapi/verbs/voided":      "voided"
    },

    /**
    @class TinCan.Verb
    @constructor
    */
    Verb = TinCan.Verb = function (cfg) {
        this.log("constructor");

        /**
        @property id
        @type String
        */
        this.id = null;

        /**
        @property display
        @type Object
        */
        this.display = null;

        this.init(cfg);
    };
    Verb.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "Verb",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");
            var i,
                directProps = [
                    "id",
                    "display"
                ],
                prop
            ;

            if (typeof cfg === "string") {
                this.id = cfg;
                this.display = {
                    und: this.id
                };

                //If simple string like "attempted" was passed in (0.9 verbs),
                //upconvert the ID to the 0.95 ADL version
                for (prop in _downConvertMap) {
                    if (_downConvertMap.hasOwnProperty(prop) && _downConvertMap[prop] === cfg) {
                        this.id = prop;
                        break;
                    }
                }
            }
            else {
                cfg = cfg || {};

                for (i = 0; i < directProps.length; i += 1) {
                    if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) {
                        this[directProps[i]] = cfg[directProps[i]];
                    }
                }

                if (this.display === null && typeof _downConvertMap[this.id] !== "undefined") {
                    this.display = {
                        "und": _downConvertMap[this.id]
                    };
                }
            }
        },

        /**
        @method toString
        @return {String} String representation of the verb
        */
        toString: function (lang) {
            this.log("toString");

            if (this.display !== null) {
                return this.getLangDictionaryValue("display", lang);
            }

            return this.id;
        },

        /**
        @method asVersion
        @param {String} [version] Version to return (defaults to newest supported)
        */
        asVersion: function (version) {
            this.log("asVersion");
            var result;

            version = version || TinCan.versions()[0];

            if (version === "0.9") {
                result = _downConvertMap[this.id];
            }
            else {
                result = {
                    id: this.id
                };
                if (this.display !== null) {
                    result.display = this.display;
                }
            }

            return result;
        },

        /**
        See {{#crossLink "TinCan.Utils/getLangDictionaryValue"}}{{/crossLink}}

        @method getLangDictionaryValue
        */
        getLangDictionaryValue: TinCan.Utils.getLangDictionaryValue
    };

    /**
    @method fromJSON
    @param {String} verbJSON String of JSON representing the verb
    @return {Object} Verb
    @static
    */
    Verb.fromJSON = function (verbJSON) {
        Verb.prototype.log("fromJSON");
        var _verb = JSON.parse(verbJSON);

        return new Verb(_verb);
    };
}());

/*
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.Result
**/
(function () {
    "use strict";

    /**
    @class TinCan.Result
    @constructor
    */
    var Result = TinCan.Result = function (cfg) {
        this.log("constructor");

        /**
        @property score
        @type TinCan.Score|null
        */
        this.score = null;

        /**
        @property success
        @type Boolean|null
        */
        this.success = null;

        /**
        @property completion
        @type Boolean|null
        */
        this.completion = null;

        /**
        @property duration
        @type String|null
        */
        this.duration = null;

        /**
        @property response
        @type String|null
        */
        this.response = null;

        /**
        @property extensions
        @type Object|null
        */
        this.extensions = null;

        this.init(cfg);
    };
    Result.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "Result",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");

            var i,
                directProps = [
                    "completion",
                    "duration",
                    "extensions",
                    "response",
                    "success"
                ]
            ;

            cfg = cfg || {};

            if (cfg.hasOwnProperty("score") && cfg.score !== null) {
                if (cfg.score instanceof TinCan.Score) {
                    this.score = cfg.score;
                }
                else {
                    this.score = new TinCan.Score (cfg.score);
                }
            }

            for (i = 0; i < directProps.length; i += 1) {
                if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) {
                    this[directProps[i]] = cfg[directProps[i]];
                }
            }

            // 0.9 used a string, store it internally as a bool
            if (this.completion === "Completed") {
                this.completion = true;
            }
        },

        /**
        @method asVersion
        @param {String} [version] Version to return (defaults to newest supported)
        */
        asVersion: function (version) {
            this.log("asVersion");
            var result = {},
                optionalDirectProps = [
                    "success",
                    "duration",
                    "response",
                    "extensions"
                ],
                optionalObjProps = [
                    "score"
                ],
                i;

            version = version || TinCan.versions()[0];

            for (i = 0; i < optionalDirectProps.length; i += 1) {
                if (this[optionalDirectProps[i]] !== null) {
                    result[optionalDirectProps[i]] = this[optionalDirectProps[i]];
                }
            }
            for (i = 0; i < optionalObjProps.length; i += 1) {
                if (this[optionalObjProps[i]] !== null) {
                    result[optionalObjProps[i]] = this[optionalObjProps[i]].asVersion(version);
                }
            }
            if (this.completion !== null) {
                if (version === "0.9") {
                    if (this.completion) {
                        result.completion = "Completed";
                    }
                }
                else {
                    result.completion = this.completion;
                }
            }

            return result;
        }
    };

    /**
    @method fromJSON
    @return {Object} Result
    @static
    */
    Result.fromJSON = function (resultJSON) {
        Result.prototype.log("fromJSON");
        var _result = JSON.parse(resultJSON);

        return new Result(_result);
    };
}());

/*
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.Score
**/
(function () {
    "use strict";

    /**
    @class TinCan.Score
    @constructor
    */
    var Score = TinCan.Score = function (cfg) {
        this.log("constructor");

        /**
        @property scaled
        @type String
        */
        this.scaled = null;

        /**
        @property raw
        @type String
        */
        this.raw = null;

        /**
        @property min
        @type String
        */
        this.min = null;

        /**
        @property max
        @type String
        */
        this.max = null;

        this.init(cfg);
    };
    Score.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "Score",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");

            var i,
                directProps = [
                    "scaled",
                    "raw",
                    "min",
                    "max"
                ]
            ;

            cfg = cfg || {};

            for (i = 0; i < directProps.length; i += 1) {
                if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) {
                    this[directProps[i]] = cfg[directProps[i]];
                }
            }
        },

        /**
        @method asVersion
        @param {String} [version] Version to return (defaults to newest supported)
        */
        asVersion: function (version) {
            this.log("asVersion");
            var result = {},
                optionalDirectProps = [
                    "scaled",
                    "raw",
                    "min",
                    "max"
                ],
                i;

            version = version || TinCan.versions()[0];

            for (i = 0; i < optionalDirectProps.length; i += 1) {
                if (this[optionalDirectProps[i]] !== null) {
                    result[optionalDirectProps[i]] = this[optionalDirectProps[i]];
                }
            }

            return result;
        }
    };

    /**
    @method fromJSON
    @return {Object} Score
    @static
    */
    Score.fromJSON = function (scoreJSON) {
        Score.prototype.log("fromJSON");
        var _score = JSON.parse(scoreJSON);

        return new Score(_score);
    };
}());

/*
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.InteractionComponent
**/
(function () {
    "use strict";

    /**
    @class TinCan.InteractionComponent
    @constructor
    */
    var InteractionComponent = TinCan.InteractionComponent = function (cfg) {
        this.log("constructor");

        /**
        @property id
        @type String
        */
        this.id = null;

        /**
        @property description
        @type Object
        */
        this.description = null;

        this.init(cfg);
    };
    InteractionComponent.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "InteractionComponent",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");
            var i,
                directProps = [
                    "id",
                    "description"
                ]
            ;

            cfg = cfg || {};

            for (i = 0; i < directProps.length; i += 1) {
                if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) {
                    this[directProps[i]] = cfg[directProps[i]];
                }
            }
        },

        /**
        @method asVersion
        @param {String} [version] Version to return (defaults to newest supported)
        */
        asVersion: function (version) {
            this.log("asVersion");
            var result = {
                    id: this.id
                },
                optionalDirectProps = [
                    "description"
                ],
                i,
                prop;

            version = version || TinCan.versions()[0];

            for (i = 0; i < optionalDirectProps.length; i += 1) {
                prop = optionalDirectProps[i];
                if (this[prop] !== null) {
                    result[prop] = this[prop];
                }
            }

            return result;
        },

        /**
        See {{#crossLink "TinCan.Utils/getLangDictionaryValue"}}{{/crossLink}}

        @method getLangDictionaryValue
        */
        getLangDictionaryValue: TinCan.Utils.getLangDictionaryValue
    };

    /**
    @method fromJSON
    @return {Object} InteractionComponent
    @static
    */
    InteractionComponent.fromJSON = function (icJSON) {
        InteractionComponent.prototype.log("fromJSON");
        var _ic = JSON.parse(icJSON);

        return new InteractionComponent(_ic);
    };
}());

/*
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.ActivityDefinition
**/
(function () {
    "use strict";

    //
    // this represents the full set of activity definition types that were
    // allowed by the .9 spec version, if an object is created with one of
    // the short forms it will be upconverted to the matching long form,
    // for local storage and use and if an object is needed in .9 version
    // consequently down converted
    //
    // hopefully this list will never grow (or change) and only the exact
    // ADL compatible URLs should be matched
    //
    var _downConvertMap = {
        "http://adlnet.gov/expapi/activities/course": "course",
        "http://adlnet.gov/expapi/activities/module": "module",
        "http://adlnet.gov/expapi/activities/meeting": "meeting",
        "http://adlnet.gov/expapi/activities/media": "media",
        "http://adlnet.gov/expapi/activities/performance": "performance",
        "http://adlnet.gov/expapi/activities/simulation": "simulation",
        "http://adlnet.gov/expapi/activities/assessment": "assessment",
        "http://adlnet.gov/expapi/activities/interaction": "interaction",
        "http://adlnet.gov/expapi/activities/cmi.interaction": "cmi.interaction",
        "http://adlnet.gov/expapi/activities/question": "question",
        "http://adlnet.gov/expapi/activities/objective": "objective",
        "http://adlnet.gov/expapi/activities/link": "link"
    },

    /**
    @class TinCan.ActivityDefinition
    @constructor
    */
    ActivityDefinition = TinCan.ActivityDefinition = function (cfg) {
        this.log("constructor");

        /**
        @property name
        @type Object
        */
        this.name = null;

        /**
        @property description
        @type Object
        */
        this.description = null;

        /**
        @property type
        @type String
        */
        this.type = null;

        /**
        @property moreInfo
        @type String
        */
        this.moreInfo = null;

        /**
        @property extensions
        @type Object
        */
        this.extensions = null;

        /**
        @property interactionType
        @type String
        */
        this.interactionType = null;

        /**
        @property correctResponsesPattern
        @type Array
        */
        this.correctResponsesPattern = null;

        /**
        @property choices
        @type Array
        */
        this.choices = null;

        /**
        @property scale
        @type Array
        */
        this.scale = null;

        /**
        @property source
        @type Array
        */
        this.source = null;

        /**
        @property target
        @type Array
        */
        this.target = null;

        /**
        @property steps
        @type Array
        */
        this.steps = null;

        this.init(cfg);
    };
    ActivityDefinition.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "ActivityDefinition",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");

            var i,
                j,
                prop,
                directProps = [
                    "name",
                    "description",
                    "moreInfo",
                    "extensions",
                    "correctResponsesPattern"
                ],
                interactionComponentProps = []
            ;

            cfg = cfg || {};

            if (cfg.hasOwnProperty("type") && cfg.type !== null) {
                // TODO: verify type is URI?
                for (prop in _downConvertMap) {
                    if (_downConvertMap.hasOwnProperty(prop) && _downConvertMap[prop] === cfg.type) {
                        cfg.type = _downConvertMap[prop];
                    }
                }
                this.type = cfg.type;
            }

            if (cfg.hasOwnProperty("interactionType") && cfg.interactionType !== null) {
                // TODO: verify interaction type in acceptable set?
                this.interactionType = cfg.interactionType;
                if (cfg.interactionType === "choice" || cfg.interactionType === "sequencing") {
                    interactionComponentProps.push("choices");
                }
                else if (cfg.interactionType === "likert") {
                    interactionComponentProps.push("scale");
                }
                else if (cfg.interactionType === "matching") {
                    interactionComponentProps.push("source");
                    interactionComponentProps.push("target");
                }
                else if (cfg.interactionType === "performance") {
                    interactionComponentProps.push("steps");
                }

                if (interactionComponentProps.length > 0) {
                    for (i = 0; i < interactionComponentProps.length; i += 1) {
                        prop = interactionComponentProps[i];
                        if (cfg.hasOwnProperty(prop) && cfg[prop] !== null) {
                            this[prop] = [];
                            for (j = 0; j < cfg[prop].length; j += 1) {
                                if (cfg[prop][j] instanceof TinCan.InteractionComponent) {
                                    this[prop].push(cfg[prop][j]);
                                } else {
                                    this[prop].push(
                                        new TinCan.InteractionComponent (
                                            cfg[prop][j]
                                        )
                                    );
                                }
                            }
                        }
                    }
                }
            }

            for (i = 0; i < directProps.length; i += 1) {
                if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) {
                    this[directProps[i]] = cfg[directProps[i]];
                }
            }
        },

        /**
        @method toString
        @return {String} String representation of the definition
        */
        toString: function (lang) {
            this.log("toString");

            if (this.name !== null) {
                return this.getLangDictionaryValue("name", lang);
            }

            if (this.description !== null) {
                return this.getLangDictionaryValue("description", lang);
            }

            return "";
        },

        /**
        @method asVersion
        @param {String} [version] Version to return (defaults to newest supported)
        */
        asVersion: function (version) {
            this.log("asVersion");
            var result = {},
                directProps = [
                    "name",
                    "description",
                    "interactionType",
                    "correctResponsesPattern",
                    "extensions"
                ],
                interactionComponentProps = [
                    "choices",
                    "scale",
                    "source",
                    "target",
                    "steps"
                ],
                i,
                j,
                prop
            ;

            version = version || TinCan.versions()[0];

            if (this.type !== null) {
                if (version === "0.9") {
                    result.type = _downConvertMap[this.type];
                }
                else {
                    result.type = this.type;
                }
            }

            for (i = 0; i < directProps.length; i += 1) {
                prop = directProps[i];
                if (this[prop] !== null) {
                    result[prop] = this[prop];
                }
            }

            for (i = 0; i < interactionComponentProps.length; i += 1) {
                prop = interactionComponentProps[i];
                if (this[prop] !== null) {
                    result[prop] = [];
                    for (j = 0; j < this[prop].length; j += 1) {
                        result[prop].push(
                            this[prop][j].asVersion(version)
                        );
                    }
                }
            }

            if (version.indexOf("0.9") !== 0) {
                if (this.moreInfo !== null) {
                    result.moreInfo = this.moreInfo;
                }
            }

            return result;
        },

        /**
        See {{#crossLink "TinCan.Utils/getLangDictionaryValue"}}{{/crossLink}}

        @method getLangDictionaryValue
        */
        getLangDictionaryValue: TinCan.Utils.getLangDictionaryValue
    };

    /**
    @method fromJSON
    @return {Object} ActivityDefinition
    @static
    */
    ActivityDefinition.fromJSON = function (definitionJSON) {
        ActivityDefinition.prototype.log("fromJSON");
        var _definition = JSON.parse(definitionJSON);

        return new ActivityDefinition(_definition);
    };
}());

/*
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.Activity
**/
(function () {
    "use strict";

    /**
    @class TinCan.Activity
    @constructor
    */
    var Activity = TinCan.Activity = function (cfg) {
        this.log("constructor");

        /**
        @property objectType
        @type String
        @default Activity
        */
        this.objectType = "Activity";

        /**
        @property id
        @type String
        */
        this.id = null;

        /**
        @property definition
        @type TinCan.ActivityDefinition
        */
        this.definition = null;

        this.init(cfg);
    };
    Activity.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "Activity",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");

            var i,
                directProps = [
                    "id"
                ]
            ;

            cfg = cfg || {};

            if (cfg.hasOwnProperty("definition")) {
                if (cfg.definition instanceof TinCan.ActivityDefinition) {
                    this.definition = cfg.definition;
                } else {
                    this.definition = new TinCan.ActivityDefinition (cfg.definition);
                }
            }

            for (i = 0; i < directProps.length; i += 1) {
                if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) {
                    this[directProps[i]] = cfg[directProps[i]];
                }
            }
        },

        /**
        @method toString
        @return {String} String representation of the activity
        */
        toString: function (lang) {
            this.log("toString");
            var defString = "";

            if (this.definition !== null) {
                defString = this.definition.toString(lang);
                if (defString !== "") {
                    return defString;
                }
            }

            if (this.id !== null) {
                return this.id;
            }

            return "Activity: unidentified";
        },

        /**
        @method asVersion
        @param {String} [version] Version to return (defaults to newest supported)
        */
        asVersion: function (version) {
            this.log("asVersion");
            var result = {
                id: this.id,
                objectType: this.objectType
            };

            version = version || TinCan.versions()[0];

            if (this.definition !== null) {
                result.definition = this.definition.asVersion(version);
            }

            return result;
        }
    };

    /**
    @method fromJSON
    @return {Object} Activity
    @static
    */
    Activity.fromJSON = function (activityJSON) {
        Activity.prototype.log("fromJSON");
        var _activity = JSON.parse(activityJSON);

        return new Activity(_activity);
    };
}());

/*
    Copyright 2013 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.ContextActivities
**/
(function () {
    "use strict";

    /**
    @class TinCan.ContextActivities
    @constructor
    */
    var ContextActivities = TinCan.ContextActivities = function (cfg) {
        this.log("constructor");

        /**
        @property category
        @type Array
        */
        this.category = null;

        /**
        @property parent
        @type Array
        */
        this.parent = null;

        /**
        @property grouping
        @type Array
        */
        this.grouping = null;

        /**
        @property other
        @type Array
        */
        this.other = null;

        this.init(cfg);
    };
    ContextActivities.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "ContextActivities",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");

            var i,
                j,
                objProps = [
                    "category",
                    "parent",
                    "grouping",
                    "other"
                ],
                prop,
                val
            ;

            cfg = cfg || {};

            for (i = 0; i < objProps.length; i += 1) {
                prop = objProps[i];
                if (cfg.hasOwnProperty(prop) && cfg[prop] !== null) {
                    if (Object.prototype.toString.call(cfg[prop]) === "[object Array]") {
                        for (j = 0; j < cfg[prop].length; j += 1) {
                            this.add(prop, cfg[prop][j]);
                        }
                    }
                    else {
                        val = cfg[prop];

                        this.add(prop, val);
                    }
                }
            }
        },

        /**
        @method add
        @param String key Property to add value to one of "category", "parent", "grouping", "other"
        @return Number index where the value was added
        */
        add: function (key, val) {
            if (key !== "category" && key !== "parent" && key !== "grouping" && key !== "other") {
                return;
            }

            if (this[key] === null) {
                this[key] = [];
            }

            if (! (val instanceof TinCan.Activity)) {
                val = typeof val === "string" ? { id: val } : val;
                val = new TinCan.Activity (val);
            }

            this[key].push(val);

            return this[key].length - 1;
        },

        /**
        @method asVersion
        @param {String} [version] Version to return (defaults to newest supported)
        */
        asVersion: function (version) {
            this.log("asVersion");
            var result = {},
                optionalObjProps = [
                    "parent",
                    "grouping",
                    "other"
                ],
                i,
                j;

            version = version || TinCan.versions()[0];

            for (i = 0; i < optionalObjProps.length; i += 1) {
                if (this[optionalObjProps[i]] !== null && this[optionalObjProps[i]].length > 0) {
                    if (version === "0.9" || version === "0.95") {
                        if (this[optionalObjProps[i]].length > 1) {
                            // TODO: exception?
                            this.log("[warning] version does not support multiple values in: " + optionalObjProps[i]);
                        }

                        result[optionalObjProps[i]] = this[optionalObjProps[i]][0].asVersion(version);
                    }
                    else {
                        result[optionalObjProps[i]] = [];
                        for (j = 0; j < this[optionalObjProps[i]].length; j += 1) {
                            result[optionalObjProps[i]].push(
                                this[optionalObjProps[i]][j].asVersion(version)
                            );
                        }
                    }
                }
            }
            if (this.category !== null && this.category.length > 0) {
                if (version === "0.9" || version === "0.95") {
                    this.log("[error] version does not support the 'category' property: " + version);
                    throw new Error(version + " does not support the 'category' property");
                }
                else {
                    result.category = [];
                    for (i = 0; i < this.category.length; i += 1) {
                        result.category.push(this.category[i].asVersion(version));
                    }
                }
            }

            return result;
        }
    };

    /**
    @method fromJSON
    @return {Object} ContextActivities
    @static
    */
    ContextActivities.fromJSON = function (contextActivitiesJSON) {
        ContextActivities.prototype.log("fromJSON");
        var _contextActivities = JSON.parse(contextActivitiesJSON);

        return new ContextActivities(_contextActivities);
    };
}());

/*
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.Context
**/
(function () {
    "use strict";

    /**
    @class TinCan.Context
    @constructor
    */
    var Context = TinCan.Context = function (cfg) {
        this.log("constructor");

        /**
        @property registration
        @type String|null
        */
        this.registration = null;

        /**
        @property instructor
        @type TinCan.Agent|TinCan.Group|null
        */
        this.instructor = null;

        /**
        @property team
        @type TinCan.Agent|TinCan.Group|null
        */
        this.team = null;

        /**
        @property contextActivities
        @type ContextActivities|null
        */
        this.contextActivities = null;

        /**
        @property revision
        @type String|null
        */
        this.revision = null;

        /**
        @property platform
        @type Object|null
        */
        this.platform = null;

        /**
        @property language
        @type String|null
        */
        this.language = null;

        /**
        @property statement
        @type StatementRef|null
        */
        this.statement = null;

        /**
        @property extensions
        @type String
        */
        this.extensions = null;

        this.init(cfg);
    };
    Context.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "Context",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");

            var i,
                directProps = [
                    "registration",
                    "revision",
                    "platform",
                    "language",
                    "extensions"
                ],
                agentGroupProps = [
                    "instructor",
                    "team"
                ],
                prop,
                val
            ;

            cfg = cfg || {};

            for (i = 0; i < directProps.length; i += 1) {
                prop = directProps[i];
                if (cfg.hasOwnProperty(prop) && cfg[prop] !== null) {
                    this[prop] = cfg[prop];
                }
            }
            for (i = 0; i < agentGroupProps.length; i += 1) {
                prop = agentGroupProps[i];
                if (cfg.hasOwnProperty(prop) && cfg[prop] !== null) {
                    val = cfg[prop];

                    if (typeof val.objectType === "undefined" || val.objectType === "Person") {
                        val.objectType = "Agent";
                    }

                    if (val.objectType === "Agent" && ! (val instanceof TinCan.Agent)) {
                        val = new TinCan.Agent (val);
                    } else if (val.objectType === "Group" && ! (val instanceof TinCan.Group)) {
                        val = new TinCan.Group (val);
                    }

                    this[prop] = val;
                }
            }

            if (cfg.hasOwnProperty("contextActivities") && cfg.contextActivities !== null) {
                if (cfg.contextActivities instanceof TinCan.ContextActivities) {
                    this.contextActivities = cfg.contextActivities;
                }
                else {
                    this.contextActivities = new TinCan.ContextActivities(cfg.contextActivities);
                }
            }

            if (cfg.hasOwnProperty("statement") && cfg.statement !== null) {
                if (cfg.statement instanceof TinCan.StatementRef) {
                    this.statement = cfg.statement;
                }
                else if (cfg.statement instanceof TinCan.SubStatement) {
                    this.statement = cfg.statement;
                }
                else if (cfg.statement.objectType === "StatementRef") {
                    this.statement = new TinCan.StatementRef(cfg.statement);
                }
                else if (cfg.statement.objectType === "SubStatement") {
                    this.statement = new TinCan.SubStatement(cfg.statement);
                }
                else {
                    this.log("Unable to parse statement.context.statement property.");
                }
            }
        },

        /**
        @method asVersion
        @param {String} [version] Version to return (defaults to newest supported)
        */
        asVersion: function (version) {
            this.log("asVersion");
            var result = {},
                optionalDirectProps = [
                    "registration",
                    "revision",
                    "platform",
                    "language",
                    "extensions"
                ],
                optionalObjProps = [
                    "instructor",
                    "team",
                    "contextActivities",
                    "statement"
                ],
                i;

            version = version || TinCan.versions()[0];

            if (this.statement instanceof TinCan.SubStatement && version !== "0.9" && version !== "0.95") {
                this.log("[error] version does not support SubStatements in the 'statement' property: " + version);
                throw new Error(version + " does not support SubStatements in the 'statement' property");
            }

            for (i = 0; i < optionalDirectProps.length; i += 1) {
                if (this[optionalDirectProps[i]] !== null) {
                    result[optionalDirectProps[i]] = this[optionalDirectProps[i]];
                }
            }
            for (i = 0; i < optionalObjProps.length; i += 1) {
                if (this[optionalObjProps[i]] !== null) {
                    result[optionalObjProps[i]] = this[optionalObjProps[i]].asVersion(version);
                }
            }

            return result;
        }
    };

    /**
    @method fromJSON
    @return {Object} Context
    @static
    */
    Context.fromJSON = function (contextJSON) {
        Context.prototype.log("fromJSON");
        var _context = JSON.parse(contextJSON);

        return new Context(_context);
    };
}());

/*
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.StatementRef
**/
(function () {
    "use strict";

    /**
    @class TinCan.StatementRef
    @constructor
    @param {Object} [cfg] Configuration used to initialize.
        @param {Object} [cfg.id] ID of statement to reference
    **/
    var StatementRef = TinCan.StatementRef = function (cfg) {
        this.log("constructor");

        /**
        @property id
        @type String
        */
        this.id = null;

        this.init(cfg);
    };

    StatementRef.prototype = {
        /**
        @property objectType
        @type String
        @default Agent
        */
        objectType: "StatementRef",

        /**
        @property LOG_SRC
        */
        LOG_SRC: "StatementRef",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize (see constructor)
        */
        init: function (cfg) {
            this.log("init");
            var i,
                directProps = [
                    "id"
                ];

            cfg = cfg || {};

            for (i = 0; i < directProps.length; i += 1) {
                if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) {
                    this[directProps[i]] = cfg[directProps[i]];
                }
            }
        },

        /**
        @method toString
        @return {String} String representation of the statement
        */
        toString: function () {
            this.log("toString");
            return this.id;
        },

        /**
        @method asVersion
        @param {String} [version] Version to return (defaults to newest supported)
        */
        asVersion: function (version) {
            this.log("asVersion");
            var result = {
                objectType: this.objectType,
                id: this.id
            };

            if (version === "0.9") {
                result.objectType = "Statement";
            }

            return result;
        }
    };

    /**
    @method fromJSON
    @return {Object} StatementRef
    @static
    */
    StatementRef.fromJSON = function (stRefJSON) {
        StatementRef.prototype.log("fromJSON");
        var _stRef = JSON.parse(stRefJSON);

        return new StatementRef(_stRef);
    };
}());

/*
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.SubStatement
**/
(function () {
    "use strict";

    /**
    @class TinCan.SubStatement
    @constructor
    @param {Object} [cfg] Configuration used to initialize.
        @param {TinCan.Agent} [cfg.actor] Actor of statement
        @param {TinCan.Verb} [cfg.verb] Verb of statement
        @param {TinCan.Activity|TinCan.Agent} [cfg.object] Alias for 'target'
        @param {TinCan.Activity|TinCan.Agent} [cfg.target] Object of statement
        @param {TinCan.Result} [cfg.result] Statement Result
        @param {TinCan.Context} [cfg.context] Statement Context
    **/
    var SubStatement = TinCan.SubStatement = function (cfg) {
        this.log("constructor");

        /**
        @property actor
        @type Object
        */
        this.actor = null;

        /**
        @property verb
        @type Object
        */
        this.verb = null;

        /**
        @property target
        @type Object
        */
        this.target = null;

        /**
        @property result
        @type Object
        */
        this.result = null;

        /**
        @property context
        @type Object
        */
        this.context = null;

        /**
        @property timestamp
        @type Date
        */
        this.timestamp = null;

        this.init(cfg);
    };

    SubStatement.prototype = {
        /**
        @property objectType
        @type String
        @default Agent
        */
        objectType: "SubStatement",

        /**
        @property LOG_SRC
        */
        LOG_SRC: "SubStatement",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize (see constructor)
        */
        init: function (cfg) {
            this.log("init");
            var i,
                directProps = [
                    "timestamp"
                ];

            cfg = cfg || {};

            if (cfg.hasOwnProperty("object")) {
                cfg.target = cfg.object;
            }

            if (cfg.hasOwnProperty("actor")) {
                if (typeof cfg.actor.objectType === "undefined" || cfg.actor.objectType === "Person") {
                    cfg.actor.objectType = "Agent";
                }

                if (cfg.actor.objectType === "Agent") {
                    if (cfg.actor instanceof TinCan.Agent) {
                        this.actor = cfg.actor;
                    } else {
                        this.actor = new TinCan.Agent (cfg.actor);
                    }
                } else if (cfg.actor.objectType === "Group") {
                    if (cfg.actor instanceof TinCan.Group) {
                        this.actor = cfg.actor;
                    } else {
                        this.actor = new TinCan.Group (cfg.actor);
                    }
                }
            }
            if (cfg.hasOwnProperty("verb")) {
                if (cfg.verb instanceof TinCan.Verb) {
                    this.verb = cfg.verb;
                } else {
                    this.verb = new TinCan.Verb (cfg.verb);
                }
            }
            if (cfg.hasOwnProperty("target")) {
                if (cfg.target instanceof TinCan.Activity ||
                    cfg.target instanceof TinCan.Agent ||
                    cfg.target instanceof TinCan.Group ||
                    cfg.target instanceof TinCan.SubStatement ||
                    cfg.target instanceof TinCan.StatementRef
                ) {
                    this.target = cfg.target;
                } else {
                    if (typeof cfg.target.objectType === "undefined") {
                        cfg.target.objectType = "Activity";
                    }

                    if (cfg.target.objectType === "Activity") {
                        this.target = new TinCan.Activity (cfg.target);
                    } else if (cfg.target.objectType === "Agent") {
                        this.target = new TinCan.Agent (cfg.target);
                    } else if (cfg.target.objectType === "Group") {
                        this.target = new TinCan.Group (cfg.target);
                    } else if (cfg.target.objectType === "SubStatement") {
                        this.target = new TinCan.SubStatement (cfg.target);
                    } else if (cfg.target.objectType === "StatementRef") {
                        this.target = new TinCan.StatementRef (cfg.target);
                    } else {
                        this.log("Unrecognized target type: " + cfg.target.objectType);
                    }
                }
            }
            if (cfg.hasOwnProperty("result")) {
                if (cfg.result instanceof TinCan.Result) {
                    this.result = cfg.result;
                } else {
                    this.result = new TinCan.Result (cfg.result);
                }
            }
            if (cfg.hasOwnProperty("context")) {
                if (cfg.context instanceof TinCan.Context) {
                    this.context = cfg.context;
                } else {
                    this.context = new TinCan.Context (cfg.context);
                }
            }

            for (i = 0; i < directProps.length; i += 1) {
                if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) {
                    this[directProps[i]] = cfg[directProps[i]];
                }
            }
        },

        /**
        @method toString
        @return {String} String representation of the statement
        */
        toString: function (lang) {
            this.log("toString");
            return (this.actor !== null ? this.actor.toString(lang) : "") +
                    " " +
                    (this.verb !== null ? this.verb.toString(lang) : "") +
                    " " +
                    (this.target !== null ? this.target.toString(lang) : "");
        },

        /**
        @method asVersion
        @param {String} [version] Version to return (defaults to newest supported)
        */
        asVersion: function (version) {
            this.log("asVersion");
            var result,
                optionalDirectProps = [
                    "timestamp"
                ],
                optionalObjProps = [
                    "actor",
                    "verb",
                    "result",
                    "context"
                ],
                i;

            result = {
                objectType: this.objectType
            };
            version = version || TinCan.versions()[0];

            for (i = 0; i < optionalDirectProps.length; i += 1) {
                if (this[optionalDirectProps[i]] !== null) {
                    result[optionalDirectProps[i]] = this[optionalDirectProps[i]];
                }
            }
            for (i = 0; i < optionalObjProps.length; i += 1) {
                if (this[optionalObjProps[i]] !== null) {
                    result[optionalObjProps[i]] = this[optionalObjProps[i]].asVersion(version);
                }
            }
            if (this.target !== null) {
                result.object = this.target.asVersion(version);
            }

            if (version === "0.9") {
                result.objectType = "Statement";
            }

            return result;
        }
    };

    /**
    @method fromJSON
    @return {Object} SubStatement
    @static
    */
    SubStatement.fromJSON = function (subStJSON) {
        SubStatement.prototype.log("fromJSON");
        var _subSt = JSON.parse(subStJSON);

        return new SubStatement(_subSt);
    };
}());

/*
    Copyright 2012-3 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.Statement
**/
(function () {
    "use strict";

    /**
    @class TinCan.Statement
    @constructor
    @param {Object} [cfg] Values to set in properties
        @param {String} [cfg.id] Statement ID (UUID)
        @param {TinCan.Agent} [cfg.actor] Actor of statement
        @param {TinCan.Verb} [cfg.verb] Verb of statement
        @param {TinCan.Activity|TinCan.Agent|TinCan.Group|TinCan.StatementRef|TinCan.SubStatement} [cfg.object] Alias for 'target'
        @param {TinCan.Activity|TinCan.Agent|TinCan.Group|TinCan.StatementRef|TinCan.SubStatement} [cfg.target] Object of statement
        @param {TinCan.Result} [cfg.result] Statement Result
        @param {TinCan.Context} [cfg.context] Statement Context
        @param {TinCan.Agent} [cfg.authority] Statement Authority
        @param {TinCan.Attachment} [cfg.attachments] Statement Attachments
        @param {String} [cfg.timestamp] ISO8601 Date/time value
        @param {String} [cfg.stored] ISO8601 Date/time value
        @param {String} [cfg.version] Version of the statement (post 0.95)
    @param {Object} [initCfg] Configuration of initialization process
        @param {Integer} [initCfg.storeOriginal] Whether to store a JSON stringified version
            of the original options object, pass number of spaces used for indent
        @param {Boolean} [initCfg.doStamp] Whether to automatically set the 'id' and 'timestamp'
            properties (default: true)
    **/
    var Statement = TinCan.Statement = function (cfg, initCfg) {
        this.log("constructor");

        // check for true value for API backwards compat
        if (typeof initCfg === "number") {
            initCfg = {
                storeOriginal: initCfg
            };
        } else {
            initCfg = initCfg || {};
        }
        if (typeof initCfg.storeOriginal === "undefined") {
            initCfg.storeOriginal = null;
        }
        if (typeof initCfg.doStamp === "undefined") {
            initCfg.doStamp = true;
        }

        /**
        @property id
        @type String
        */
        this.id = null;

        /**
        @property actor
        @type TinCan.Agent|TinCan.Group|null
        */
        this.actor = null;

        /**
        @property verb
        @type TinCan.Verb|null
        */
        this.verb = null;

        /**
        @property target
        @type TinCan.Activity|TinCan.Agent|TinCan.Group|TinCan.StatementRef|TinCan.SubStatement|null
        */
        this.target = null;

        /**
        @property result
        @type Object
        */
        this.result = null;

        /**
        @property context
        @type Object
        */
        this.context = null;

        /**
        @property timestamp
        @type String
        */
        this.timestamp = null;

        /**
        @property stored
        @type String
        */
        this.stored = null;

        /**
        @property authority
        @type TinCan.Agent|null
        */
        this.authority = null;

        /**
        @property attachments
        @type Array of TinCan.Attachment
        */
        this.attachments = null;

        /**
        @property version
        @type String
        */
        this.version = null;

        /**
        @property degraded
        @type Boolean
        @default false
        */
        this.degraded = false;

        /**
        @property voided
        @type Boolean
        @default null
        @deprecated
        */
        this.voided = null;

        /**
        @property inProgress
        @type Boolean
        @deprecated
        */
        this.inProgress = null;

        /**
        @property originalJSON
        @type String
        */
        this.originalJSON = null;

        this.init(cfg, initCfg);
    };

    Statement.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "Statement",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [properties] Configuration used to set properties (see constructor)
        @param {Object} [cfg] Configuration used to initialize (see constructor)
        */
        init: function (cfg, initCfg) {
            this.log("init");
            var i,
                directProps = [
                    "id",
                    "stored",
                    "timestamp",
                    "version",
                    "inProgress",
                    "voided"
                ];

            cfg = cfg || {};

            if (initCfg.storeOriginal) {
                this.originalJSON = JSON.stringify(cfg, null, initCfg.storeOriginal);
            }

            if (cfg.hasOwnProperty("object")) {
                cfg.target = cfg.object;
            }

            if (cfg.hasOwnProperty("actor")) {
                if (typeof cfg.actor.objectType === "undefined" || cfg.actor.objectType === "Person") {
                    cfg.actor.objectType = "Agent";
                }

                if (cfg.actor.objectType === "Agent") {
                    if (cfg.actor instanceof TinCan.Agent) {
                        this.actor = cfg.actor;
                    } else {
                        this.actor = new TinCan.Agent (cfg.actor);
                    }
                } else if (cfg.actor.objectType === "Group") {
                    if (cfg.actor instanceof TinCan.Group) {
                        this.actor = cfg.actor;
                    } else {
                        this.actor = new TinCan.Group (cfg.actor);
                    }
                }
            }
            if (cfg.hasOwnProperty("authority")) {
                if (typeof cfg.authority.objectType === "undefined" || cfg.authority.objectType === "Person") {
                    cfg.authority.objectType = "Agent";
                }

                if (cfg.authority.objectType === "Agent") {
                    if (cfg.authority instanceof TinCan.Agent) {
                        this.authority = cfg.authority;
                    } else {
                        this.authority = new TinCan.Agent (cfg.authority);
                    }
                } else if (cfg.authority.objectType === "Group") {
                    if (cfg.actor instanceof TinCan.Group) {
                        this.authority = cfg.authority;
                    } else {
                        this.authority = new TinCan.Group (cfg.authority);
                    }
                }
            }
            if (cfg.hasOwnProperty("verb")) {
                if (cfg.verb instanceof TinCan.Verb) {
                    this.verb = cfg.verb;
                } else {
                    this.verb = new TinCan.Verb (cfg.verb);
                }
            }
            if (cfg.hasOwnProperty("target")) {
                if (cfg.target instanceof TinCan.Activity ||
                    cfg.target instanceof TinCan.Agent ||
                    cfg.target instanceof TinCan.Group ||
                    cfg.target instanceof TinCan.SubStatement ||
                    cfg.target instanceof TinCan.StatementRef
                ) {
                    this.target = cfg.target;
                } else {
                    if (typeof cfg.target.objectType === "undefined") {
                        cfg.target.objectType = "Activity";
                    }

                    if (cfg.target.objectType === "Activity") {
                        this.target = new TinCan.Activity (cfg.target);
                    } else if (cfg.target.objectType === "Agent") {
                        this.target = new TinCan.Agent (cfg.target);
                    } else if (cfg.target.objectType === "Group") {
                        this.target = new TinCan.Group (cfg.target);
                    } else if (cfg.target.objectType === "SubStatement") {
                        this.target = new TinCan.SubStatement (cfg.target);
                    } else if (cfg.target.objectType === "StatementRef") {
                        this.target = new TinCan.StatementRef (cfg.target);
                    } else {
                        this.log("Unrecognized target type: " + cfg.target.objectType);
                    }
                }
            }
            if (cfg.hasOwnProperty("result")) {
                if (cfg.result instanceof TinCan.Result) {
                    this.result = cfg.result;
                } else {
                    this.result = new TinCan.Result (cfg.result);
                }
            }
            if (cfg.hasOwnProperty("context")) {
                if (cfg.context instanceof TinCan.Context) {
                    this.context = cfg.context;
                } else {
                    this.context = new TinCan.Context (cfg.context);
                }
            }
            if (cfg.hasOwnProperty("attachments") && cfg.attachments !== null) {
                this.attachments = [];
                for (i = 0; i < cfg.attachments.length; i += 1) {
                    if (! (cfg.attachments[i] instanceof TinCan.Attachment)) {
                        this.attachments.push(new TinCan.Attachment (cfg.attachments[i]));
                    }
                    else {
                        this.attachments.push(cfg.attachments[i]);
                    }
                }
            }

            for (i = 0; i < directProps.length; i += 1) {
                if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) {
                    this[directProps[i]] = cfg[directProps[i]];
                }
            }

            if (initCfg.doStamp) {
                this.stamp();
            }
        },

        /**
        @method toString
        @return {String} String representation of the statement
        */
        toString: function (lang) {
            this.log("toString");
            return (this.actor !== null ? this.actor.toString(lang) : "") +
                    " " +
                    (this.verb !== null ? this.verb.toString(lang) : "") +
                    " " +
                    (this.target !== null ? this.target.toString(lang) : "");
        },

        /**
        @method asVersion
        @param {String} [version] Version to return (defaults to newest supported)
        */
        asVersion: function (version) {
            this.log("asVersion");
            var result = {},
                optionalDirectProps = [
                    "id",
                    "timestamp"
                ],
                optionalObjProps = [
                    "actor",
                    "verb",
                    "result",
                    "context",
                    "authority"
                ],
                i;

            version = version || TinCan.versions()[0];

            for (i = 0; i < optionalDirectProps.length; i += 1) {
                if (this[optionalDirectProps[i]] !== null) {
                    result[optionalDirectProps[i]] = this[optionalDirectProps[i]];
                }
            }
            for (i = 0; i < optionalObjProps.length; i += 1) {
                if (this[optionalObjProps[i]] !== null) {
                    result[optionalObjProps[i]] = this[optionalObjProps[i]].asVersion(version);
                }
            }
            if (this.target !== null) {
                result.object = this.target.asVersion(version);
            }

            if (version === "0.9" || version === "0.95") {
                if (this.voided !== null) {
                    result.voided = this.voided;
                }
            }
            if (version === "0.9" && this.inProgress !== null) {
                result.inProgress = this.inProgress;
            }
            if (this.attachments !== null) {
                if (! (version === "0.9" || version === "0.95")) {
                    result.attachments = [];
                    for (i = 0; i < this.attachments.length; i += 1) {
                        if (this.attachments[i] instanceof TinCan.Attachment) {
                            result.attachments.push(this.attachments[i].asVersion(version));
                        }
                        else {
                            result.attachments.push(new TinCan.Attachment(this.attachments[i]).asVersion(version));
                        }
                    }
                }
            }

            return result;
        },

        /**
        Sets 'id' and 'timestamp' properties if not already set

        @method stamp
        */
        stamp: function () {
            this.log("stamp");
            if (this.id === null) {
                this.id = TinCan.Utils.getUUID();
            }
            if (this.timestamp === null) {
                this.timestamp = TinCan.Utils.getISODateString(new Date());
            }
        },

        /**
        Checks if the Statement has at least one attachment with content

        @method hasAttachmentsWithContent
        */
        hasAttachmentWithContent: function () {
            this.log("hasAttachmentWithContent");
            var i;

            if (this.attachments === null) {
                return false;
            }

            for (i = 0; i < this.attachments.length; i += 1) {
                if (this.attachments[i].content !== null) {
                    return true;
                }
            }

            return false;
        }
    };

    /**
    @method fromJSON
    @return {Object} Statement
    @static
    */
    Statement.fromJSON = function (stJSON) {
        Statement.prototype.log("fromJSON");
        var _st = JSON.parse(stJSON);

        return new Statement(_st);
    };
}());

/*
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.StatementsResult
**/
(function () {
    "use strict";

    /**
    @class TinCan.StatementsResult
    @constructor
    @param {Object} options Configuration used to initialize.
        @param {Array} options.statements Actor of statement
        @param {String} options.more URL to fetch more data
    **/
    var StatementsResult = TinCan.StatementsResult = function (cfg) {
        this.log("constructor");

        /**
        @property statements
        @type Array
        */
        this.statements = null;

        /**
        @property more
        @type String
        */
        this.more = null;

        this.init(cfg);
    };

    StatementsResult.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "StatementsResult",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");

            cfg = cfg || {};

            if (cfg.hasOwnProperty("statements")) {
                this.statements = cfg.statements;
            }
            if (cfg.hasOwnProperty("more")) {
                this.more = cfg.more;
            }
        }
    };

    /**
    @method fromJSON
    @return {Object} Agent
    @static
    */
    StatementsResult.fromJSON = function (resultJSON) {
        StatementsResult.prototype.log("fromJSON");
        var _result,
            stmts = [],
            stmt,
            i
        ;

        try {
            _result = JSON.parse(resultJSON);
        } catch (parseError) {
            StatementsResult.prototype.log("fromJSON - JSON.parse error: " + parseError);
        }

        if (_result) {
            for (i = 0; i < _result.statements.length; i += 1) {
                try {
                    stmt = new TinCan.Statement (_result.statements[i], 4);
                } catch (error) {
                    StatementsResult.prototype.log("fromJSON - statement instantiation failed: " + error + " (" + JSON.stringify(_result.statements[i]) + ")");

                    stmt = new TinCan.Statement (
                        {
                            id: _result.statements[i].id
                        },
                        4
                    );
                }

                stmts.push(stmt);
            }
            _result.statements = stmts;
        }

        return new StatementsResult (_result);
    };
}());

/*
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.State
**/
(function () {
    "use strict";

    /**
    @class TinCan.State
    @constructor
    */
    var State = TinCan.State = function (cfg) {
        this.log("constructor");

        /**
        @property id
        @type String
        */
        this.id = null;

        /**
        @property updated
        @type Boolean
        */
        this.updated = null;

        /**
        @property contents
        @type String
        */
        this.contents = null;

        /**
        @property etag
        @type String
        */
        this.etag = null;

        /**
        @property contentType
        @type String
        */
        this.contentType = null;

        this.init(cfg);
    };
    State.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "State",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");
            var i,
                directProps = [
                    "id",
                    "contents",
                    "etag",
                    "contentType"
                ];

            cfg = cfg || {};

            for (i = 0; i < directProps.length; i += 1) {
                if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) {
                    this[directProps[i]] = cfg[directProps[i]];
                }
            }

            this.updated = false;
        }
    };

    /**
    @method fromJSON
    @return {Object} State
    @static
    */
    State.fromJSON = function (stateJSON) {
        State.prototype.log("fromJSON");
        var _state = JSON.parse(stateJSON);

        return new State(_state);
    };
}());

/*
    Copyright 2012 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.ActivityProfile
**/
(function () {
    "use strict";

    /**
    @class TinCan.ActivityProfile
    @constructor
    */
    var ActivityProfile = TinCan.ActivityProfile = function (cfg) {
        this.log("constructor");

        /**
        @property id
        @type String
        */
        this.id = null;

        /**
        @property activity
        @type TinCan.Activity
        */
        this.activity = null;

        /**
        @property updated
        @type String
        */
        this.updated = null;

        /**
        @property contents
        @type String
        */
        this.contents = null;

        /**
        SHA1 of contents as provided by the server during last fetch,
        this should be passed through to saveActivityProfile

        @property etag
        @type String
        */
        this.etag = null;

        /**
        @property contentType
        @type String
        */
        this.contentType = null;

        this.init(cfg);
    };
    ActivityProfile.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "ActivityProfile",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");
            var i,
                directProps = [
                    "id",
                    "contents",
                    "etag",
                    "contentType"
                ];

            cfg = cfg || {};

            if (cfg.hasOwnProperty("activity")) {
                if (cfg.activity instanceof TinCan.Activity) {
                    this.activity = cfg.activity;
                }
                else {
                    this.activity = new TinCan.Activity (cfg.activity);
                }
            }

            for (i = 0; i < directProps.length; i += 1) {
                if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) {
                    this[directProps[i]] = cfg[directProps[i]];
                }
            }

            this.updated = false;
        }
    };

    /**
    @method fromJSON
    @return {Object} ActivityProfile
    @static
    */
    ActivityProfile.fromJSON = function (stateJSON) {
        ActivityProfile.prototype.log("fromJSON");
        var _state = JSON.parse(stateJSON);

        return new ActivityProfile(_state);
    };
}());

/*
    Copyright 2013 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.AgentProfile
**/
(function () {
    "use strict";

    /**
    @class TinCan.AgentProfile
    @constructor
    */
    var AgentProfile = TinCan.AgentProfile = function (cfg) {
        this.log("constructor");

        /**
        @property id
        @type String
        */
        this.id = null;

        /**
        @property agent
        @type TinCan.Agent
        */
        this.agent = null;

        /**
        @property updated
        @type String
        */
        this.updated = null;

        /**
        @property contents
        @type String
        */
        this.contents = null;

        /**
        SHA1 of contents as provided by the server during last fetch,
        this should be passed through to saveAgentProfile

        @property etag
        @type String
        */
        this.etag = null;

        /**
        @property contentType
        @type String
        */
        this.contentType = null;

        this.init(cfg);
    };
    AgentProfile.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "AgentProfile",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");
            var i,
                directProps = [
                    "id",
                    "contents",
                    "etag",
                    "contentType"
                ];

            cfg = cfg || {};

            if (cfg.hasOwnProperty("agent")) {
                if (cfg.agent instanceof TinCan.Agent) {
                    this.agent = cfg.agent;
                }
                else {
                    this.agent = new TinCan.Agent (cfg.agent);
                }
            }

            for (i = 0; i < directProps.length; i += 1) {
                if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) {
                    this[directProps[i]] = cfg[directProps[i]];
                }
            }

            this.updated = false;
        }
    };

    /**
    @method fromJSON
    @return {Object} AgentProfile
    @static
    */
    AgentProfile.fromJSON = function (stateJSON) {
        AgentProfile.prototype.log("fromJSON");
        var _state = JSON.parse(stateJSON);

        return new AgentProfile(_state);
    };
}());

/*
    Copyright 2014 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.About
**/
(function () {
    "use strict";

    /**
    @class TinCan.About
    @constructor
    */
    var About = TinCan.About = function (cfg) {
        this.log("constructor");

        /**
        @property version
        @type {String[]}
        */
        this.version = null;

        this.init(cfg);
    };
    About.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "About",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");
            var i,
                directProps = [
                    "version"
                ];

            cfg = cfg || {};

            for (i = 0; i < directProps.length; i += 1) {
                if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) {
                    this[directProps[i]] = cfg[directProps[i]];
                }
            }
        }
    };

    /**
    @method fromJSON
    @return {Object} About
    @static
    */
    About.fromJSON = function (aboutJSON) {
        About.prototype.log("fromJSON");
        var _about = JSON.parse(aboutJSON);

        return new About(_about);
    };
}());

/*
    Copyright 2016 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.Attachment
**/
(function () {
    "use strict";

    /**
    @class TinCan.Attachment
    @constructor
    */
    var Attachment = TinCan.Attachment = function (cfg) {
        this.log("constructor");

        /**
        @property usageType
        @type String
        */
        this.usageType = null;

        /**
        @property display
        @type Object
        */
        this.display = null;

        /**
        @property contentType
        @type String
        */
        this.contentType = null;

        /**
        @property length
        @type int
        */
        this.length = null;

        /**
        @property sha2
        @type String
        */
        this.sha2 = null;

        /**
        @property description
        @type Object
        */
        this.description = null;

        /**
        @property fileUrl
        @type String
        */
        this.fileUrl = null;

        /**
        @property content
        @type ArrayBuffer
        */
        this.content = null;

        this.init(cfg);
    };
    Attachment.prototype = {
        /**
        @property LOG_SRC
        */
        LOG_SRC: "Attachment",

        /**
        @method log
        */
        log: TinCan.prototype.log,

        /**
        @method init
        @param {Object} [options] Configuration used to initialize
        */
        init: function (cfg) {
            this.log("init");
            var i,
                directProps = [
                    "contentType",
                    "length",
                    "sha2",
                    "usageType",
                    "display",
                    "description",
                    "fileUrl"
                ]
            ;

            cfg = cfg || {};

            for (i = 0; i < directProps.length; i += 1) {
                if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) {
                    this[directProps[i]] = cfg[directProps[i]];
                }
            }

            if (cfg.hasOwnProperty("content") && cfg.content !== null) {
                if (typeof cfg.content === "string") {
                    this.setContentFromString(cfg.content);
                }
                else {
                    this.setContent(cfg.content);
                }
            }
        },

        /**
        @method asVersion
        @param {String} [version] Version to return (defaults to newest supported)
        */
        asVersion: function (version) {
            this.log("asVersion");
            var result;

            version = version || TinCan.versions()[0];

            if (version === "0.9" || version === "0.95") {
                result = null;
            }
            else {
                result = {
                    contentType: this.contentType,
                    display: this.display,
                    length: this.length,
                    sha2: this.sha2,
                    usageType: this.usageType
                };

                if (this.fileUrl !== null) {
                    result.fileUrl = this.fileUrl;
                }
                if (this.description !== null) {
                    result.description = this.description;
                }
            }

            return result;
        },

        /**
        See {{#crossLink "TinCan.Utils/getLangDictionaryValue"}}{{/crossLink}}

        @method getLangDictionaryValue
        */
        getLangDictionaryValue: TinCan.Utils.getLangDictionaryValue,

        /**
        @method setContent
        @param {ArrayBuffer} content Sets content from ArrayBuffer
        */
        setContent: function (content) {
            this.content = content;
            this.length = content.byteLength;
            this.sha2 = TinCan.Utils.getSHA256String(content);
        },

        /**
        @method setContentFromString
        @param {String} content Sets the content property of the attachment from a string
        */
        setContentFromString: function (content) {
            var _content = content;

            _content = TinCan.Utils.stringToArrayBuffer(content);

            this.setContent(_content);
        },

        /**
        @method getContentAsString
        @return {String} Value of content property as a string
        */
        getContentAsString: function () {
            return TinCan.Utils.stringFromArrayBuffer(this.content);
        }
    };

    /**
    @method fromJSON
    @return {Object} Attachment
    @static
    */
    Attachment.fromJSON = function (attachmentJSON) {
        Attachment.prototype.log("fromJSON");
        var _attachment = JSON.parse(attachmentJSON);

        return new Attachment(_attachment);
    };

    Attachment._defaultEncoding = "utf-8";
}());

/*
    Copyright 2012-2013 Rustici Software

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
TinCan client library

@module TinCan
@submodule TinCan.Environment.Browser
**/
(function () {
    /* globals window, XMLHttpRequest, XDomainRequest, Blob */
    "use strict";
    var LOG_SRC = "Environment.Browser",
        requestComplete,
        __IEModeConversion,
        nativeRequest,
        xdrRequest,
        __createJSONSegment,
        __createAttachmentSegment,
        __delay,
        env = {},
        log = TinCan.prototype.log;

    if (typeof window === "undefined") {
        log("'window' not defined", LOG_SRC);
        return;
    }

    /* Shims for browsers not supporting our needs, mainly IE */

    //
    // Make JSON safe for IE6
    // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/JSON#Browser_compatibility
    //
    if (!window.JSON) {
        window.JSON = {
            parse: function (sJSON) {
                /*jslint evil: true */
                return eval("(" + sJSON + ")");
            },
            stringify: function (vContent) {
                var sOutput = "",
                    nId,
                    sProp
                ;
                if (vContent instanceof Object) {
                    if (vContent.constructor === Array) {
                        for (nId = 0; nId < vContent.length; nId += 1) {
                            sOutput += this.stringify(vContent[nId]) + ",";
                        }
                        return "[" + sOutput.substr(0, sOutput.length - 1) + "]";
                    }
                    if (vContent.toString !== Object.prototype.toString) { return "\"" + vContent.toString().replace(/"/g, "\\$&") + "\""; }
                    for (sProp in vContent) {
                        if (vContent.hasOwnProperty(sProp)) {
                            sOutput += "\"" + sProp.replace(/"/g, "\\$&") + "\":" + this.stringify(vContent[sProp]) + ",";
                        }
                    }
                    return "{" + sOutput.substr(0, sOutput.length - 1) + "}";
                }
                return typeof vContent === "string" ? "\"" + vContent.replace(/"/g, "\\$&") + "\"" : String(vContent);
            }
        };
    }

    //
    // Make Date.now safe for IE < 9
    // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/now
    //
    if (!Date.now) {
        Date.now = function () {
            return +(new Date ());
        };
    }

    //
    // Add .forEach implementation for supporting our string encoding polyfill
    // imported from js-polyfills to avoid bringing in the whole es5 shim
    // for now, a rewrite probably moves all shims out of the main build or at
    // least Environment file and leverages more of them
    //

    // ES5 15.4.4.18 Array.prototype.forEach ( callbackfn [ , thisArg ] )
    // From https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach
    if (!Array.prototype.forEach) {
      /* jshint freeze:false,bitwise:false */
      Array.prototype.forEach = function (fun /*, thisp */) {
        if (this === void 0 || this === null) { throw new TypeError(); }

        var t = Object(this);
        var len = t.length >>> 0;
        if (typeof fun !== "function") { throw new TypeError(); }

        var thisp = arguments[1], i;
        for (i = 0; i < len; i += 1) {
          if (i in t) {
            fun.call(thisp, t[i], i, t);
          }
        }
      };
    }

    /* Detect CORS and XDR support */
    env.hasCORS = false;
    env.useXDR = false;

    if (typeof XMLHttpRequest !== "undefined" && typeof (new XMLHttpRequest()).withCredentials !== "undefined") {
        env.hasCORS = true;
    }
    else if (typeof XDomainRequest !== "undefined") {
        env.hasCORS = true;
        env.useXDR = true;
    }

    // TODO: should we have our own internal "Request" object
    //       that replaces the need for "control"?

    //
    // Setup request callback
    //
    requestComplete = function (xhr, cfg, control) {
        log("requestComplete: " + control.finished + ", xhr.status: " + xhr.status, LOG_SRC);
        var requestCompleteResult,
            notFoundOk,
            httpStatus;

        //
        // XDomainRequest doesn't give us a way to get the status,
        // so allow passing in a forged one
        //
        if (typeof xhr.status === "undefined") {
            httpStatus = control.fakeStatus;
        }
        else {
            //
            // older versions of IE don't properly handle 204 status codes
            // so correct when receiving a 1223 to be 204 locally
            // http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request
            //
            httpStatus = (xhr.status === 1223) ? 204 : xhr.status;
        }

        if (! control.finished) {
            // may be in sync or async mode, using XMLHttpRequest or IE XDomainRequest, onreadystatechange or
            // onload or both might fire depending upon browser, just covering all bases with event hooks and
            // using 'finished' flag to avoid triggering events multiple times
            control.finished = true;

            notFoundOk = (cfg.ignore404 && httpStatus === 404);
            if ((httpStatus >= 200 && httpStatus < 400) || notFoundOk) {
                if (cfg.callback) {
                    cfg.callback(null, xhr);
                }
                else {
                    requestCompleteResult = {
                        err: null,
                        xhr: xhr
                    };
                    return requestCompleteResult;
                }
            }
            else {
                requestCompleteResult = {
                    err: httpStatus,
                    xhr: xhr
                };
                if (httpStatus === 0) {
                    log("[warning] There was a problem communicating with the Learning Record Store. Aborted, offline, or invalid CORS endpoint (" + httpStatus + ")", LOG_SRC);
                }
                else {
                    log("[warning] There was a problem communicating with the Learning Record Store. (" + httpStatus + " | " + xhr.responseText+ ")", LOG_SRC);
                }
                if (cfg.callback) {
                    cfg.callback(httpStatus, xhr);
                }
                return requestCompleteResult;
            }
        }
        else {
            return requestCompleteResult;
        }
    };

    //
    // Converts an HTTP request cfg of above a set length (//MAX_REQUEST_LENGTH) to a post
    // request cfg, with the original request as the form data.
    //
    __IEModeConversion = function (fullUrl, headers, pairs, cfg) {
        var prop;

        // 'pairs' already holds the original cfg params, now needs headers and data
        // from the original cfg to add as the form data to the POST request
        for (prop in headers) {
            if (headers.hasOwnProperty(prop)) {
                pairs.push(prop + "=" + encodeURIComponent(headers[prop]));
            }
        }

        if (typeof cfg.data !== "undefined") {
            pairs.push("content=" + encodeURIComponent(cfg.data));
        }

        // the Authorization and xAPI version headers need to still be present, but
        // the content type must exist and be of type application/x-www-form-urlencoded
        headers["Content-Type"] = "application/x-www-form-urlencoded";
        fullUrl += "?method=" + cfg.method;
        cfg.method = "POST";
        cfg.params = {};
        if (pairs.length > 0) {
            cfg.data = pairs.join("&");
        }
        return fullUrl;
    };

    //
    // one of the two of these is stuffed into the LRS' instance
    // as ._makeRequest
    //
    nativeRequest = function (fullUrl, headers, cfg) {
        /*global ActiveXObject*/
        log("sendRequest using XMLHttpRequest", LOG_SRC);
        var self = this,
            xhr,
            prop,
            pairs = [],
            data,
            control = {
                finished: false,
                fakeStatus: null
            },
            async = typeof cfg.callback !== "undefined",
            fullRequest = fullUrl,
            err,
            MAX_REQUEST_LENGTH = 2048
        ;
        log("sendRequest using XMLHttpRequest - async: " + async, LOG_SRC);

        for (prop in cfg.params) {
            if (cfg.params.hasOwnProperty(prop)) {
                pairs.push(prop + "=" + encodeURIComponent(cfg.params[prop]));
            }
        }

        if (pairs.length > 0) {
            fullRequest += "?" + pairs.join("&");
        }

        if (fullRequest.length >= MAX_REQUEST_LENGTH) {
            if (typeof cfg.method === "undefined") {
                err = new Error("method must not be undefined for an IE Mode Request conversion");
                if (typeof cfg.callback !== "undefined") {
                    cfg.callback(err, null);
                }
                return {
                    err: err,
                    xhr: null
                };
            }

            fullUrl = __IEModeConversion(fullUrl, headers, pairs, cfg);
        }
        else {
            fullUrl = fullRequest;
        }

        if (typeof XMLHttpRequest !== "undefined") {
            xhr = new XMLHttpRequest();
        }
        else {
            //
            // IE6 implements XMLHttpRequest through ActiveX control
            // http://blogs.msdn.com/b/ie/archive/2006/01/23/516393.aspx
            //
            xhr = new ActiveXObject("Microsoft.XMLHTTP");

            if (cfg.expectMultipart) {
                err = new Error("Attachment support not available");
                if (typeof cfg.callback !== "undefined") {
                    cfg.callback(err, null);
                }
                return {
                    err: err,
                    xhr: null
                };
            }
        }

        xhr.open(cfg.method, fullUrl, async);

        //
        // setting the .responseType before .open was causing IE to fail
        // with a StateError, so moved it to here
        //
        if (cfg.expectMultipart) {
            xhr.responseType = "arraybuffer";
        }

        for (prop in headers) {
            if (headers.hasOwnProperty(prop)) {
                xhr.setRequestHeader(prop, headers[prop]);
            }
        }

        data = cfg.data;

        if (async) {
            xhr.onreadystatechange = function () {
                log("xhr.onreadystatechange - xhr.readyState: " + xhr.readyState, LOG_SRC);
                if (xhr.readyState === 4) {
                    requestComplete.call(self, xhr, cfg, control);
                }
            };
        }

        //
        // research indicates that IE is known to just throw exceptions
        // on .send and it seems everyone pretty much just ignores them
        // including jQuery (https://github.com/jquery/jquery/blob/1.10.2/src/ajax.js#L549
        // https://github.com/jquery/jquery/blob/1.10.2/src/ajax/xhr.js#L97)
        //
        try {
            xhr.send(data);
        }
        catch (ex) {
            log("sendRequest caught send exception: " + ex, LOG_SRC);
        }

        if (async) {
            //
            // for async requests give them the XHR object directly
            // as the return value, the actual stuff they should be
            // caring about is params to the callback, for sync
            // requests they got the return value above
            //
            return xhr;
        }

        return requestComplete.call(this, xhr, cfg, control);
    };
    xdrRequest = function (fullUrl, headers, cfg) {
        log("sendRequest using XDomainRequest", LOG_SRC);
        var self = this,
            xhr,
            pairs = [],
            data,
            prop,
            until,
            control = {
                finished: false,
                fakeStatus: null
            },
            err;

        if (cfg.expectMultipart) {
            err = new Error("Attachment support not available");
            if (typeof cfg.callback !== "undefined") {
                cfg.callback(err, null);
            }
            return {
                err: err,
                xhr: null
            };
        }

        // method has to go on querystring, and nothing else,
        // and the actual method is then always POST
        fullUrl += "?method=" + cfg.method;

        // params end up in the body
        for (prop in cfg.params) {
            if (cfg.params.hasOwnProperty(prop)) {
                pairs.push(prop + "=" + encodeURIComponent(cfg.params[prop]));
            }
        }

        // headers go into form data
        for (prop in headers) {
            if (headers.hasOwnProperty(prop)) {
                pairs.push(prop + "=" + encodeURIComponent(headers[prop]));
            }
        }

        // the original data is repackaged as "content" form var
        if (typeof cfg.data !== "undefined") {
            pairs.push("content=" + encodeURIComponent(cfg.data));
        }

        data = pairs.join("&");

        xhr = new XDomainRequest ();
        xhr.open("POST", fullUrl);

        if (! cfg.callback) {
            xhr.onload = function () {
                control.fakeStatus = 200;
            };
            xhr.onerror = function () {
                control.fakeStatus = 400;
            };
            xhr.ontimeout = function () {
                control.fakeStatus = 0;
            };
        }
        else {
            xhr.onload = function () {
                control.fakeStatus = 200;
                requestComplete.call(self, xhr, cfg, control);
            };
            xhr.onerror = function () {
                control.fakeStatus = 400;
                requestComplete.call(self, xhr, cfg, control);
            };
            xhr.ontimeout = function () {
                control.fakeStatus = 0;
                requestComplete.call(self, xhr, cfg, control);
            };
        }

        // IE likes to randomly abort requests when some handlers
        // aren't defined, so define them with no-ops, see:
        //
        // http://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/
        // http://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified
        //
        xhr.onprogress = function () {};
        xhr.timeout = 0;

        //
        // research indicates that IE is known to just throw exceptions
        // on .send and it seems everyone pretty much just ignores them
        // including jQuery (https://github.com/jquery/jquery/blob/1.10.2/src/ajax.js#L549
        // https://github.com/jquery/jquery/blob/1.10.2/src/ajax/xhr.js#L97)
        //
        try {
            xhr.send(data);
        }
        catch (ex) {
            log("sendRequest caught send exception: " + ex, LOG_SRC);
        }

        if (! cfg.callback) {
            // synchronous call in IE, with no synchronous mode available
            until = 10000 + Date.now();
            log("sendRequest - until: " + until + ", finished: " + control.finished, LOG_SRC);

            while (Date.now() < until && control.fakeStatus === null) {
                //log("calling __delay", LOG_SRC);
                __delay();
            }
            return requestComplete.call(self, xhr, cfg, control);
        }

        //
        // for async requests give them the XHR object directly
        // as the return value, the actual stuff they should be
        // caring about is params to the callback, for sync
        // requests they got the return value above
        //
        return xhr;
    };

    //
    // Override LRS' init method to set up our request handling
    // capabilities
    //
    TinCan.LRS.prototype._initByEnvironment = function (cfg) {
        /*jslint regexp: true, laxbreak: true */
        /* globals location */
        log("_initByEnvironment", LOG_SRC);
        var urlParts,
            schemeMatches,
            locationPort,
            isXD
        ;

        cfg = cfg || {};

        //
        // default to native request mode
        //
        this._makeRequest = nativeRequest;

        //
        // overload LRS ._IEModeConversion to be able to test this method,
        // which only applies in a browser setting
        //
        this._IEModeConversion = __IEModeConversion;

        urlParts = this.endpoint.toLowerCase().match(/([A-Za-z]+:)\/\/([^:\/]+):?(\d+)?(\/.*)?$/);
        if (urlParts === null) {
            log("[error] LRS invalid: failed to divide URL parts", LOG_SRC);
            throw {
                code: 4,
                mesg: "LRS invalid: failed to divide URL parts"
            };
        }

        //
        // determine whether this is a cross domain request,
        // whether our browser has CORS support at all, and then
        // if it does then if we are in IE with XDR only check that
        // the schemes match to see if we should be able to talk to
        // the LRS
        //
        locationPort = location.port;
        schemeMatches = location.protocol.toLowerCase() === urlParts[1];

        //
        // normalize the location.port cause it appears to be "" when 80/443
        // but our endpoint may have provided it
        //
        if (locationPort === "") {
            locationPort = (location.protocol.toLowerCase() === "http:" ? "80" : (location.protocol.toLowerCase() === "https:" ? "443" : ""));
        }

        isXD = (
            // is same scheme?
            ! schemeMatches

            // is same host?
            || location.hostname.toLowerCase() !== urlParts[2]

            // is same port?
            || locationPort !== (
                (urlParts[3] !== null && typeof urlParts[3] !== "undefined" && urlParts[3] !== "") ? urlParts[3] : (urlParts[1] === "http:" ? "80" : (urlParts[1] === "https:" ? "443" : ""))
            )
        );
        if (isXD) {
            if (env.hasCORS) {
                if (env.useXDR && schemeMatches) {
                    this._makeRequest = xdrRequest;
                }
                else if (env.useXDR && ! schemeMatches) {
                    if (cfg.allowFail) {
                        log("[warning] LRS invalid: cross domain request for differing scheme in IE with XDR (allowed to fail)", LOG_SRC);
                    }
                    else {
                        log("[error] LRS invalid: cross domain request for differing scheme in IE with XDR", LOG_SRC);
                        throw {
                            code: 2,
                            mesg: "LRS invalid: cross domain request for differing scheme in IE with XDR"
                        };
                    }
                }
            }
            else {
                if (cfg.allowFail) {
                    log("[warning] LRS invalid: cross domain requests not supported in this browser (allowed to fail)", LOG_SRC);
                }
                else {
                    log("[error] LRS invalid: cross domain requests not supported in this browser", LOG_SRC);
                    throw {
                        code: 1,
                        mesg: "LRS invalid: cross domain requests not supported in this browser"
                    };
                }
            }
        }
    };

    /**
    Non-environment safe method used to create a delay to give impression
    of synchronous response (for IE, shocker)

    @method __delay
    @private
    */
    __delay = function () {
        //
        // use a synchronous request to the current location to allow the browser
        // to yield to the asynchronous request's events but still block in the
        // outer loop to make it seem synchronous to the end user
        //
        // removing this made the while loop too tight to allow the asynchronous
        // events through to get handled so that the response was correctly handled
        //
        var xhr = new XMLHttpRequest (),
            url = window.location + "?forcenocache=" + TinCan.Utils.getUUID()
        ;
        xhr.open("GET", url, false);
        xhr.send(null);
    };

    //
    // Synchronous xhr handling is accepted in the browser environment
    //
    TinCan.LRS.syncEnabled = true;

    TinCan.LRS.prototype._getMultipartRequestData = function (boundary, jsonContent, requestAttachments) {
        var parts = [],
            i;

        parts.push(
            __createJSONSegment(
                boundary,
                jsonContent
            )
        );
        for (i = 0; i < requestAttachments.length; i += 1) {
            if (requestAttachments[i].content !== null) {
                parts.push(
                    __createAttachmentSegment(
                        boundary,
                        requestAttachments[i].content,
                        requestAttachments[i].sha2,
                        requestAttachments[i].contentType
                    )
                );
            }
        }
        parts.push("\r\n--" + boundary + "--\r\n");

        return new Blob(parts);
    };

    __createJSONSegment = function (boundary, jsonContent) {
        var content = [
                "--" + boundary,
                "Content-Type: application/json",
                "",
                JSON.stringify(jsonContent)
            ].join("\r\n");

        content += "\r\n";

        return content;
    };

    __createAttachmentSegment = function (boundary, content, sha2, contentType) {
        var blobParts = [],
            header = [
                "--" + boundary,
                "Content-Type: " + contentType,
                "Content-Transfer-Encoding: binary",
                "X-Experience-API-Hash: " + sha2
            ].join("\r\n");

        header += "\r\n\r\n";

        blobParts.push(header);
        blobParts.push(content);

        return new Blob(blobParts);
    };

    TinCan.Utils.stringToArrayBuffer = function (content, encoding) {
        /* global TextEncoder */
        var encoder;

        if (! encoding) {
            encoding = TinCan.Utils.defaultEncoding;
        }

        encoder = new TextEncoder(encoding);

        return encoder.encode(content).buffer;
    };

    TinCan.Utils.stringFromArrayBuffer = function (content, encoding) {
        /* global TextDecoder */
        var decoder;

        if (! encoding) {
            encoding = TinCan.Utils.defaultEncoding;
        }

        decoder = new TextDecoder(encoding);

        return decoder.decode(content);
    };
}());

/*
 Copyright (c) 2010, Linden Research, Inc.
 Copyright (c) 2014, Joshua Bell

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in
 all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 $/LicenseInfo$
 */

// Original can be found at:
//   https://bitbucket.org/lindenlab/llsd
// Modifications by Joshua Bell inexorabletash@gmail.com
//   https://github.com/inexorabletash/polyfill

// ES3/ES5 implementation of the Krhonos Typed Array Specification
//   Ref: http://www.khronos.org/registry/typedarray/specs/latest/
//   Date: 2011-02-01
//
// Variations:
//  * Allows typed_array.get/set() as alias for subscripts (typed_array[])
//  * Gradually migrating structure from Khronos spec to ES2015 spec
//
// Caveats:
//  * Beyond 10000 or so entries, polyfilled array accessors (ta[0],
//    etc) become memory-prohibitive, so array creation will fail. Set
//    self.TYPED_ARRAY_POLYFILL_NO_ARRAY_ACCESSORS=true to disable
//    creation of accessors. Your code will need to use the
//    non-standard get()/set() instead, and will need to add those to
//    native arrays for interop.
(function(global) {
  'use strict';
  var undefined = (void 0); // Paranoia

  // Beyond this value, index getters/setters (i.e. array[0], array[1]) are so slow to
  // create, and consume so much memory, that the browser appears frozen.
  var MAX_ARRAY_LENGTH = 1e5;

  // Approximations of internal ECMAScript conversion functions
  function Type(v) {
    switch(typeof v) {
    case 'undefined': return 'undefined';
    case 'boolean': return 'boolean';
    case 'number': return 'number';
    case 'string': return 'string';
    default: return v === null ? 'null' : 'object';
    }
  }

  // Class returns internal [[Class]] property, used to avoid cross-frame instanceof issues:
  function Class(v) { return Object.prototype.toString.call(v).replace(/^\[object *|\]$/g, ''); }
  function IsCallable(o) { return typeof o === 'function'; }
  function ToObject(v) {
    if (v === null || v === undefined) throw TypeError();
    return Object(v);
  }
  function ToInt32(v) { return v >> 0; }
  function ToUint32(v) { return v >>> 0; }

  // Snapshot intrinsics
  var LN2 = Math.LN2,
      abs = Math.abs,
      floor = Math.floor,
      log = Math.log,
      max = Math.max,
      min = Math.min,
      pow = Math.pow,
      round = Math.round;

  // emulate ES5 getter/setter API using legacy APIs
  // http://blogs.msdn.com/b/ie/archive/2010/09/07/transitioning-existing-code-to-the-es5-getter-setter-apis.aspx
  // (second clause tests for Object.defineProperty() in IE<9 that only supports extending DOM prototypes, but
  // note that IE<9 does not support __defineGetter__ or __defineSetter__ so it just renders the method harmless)

  (function() {
    var orig = Object.defineProperty;
    var dom_only = !(function(){try{return Object.defineProperty({},'x',{});}catch(_){return false;}}());

    if (!orig || dom_only) {
      Object.defineProperty = function (o, prop, desc) {
        // In IE8 try built-in implementation for defining properties on DOM prototypes.
        if (orig)
          try { return orig(o, prop, desc); } catch (_) {}
        if (o !== Object(o))
          throw TypeError('Object.defineProperty called on non-object');
        if (Object.prototype.__defineGetter__ && ('get' in desc))
          Object.prototype.__defineGetter__.call(o, prop, desc.get);
        if (Object.prototype.__defineSetter__ && ('set' in desc))
          Object.prototype.__defineSetter__.call(o, prop, desc.set);
        if ('value' in desc)
          o[prop] = desc.value;
        return o;
      };
    }
  }());

  // ES5: Make obj[index] an alias for obj._getter(index)/obj._setter(index, value)
  // for index in 0 ... obj.length
  function makeArrayAccessors(obj) {
    if ('TYPED_ARRAY_POLYFILL_NO_ARRAY_ACCESSORS' in global)
      return;

    if (obj.length > MAX_ARRAY_LENGTH) throw RangeError('Array too large for polyfill');

    function makeArrayAccessor(index) {
      Object.defineProperty(obj, index, {
        'get': function() { return obj._getter(index); },
        'set': function(v) { obj._setter(index, v); },
        enumerable: true,
        configurable: false
      });
    }

    var i;
    for (i = 0; i < obj.length; i += 1) {
      makeArrayAccessor(i);
    }
  }

  // Internal conversion functions:
  //    pack<Type>()   - take a number (interpreted as Type), output a byte array
  //    unpack<Type>() - take a byte array, output a Type-like number

  function as_signed(value, bits) { var s = 32 - bits; return (value << s) >> s; }
  function as_unsigned(value, bits) { var s = 32 - bits; return (value << s) >>> s; }

  function packI8(n) { return [n & 0xff]; }
  function unpackI8(bytes) { return as_signed(bytes[0], 8); }

  function packU8(n) { return [n & 0xff]; }
  function unpackU8(bytes) { return as_unsigned(bytes[0], 8); }

  function packU8Clamped(n) { n = round(Number(n)); return [n < 0 ? 0 : n > 0xff ? 0xff : n & 0xff]; }

  function packI16(n) { return [n & 0xff, (n >> 8) & 0xff]; }
  function unpackI16(bytes) { return as_signed(bytes[1] << 8 | bytes[0], 16); }

  function packU16(n) { return [n & 0xff, (n >> 8) & 0xff]; }
  function unpackU16(bytes) { return as_unsigned(bytes[1] << 8 | bytes[0], 16); }

  function packI32(n) { return [n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]; }
  function unpackI32(bytes) { return as_signed(bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0], 32); }

  function packU32(n) { return [n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]; }
  function unpackU32(bytes) { return as_unsigned(bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0], 32); }

  function packIEEE754(v, ebits, fbits) {

    var bias = (1 << (ebits - 1)) - 1;

    function roundToEven(n) {
      var w = floor(n), f = n - w;
      if (f < 0.5)
        return w;
      if (f > 0.5)
        return w + 1;
      return w % 2 ? w + 1 : w;
    }

    // Compute sign, exponent, fraction
    var s, e, f;
    if (v !== v) {
      // NaN
      // http://dev.w3.org/2006/webapi/WebIDL/#es-type-mapping
      e = (1 << ebits) - 1; f = pow(2, fbits - 1); s = 0;
    } else if (v === Infinity || v === -Infinity) {
      e = (1 << ebits) - 1; f = 0; s = (v < 0) ? 1 : 0;
    } else if (v === 0) {
      e = 0; f = 0; s = (1 / v === -Infinity) ? 1 : 0;
    } else {
      s = v < 0;
      v = abs(v);

      if (v >= pow(2, 1 - bias)) {
        // Normalized
        e = min(floor(log(v) / LN2), 1023);
        var significand = v / pow(2, e);
        if (significand < 1) {
          e -= 1;
          significand *= 2;
        }
        if (significand >= 2) {
          e += 1;
          significand /= 2;
        }
        var d = pow(2, fbits);
        f = roundToEven(significand * d) - d;
        e += bias;
        if (f / d >= 1) {
          e += 1;
          f = 0;
        }
        if (e > 2 * bias) {
          // Overflow
          e = (1 << ebits) - 1;
          f = 0;
        }
      } else {
        // Denormalized
        e = 0;
        f = roundToEven(v / pow(2, 1 - bias - fbits));
      }
    }

    // Pack sign, exponent, fraction
    var bits = [], i;
    for (i = fbits; i; i -= 1) { bits.push(f % 2 ? 1 : 0); f = floor(f / 2); }
    for (i = ebits; i; i -= 1) { bits.push(e % 2 ? 1 : 0); e = floor(e / 2); }
    bits.push(s ? 1 : 0);
    bits.reverse();
    var str = bits.join('');

    // Bits to bytes
    var bytes = [];
    while (str.length) {
      bytes.unshift(parseInt(str.substring(0, 8), 2));
      str = str.substring(8);
    }
    return bytes;
  }

  function unpackIEEE754(bytes, ebits, fbits) {
    // Bytes to bits
    var bits = [], i, j, b, str,
        bias, s, e, f;

    for (i = 0; i < bytes.length; ++i) {
      b = bytes[i];
      for (j = 8; j; j -= 1) {
        bits.push(b % 2 ? 1 : 0); b = b >> 1;
      }
    }
    bits.reverse();
    str = bits.join('');

    // Unpack sign, exponent, fraction
    bias = (1 << (ebits - 1)) - 1;
    s = parseInt(str.substring(0, 1), 2) ? -1 : 1;
    e = parseInt(str.substring(1, 1 + ebits), 2);
    f = parseInt(str.substring(1 + ebits), 2);

    // Produce number
    if (e === (1 << ebits) - 1) {
      return f !== 0 ? NaN : s * Infinity;
    } else if (e > 0) {
      // Normalized
      return s * pow(2, e - bias) * (1 + f / pow(2, fbits));
    } else if (f !== 0) {
      // Denormalized
      return s * pow(2, -(bias - 1)) * (f / pow(2, fbits));
    } else {
      return s < 0 ? -0 : 0;
    }
  }

  function unpackF64(b) { return unpackIEEE754(b, 11, 52); }
  function packF64(v) { return packIEEE754(v, 11, 52); }
  function unpackF32(b) { return unpackIEEE754(b, 8, 23); }
  function packF32(v) { return packIEEE754(v, 8, 23); }

  //
  // 3 The ArrayBuffer Type
  //

  (function() {

    function ArrayBuffer(length) {
      length = ToInt32(length);
      if (length < 0) throw RangeError('ArrayBuffer size is not a small enough positive integer.');
      Object.defineProperty(this, 'byteLength', {value: length});
      Object.defineProperty(this, '_bytes', {value: Array(length)});

      for (var i = 0; i < length; i += 1)
        this._bytes[i] = 0;
    }

    global.ArrayBuffer = global.ArrayBuffer || ArrayBuffer;

    //
    // 5 The Typed Array View Types
    //

    function $TypedArray$() {

      // %TypedArray% ( length )
      if (!arguments.length || typeof arguments[0] !== 'object') {
        return (function(length) {
          length = ToInt32(length);
          if (length < 0) throw RangeError('length is not a small enough positive integer.');
          Object.defineProperty(this, 'length', {value: length});
          Object.defineProperty(this, 'byteLength', {value: length * this.BYTES_PER_ELEMENT});
          Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(this.byteLength)});
          Object.defineProperty(this, 'byteOffset', {value: 0});

         }).apply(this, arguments);
      }

      // %TypedArray% ( typedArray )
      if (arguments.length >= 1 &&
          Type(arguments[0]) === 'object' &&
          arguments[0] instanceof $TypedArray$) {
        return (function(typedArray){
          if (this.constructor !== typedArray.constructor) throw TypeError();

          var byteLength = typedArray.length * this.BYTES_PER_ELEMENT;
          Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(byteLength)});
          Object.defineProperty(this, 'byteLength', {value: byteLength});
          Object.defineProperty(this, 'byteOffset', {value: 0});
          Object.defineProperty(this, 'length', {value: typedArray.length});

          for (var i = 0; i < this.length; i += 1)
            this._setter(i, typedArray._getter(i));

        }).apply(this, arguments);
      }

      // %TypedArray% ( array )
      if (arguments.length >= 1 &&
          Type(arguments[0]) === 'object' &&
          !(arguments[0] instanceof $TypedArray$) &&
          !(arguments[0] instanceof ArrayBuffer || Class(arguments[0]) === 'ArrayBuffer')) {
        return (function(array) {

          var byteLength = array.length * this.BYTES_PER_ELEMENT;
          Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(byteLength)});
          Object.defineProperty(this, 'byteLength', {value: byteLength});
          Object.defineProperty(this, 'byteOffset', {value: 0});
          Object.defineProperty(this, 'length', {value: array.length});

          for (var i = 0; i < this.length; i += 1) {
            var s = array[i];
            this._setter(i, Number(s));
          }
        }).apply(this, arguments);
      }

      // %TypedArray% ( buffer, byteOffset=0, length=undefined )
      if (arguments.length >= 1 &&
          Type(arguments[0]) === 'object' &&
          (arguments[0] instanceof ArrayBuffer || Class(arguments[0]) === 'ArrayBuffer')) {
        return (function(buffer, byteOffset, length) {

          byteOffset = ToUint32(byteOffset);
          if (byteOffset > buffer.byteLength)
            throw RangeError('byteOffset out of range');

          // The given byteOffset must be a multiple of the element
          // size of the specific type, otherwise an exception is raised.
          if (byteOffset % this.BYTES_PER_ELEMENT)
            throw RangeError('buffer length minus the byteOffset is not a multiple of the element size.');

          if (length === undefined) {
            var byteLength = buffer.byteLength - byteOffset;
            if (byteLength % this.BYTES_PER_ELEMENT)
              throw RangeError('length of buffer minus byteOffset not a multiple of the element size');
            length = byteLength / this.BYTES_PER_ELEMENT;

          } else {
            length = ToUint32(length);
            byteLength = length * this.BYTES_PER_ELEMENT;
          }

          if ((byteOffset + byteLength) > buffer.byteLength)
            throw RangeError('byteOffset and length reference an area beyond the end of the buffer');

          Object.defineProperty(this, 'buffer', {value: buffer});
          Object.defineProperty(this, 'byteLength', {value: byteLength});
          Object.defineProperty(this, 'byteOffset', {value: byteOffset});
          Object.defineProperty(this, 'length', {value: length});

        }).apply(this, arguments);
      }

      // %TypedArray% ( all other argument combinations )
      throw TypeError();
    }

    // Properties of the %TypedArray Instrinsic Object

    // %TypedArray%.from ( source , mapfn=undefined, thisArg=undefined )
    Object.defineProperty($TypedArray$, 'from', {value: function(iterable) {
      return new this(iterable);
    }});

    // %TypedArray%.of ( ...items )
    Object.defineProperty($TypedArray$, 'of', {value: function(/*...items*/) {
      return new this(arguments);
    }});

    // %TypedArray%.prototype
    var $TypedArrayPrototype$ = {};
    $TypedArray$.prototype = $TypedArrayPrototype$;

    // WebIDL: getter type (unsigned long index);
    Object.defineProperty($TypedArray$.prototype, '_getter', {value: function(index) {
      if (arguments.length < 1) throw SyntaxError('Not enough arguments');

      index = ToUint32(index);
      if (index >= this.length)
        return undefined;

      var bytes = [], i, o;
      for (i = 0, o = this.byteOffset + index * this.BYTES_PER_ELEMENT;
           i < this.BYTES_PER_ELEMENT;
           i += 1, o += 1) {
        bytes.push(this.buffer._bytes[o]);
      }
      return this._unpack(bytes);
    }});

    // NONSTANDARD: convenience alias for getter: type get(unsigned long index);
    Object.defineProperty($TypedArray$.prototype, 'get', {value: $TypedArray$.prototype._getter});

    // WebIDL: setter void (unsigned long index, type value);
    Object.defineProperty($TypedArray$.prototype, '_setter', {value: function(index, value) {
      if (arguments.length < 2) throw SyntaxError('Not enough arguments');

      index = ToUint32(index);
      if (index >= this.length)
        return;

      var bytes = this._pack(value), i, o;
      for (i = 0, o = this.byteOffset + index * this.BYTES_PER_ELEMENT;
           i < this.BYTES_PER_ELEMENT;
           i += 1, o += 1) {
        this.buffer._bytes[o] = bytes[i];
      }
    }});

    // get %TypedArray%.prototype.buffer
    // get %TypedArray%.prototype.byteLength
    // get %TypedArray%.prototype.byteOffset
    // -- applied directly to the object in the constructor

    // %TypedArray%.prototype.constructor
    Object.defineProperty($TypedArray$.prototype, 'constructor', {value: $TypedArray$});

    // %TypedArray%.prototype.copyWithin (target, start, end = this.length )
    Object.defineProperty($TypedArray$.prototype, 'copyWithin', {value: function(target, start) {
      var end = arguments[2];

      var o = ToObject(this);
      var lenVal = o.length;
      var len = ToUint32(lenVal);
      len = max(len, 0);
      var relativeTarget = ToInt32(target);
      var to;
      if (relativeTarget < 0)
        to = max(len + relativeTarget, 0);
      else
        to = min(relativeTarget, len);
      var relativeStart = ToInt32(start);
      var from;
      if (relativeStart < 0)
        from = max(len + relativeStart, 0);
      else
        from = min(relativeStart, len);
      var relativeEnd;
      if (end === undefined)
        relativeEnd = len;
      else
        relativeEnd = ToInt32(end);
      var final;
      if (relativeEnd < 0)
        final = max(len + relativeEnd, 0);
      else
        final = min(relativeEnd, len);
      var count = min(final - from, len - to);
      var direction;
      if (from < to && to < from + count) {
        direction = -1;
        from = from + count - 1;
        to = to + count - 1;
      } else {
        direction = 1;
      }
      while (count > 0) {
        o._setter(to, o._getter(from));
        from = from + direction;
        to = to + direction;
        count = count - 1;
      }
      return o;
    }});

    // %TypedArray%.prototype.entries ( )
    // -- defined in es6.js to shim browsers w/ native TypedArrays

    // %TypedArray%.prototype.every ( callbackfn, thisArg = undefined )
    Object.defineProperty($TypedArray$.prototype, 'every', {value: function(callbackfn) {
      if (this === undefined || this === null) throw TypeError();
      var t = Object(this);
      var len = ToUint32(t.length);
      if (!IsCallable(callbackfn)) throw TypeError();
      var thisArg = arguments[1];
      for (var i = 0; i < len; i++) {
        if (!callbackfn.call(thisArg, t._getter(i), i, t))
          return false;
      }
      return true;
    }});

    // %TypedArray%.prototype.fill (value, start = 0, end = this.length )
    Object.defineProperty($TypedArray$.prototype, 'fill', {value: function(value) {
      var start = arguments[1],
          end = arguments[2];

      var o = ToObject(this);
      var lenVal = o.length;
      var len = ToUint32(lenVal);
      len = max(len, 0);
      var relativeStart = ToInt32(start);
      var k;
      if (relativeStart < 0)
        k = max((len + relativeStart), 0);
      else
        k = min(relativeStart, len);
      var relativeEnd;
      if (end === undefined)
        relativeEnd = len;
      else
        relativeEnd = ToInt32(end);
      var final;
      if (relativeEnd < 0)
        final = max((len + relativeEnd), 0);
      else
        final = min(relativeEnd, len);
      while (k < final) {
        o._setter(k, value);
        k += 1;
      }
      return o;
    }});

    // %TypedArray%.prototype.filter ( callbackfn, thisArg = undefined )
    Object.defineProperty($TypedArray$.prototype, 'filter', {value: function(callbackfn) {
      if (this === undefined || this === null) throw TypeError();
      var t = Object(this);
      var len = ToUint32(t.length);
      if (!IsCallable(callbackfn)) throw TypeError();
      var res = [];
      var thisp = arguments[1];
      for (var i = 0; i < len; i++) {
        var val = t._getter(i); // in case fun mutates this
        if (callbackfn.call(thisp, val, i, t))
          res.push(val);
      }
      return new this.constructor(res);
    }});

    // %TypedArray%.prototype.find (predicate, thisArg = undefined)
    Object.defineProperty($TypedArray$.prototype, 'find', {value: function(predicate) {
      var o = ToObject(this);
      var lenValue = o.length;
      var len = ToUint32(lenValue);
      if (!IsCallable(predicate)) throw TypeError();
      var t = arguments.length > 1 ? arguments[1] : undefined;
      var k = 0;
      while (k < len) {
        var kValue = o._getter(k);
        var testResult = predicate.call(t, kValue, k, o);
        if (Boolean(testResult))
          return kValue;
        ++k;
      }
      return undefined;
    }});

    // %TypedArray%.prototype.findIndex ( predicate, thisArg = undefined )
    Object.defineProperty($TypedArray$.prototype, 'findIndex', {value: function(predicate) {
      var o = ToObject(this);
      var lenValue = o.length;
      var len = ToUint32(lenValue);
      if (!IsCallable(predicate)) throw TypeError();
      var t = arguments.length > 1 ? arguments[1] : undefined;
      var k = 0;
      while (k < len) {
        var kValue = o._getter(k);
        var testResult = predicate.call(t, kValue, k, o);
        if (Boolean(testResult))
          return k;
        ++k;
      }
      return -1;
    }});

    // %TypedArray%.prototype.forEach ( callbackfn, thisArg = undefined )
    Object.defineProperty($TypedArray$.prototype, 'forEach', {value: function(callbackfn) {
      if (this === undefined || this === null) throw TypeError();
      var t = Object(this);
      var len = ToUint32(t.length);
      if (!IsCallable(callbackfn)) throw TypeError();
      var thisp = arguments[1];
      for (var i = 0; i < len; i++)
        callbackfn.call(thisp, t._getter(i), i, t);
    }});

    // %TypedArray%.prototype.indexOf (searchElement, fromIndex = 0 )
    Object.defineProperty($TypedArray$.prototype, 'indexOf', {value: function(searchElement) {
      if (this === undefined || this === null) throw TypeError();
      var t = Object(this);
      var len = ToUint32(t.length);
      if (len === 0) return -1;
      var n = 0;
      if (arguments.length > 0) {
        n = Number(arguments[1]);
        if (n !== n) {
          n = 0;
        } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) {
          n = (n > 0 || -1) * floor(abs(n));
        }
      }
      if (n >= len) return -1;
      var k = n >= 0 ? n : max(len - abs(n), 0);
      for (; k < len; k++) {
        if (t._getter(k) === searchElement) {
          return k;
        }
      }
      return -1;
    }});

    // %TypedArray%.prototype.join ( separator )
    Object.defineProperty($TypedArray$.prototype, 'join', {value: function(separator) {
      if (this === undefined || this === null) throw TypeError();
      var t = Object(this);
      var len = ToUint32(t.length);
      var tmp = Array(len);
      for (var i = 0; i < len; ++i)
        tmp[i] = t._getter(i);
      return tmp.join(separator === undefined ? ',' : separator); // Hack for IE7
    }});

    // %TypedArray%.prototype.keys ( )
    // -- defined in es6.js to shim browsers w/ native TypedArrays

    // %TypedArray%.prototype.lastIndexOf ( searchElement, fromIndex = this.length-1 )
    Object.defineProperty($TypedArray$.prototype, 'lastIndexOf', {value: function(searchElement) {
      if (this === undefined || this === null) throw TypeError();
      var t = Object(this);
      var len = ToUint32(t.length);
      if (len === 0) return -1;
      var n = len;
      if (arguments.length > 1) {
        n = Number(arguments[1]);
        if (n !== n) {
          n = 0;
        } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) {
          n = (n > 0 || -1) * floor(abs(n));
        }
      }
      var k = n >= 0 ? min(n, len - 1) : len - abs(n);
      for (; k >= 0; k--) {
        if (t._getter(k) === searchElement)
          return k;
      }
      return -1;
    }});

    // get %TypedArray%.prototype.length
    // -- applied directly to the object in the constructor

    // %TypedArray%.prototype.map ( callbackfn, thisArg = undefined )
    Object.defineProperty($TypedArray$.prototype, 'map', {value: function(callbackfn) {
      if (this === undefined || this === null) throw TypeError();
      var t = Object(this);
      var len = ToUint32(t.length);
      if (!IsCallable(callbackfn)) throw TypeError();
      var res = []; res.length = len;
      var thisp = arguments[1];
      for (var i = 0; i < len; i++)
        res[i] = callbackfn.call(thisp, t._getter(i), i, t);
      return new this.constructor(res);
    }});

    // %TypedArray%.prototype.reduce ( callbackfn [, initialValue] )
    Object.defineProperty($TypedArray$.prototype, 'reduce', {value: function(callbackfn) {
      if (this === undefined || this === null) throw TypeError();
      var t = Object(this);
      var len = ToUint32(t.length);
      if (!IsCallable(callbackfn)) throw TypeError();
      // no value to return if no initial value and an empty array
      if (len === 0 && arguments.length === 1) throw TypeError();
      var k = 0;
      var accumulator;
      if (arguments.length >= 2) {
        accumulator = arguments[1];
      } else {
        accumulator = t._getter(k++);
      }
      while (k < len) {
        accumulator = callbackfn.call(undefined, accumulator, t._getter(k), k, t);
        k++;
      }
      return accumulator;
    }});

    // %TypedArray%.prototype.reduceRight ( callbackfn [, initialValue] )
    Object.defineProperty($TypedArray$.prototype, 'reduceRight', {value: function(callbackfn) {
      if (this === undefined || this === null) throw TypeError();
      var t = Object(this);
      var len = ToUint32(t.length);
      if (!IsCallable(callbackfn)) throw TypeError();
      // no value to return if no initial value, empty array
      if (len === 0 && arguments.length === 1) throw TypeError();
      var k = len - 1;
      var accumulator;
      if (arguments.length >= 2) {
        accumulator = arguments[1];
      } else {
        accumulator = t._getter(k--);
      }
      while (k >= 0) {
        accumulator = callbackfn.call(undefined, accumulator, t._getter(k), k, t);
        k--;
      }
      return accumulator;
    }});

    // %TypedArray%.prototype.reverse ( )
    Object.defineProperty($TypedArray$.prototype, 'reverse', {value: function() {
      if (this === undefined || this === null) throw TypeError();
      var t = Object(this);
      var len = ToUint32(t.length);
      var half = floor(len / 2);
      for (var i = 0, j = len - 1; i < half; ++i, --j) {
        var tmp = t._getter(i);
        t._setter(i, t._getter(j));
        t._setter(j, tmp);
      }
      return t;
    }});

    // %TypedArray%.prototype.set(array, offset = 0 )
    // %TypedArray%.prototype.set(typedArray, offset = 0 )
    // WebIDL: void set(TypedArray array, optional unsigned long offset);
    // WebIDL: void set(sequence<type> array, optional unsigned long offset);
    Object.defineProperty($TypedArray$.prototype, 'set', {value: function(index, value) {
      if (arguments.length < 1) throw SyntaxError('Not enough arguments');
      var array, sequence, offset, len,
          i, s, d,
          byteOffset, byteLength, tmp;

      if (typeof arguments[0] === 'object' && arguments[0].constructor === this.constructor) {
        // void set(TypedArray array, optional unsigned long offset);
        array = arguments[0];
        offset = ToUint32(arguments[1]);

        if (offset + array.length > this.length) {
          throw RangeError('Offset plus length of array is out of range');
        }

        byteOffset = this.byteOffset + offset * this.BYTES_PER_ELEMENT;
        byteLength = array.length * this.BYTES_PER_ELEMENT;

        if (array.buffer === this.buffer) {
          tmp = [];
          for (i = 0, s = array.byteOffset; i < byteLength; i += 1, s += 1) {
            tmp[i] = array.buffer._bytes[s];
          }
          for (i = 0, d = byteOffset; i < byteLength; i += 1, d += 1) {
            this.buffer._bytes[d] = tmp[i];
          }
        } else {
          for (i = 0, s = array.byteOffset, d = byteOffset;
               i < byteLength; i += 1, s += 1, d += 1) {
            this.buffer._bytes[d] = array.buffer._bytes[s];
          }
        }
      } else if (typeof arguments[0] === 'object' && typeof arguments[0].length !== 'undefined') {
        // void set(sequence<type> array, optional unsigned long offset);
        sequence = arguments[0];
        len = ToUint32(sequence.length);
        offset = ToUint32(arguments[1]);

        if (offset + len > this.length) {
          throw RangeError('Offset plus length of array is out of range');
        }

        for (i = 0; i < len; i += 1) {
          s = sequence[i];
          this._setter(offset + i, Number(s));
        }
      } else {
        throw TypeError('Unexpected argument type(s)');
      }
    }});

    // %TypedArray%.prototype.slice ( start, end )
    Object.defineProperty($TypedArray$.prototype, 'slice', {value: function(start, end) {
      var o = ToObject(this);
      var lenVal = o.length;
      var len = ToUint32(lenVal);
      var relativeStart = ToInt32(start);
      var k = (relativeStart < 0) ? max(len + relativeStart, 0) : min(relativeStart, len);
      var relativeEnd = (end === undefined) ? len : ToInt32(end);
      var final = (relativeEnd < 0) ? max(len + relativeEnd, 0) : min(relativeEnd, len);
      var count = final - k;
      var c = o.constructor;
      var a = new c(count);
      var n = 0;
      while (k < final) {
        var kValue = o._getter(k);
        a._setter(n, kValue);
        ++k;
        ++n;
      }
      return a;
    }});

    // %TypedArray%.prototype.some ( callbackfn, thisArg = undefined )
    Object.defineProperty($TypedArray$.prototype, 'some', {value: function(callbackfn) {
      if (this === undefined || this === null) throw TypeError();
      var t = Object(this);
      var len = ToUint32(t.length);
      if (!IsCallable(callbackfn)) throw TypeError();
      var thisp = arguments[1];
      for (var i = 0; i < len; i++) {
        if (callbackfn.call(thisp, t._getter(i), i, t)) {
          return true;
        }
      }
      return false;
    }});

    // %TypedArray%.prototype.sort ( comparefn )
    Object.defineProperty($TypedArray$.prototype, 'sort', {value: function(comparefn) {
      if (this === undefined || this === null) throw TypeError();
      var t = Object(this);
      var len = ToUint32(t.length);
      var tmp = Array(len);
      for (var i = 0; i < len; ++i)
        tmp[i] = t._getter(i);
      if (comparefn) tmp.sort(comparefn); else tmp.sort(); // Hack for IE8/9
      for (i = 0; i < len; ++i)
        t._setter(i, tmp[i]);
      return t;
    }});

    // %TypedArray%.prototype.subarray(begin = 0, end = this.length )
    // WebIDL: TypedArray subarray(long begin, optional long end);
    Object.defineProperty($TypedArray$.prototype, 'subarray', {value: function(start, end) {
      function clamp(v, min, max) { return v < min ? min : v > max ? max : v; }

      start = ToInt32(start);
      end = ToInt32(end);

      if (arguments.length < 1) { start = 0; }
      if (arguments.length < 2) { end = this.length; }

      if (start < 0) { start = this.length + start; }
      if (end < 0) { end = this.length + end; }

      start = clamp(start, 0, this.length);
      end = clamp(end, 0, this.length);

      var len = end - start;
      if (len < 0) {
        len = 0;
      }

      return new this.constructor(
        this.buffer, this.byteOffset + start * this.BYTES_PER_ELEMENT, len);
    }});

    // %TypedArray%.prototype.toLocaleString ( )
    // %TypedArray%.prototype.toString ( )
    // %TypedArray%.prototype.values ( )
    // %TypedArray%.prototype [ @@iterator ] ( )
    // get %TypedArray%.prototype [ @@toStringTag ]
    // -- defined in es6.js to shim browsers w/ native TypedArrays

    function makeTypedArray(elementSize, pack, unpack) {
      // Each TypedArray type requires a distinct constructor instance with
      // identical logic, which this produces.
      var TypedArray = function() {
        Object.defineProperty(this, 'constructor', {value: TypedArray});
        $TypedArray$.apply(this, arguments);
        makeArrayAccessors(this);
      };
      if ('__proto__' in TypedArray) {
        TypedArray.__proto__ = $TypedArray$;
      } else {
        TypedArray.from = $TypedArray$.from;
        TypedArray.of = $TypedArray$.of;
      }

      TypedArray.BYTES_PER_ELEMENT = elementSize;

      var TypedArrayPrototype = function() {};
      TypedArrayPrototype.prototype = $TypedArrayPrototype$;

      TypedArray.prototype = new TypedArrayPrototype();

      Object.defineProperty(TypedArray.prototype, 'BYTES_PER_ELEMENT', {value: elementSize});
      Object.defineProperty(TypedArray.prototype, '_pack', {value: pack});
      Object.defineProperty(TypedArray.prototype, '_unpack', {value: unpack});

      return TypedArray;
    }

    var Int8Array = makeTypedArray(1, packI8, unpackI8);
    var Uint8Array = makeTypedArray(1, packU8, unpackU8);
    var Uint8ClampedArray = makeTypedArray(1, packU8Clamped, unpackU8);
    var Int16Array = makeTypedArray(2, packI16, unpackI16);
    var Uint16Array = makeTypedArray(2, packU16, unpackU16);
    var Int32Array = makeTypedArray(4, packI32, unpackI32);
    var Uint32Array = makeTypedArray(4, packU32, unpackU32);
    var Float32Array = makeTypedArray(4, packF32, unpackF32);
    var Float64Array = makeTypedArray(8, packF64, unpackF64);

    global.Int8Array = global.Int8Array || Int8Array;
    global.Uint8Array = global.Uint8Array || Uint8Array;
    global.Uint8ClampedArray = global.Uint8ClampedArray || Uint8ClampedArray;
    global.Int16Array = global.Int16Array || Int16Array;
    global.Uint16Array = global.Uint16Array || Uint16Array;
    global.Int32Array = global.Int32Array || Int32Array;
    global.Uint32Array = global.Uint32Array || Uint32Array;
    global.Float32Array = global.Float32Array || Float32Array;
    global.Float64Array = global.Float64Array || Float64Array;
  }());

  //
  // 6 The DataView View Type
  //

  (function() {
    function r(array, index) {
      return IsCallable(array.get) ? array.get(index) : array[index];
    }

    var IS_BIG_ENDIAN = (function() {
      var u16array = new Uint16Array([0x1234]),
          u8array = new Uint8Array(u16array.buffer);
      return r(u8array, 0) === 0x12;
    }());

    // DataView(buffer, byteOffset=0, byteLength=undefined)
    // WebIDL: Constructor(ArrayBuffer buffer,
    //                     optional unsigned long byteOffset,
    //                     optional unsigned long byteLength)
    function DataView(buffer, byteOffset, byteLength) {
      if (!(buffer instanceof ArrayBuffer || Class(buffer) === 'ArrayBuffer')) throw TypeError();

      byteOffset = ToUint32(byteOffset);
      if (byteOffset > buffer.byteLength)
        throw RangeError('byteOffset out of range');

      if (byteLength === undefined)
        byteLength = buffer.byteLength - byteOffset;
      else
        byteLength = ToUint32(byteLength);

      if ((byteOffset + byteLength) > buffer.byteLength)
        throw RangeError('byteOffset and length reference an area beyond the end of the buffer');

      Object.defineProperty(this, 'buffer', {value: buffer});
      Object.defineProperty(this, 'byteLength', {value: byteLength});
      Object.defineProperty(this, 'byteOffset', {value: byteOffset});
    };

    // get DataView.prototype.buffer
    // get DataView.prototype.byteLength
    // get DataView.prototype.byteOffset
    // -- applied directly to instances by the constructor

    function makeGetter(arrayType) {
      return function GetViewValue(byteOffset, littleEndian) {
        byteOffset = ToUint32(byteOffset);

        if (byteOffset + arrayType.BYTES_PER_ELEMENT > this.byteLength)
          throw RangeError('Array index out of range');

        byteOffset += this.byteOffset;

        var uint8Array = new Uint8Array(this.buffer, byteOffset, arrayType.BYTES_PER_ELEMENT),
            bytes = [];
        for (var i = 0; i < arrayType.BYTES_PER_ELEMENT; i += 1)
          bytes.push(r(uint8Array, i));

        if (Boolean(littleEndian) === Boolean(IS_BIG_ENDIAN))
          bytes.reverse();

        return r(new arrayType(new Uint8Array(bytes).buffer), 0);
      };
    }

    Object.defineProperty(DataView.prototype, 'getUint8', {value: makeGetter(Uint8Array)});
    Object.defineProperty(DataView.prototype, 'getInt8', {value: makeGetter(Int8Array)});
    Object.defineProperty(DataView.prototype, 'getUint16', {value: makeGetter(Uint16Array)});
    Object.defineProperty(DataView.prototype, 'getInt16', {value: makeGetter(Int16Array)});
    Object.defineProperty(DataView.prototype, 'getUint32', {value: makeGetter(Uint32Array)});
    Object.defineProperty(DataView.prototype, 'getInt32', {value: makeGetter(Int32Array)});
    Object.defineProperty(DataView.prototype, 'getFloat32', {value: makeGetter(Float32Array)});
    Object.defineProperty(DataView.prototype, 'getFloat64', {value: makeGetter(Float64Array)});

    function makeSetter(arrayType) {
      return function SetViewValue(byteOffset, value, littleEndian) {
        byteOffset = ToUint32(byteOffset);
        if (byteOffset + arrayType.BYTES_PER_ELEMENT > this.byteLength)
          throw RangeError('Array index out of range');

        // Get bytes
        var typeArray = new arrayType([value]),
            byteArray = new Uint8Array(typeArray.buffer),
            bytes = [], i, byteView;

        for (i = 0; i < arrayType.BYTES_PER_ELEMENT; i += 1)
          bytes.push(r(byteArray, i));

        // Flip if necessary
        if (Boolean(littleEndian) === Boolean(IS_BIG_ENDIAN))
          bytes.reverse();

        // Write them
        byteView = new Uint8Array(this.buffer, byteOffset, arrayType.BYTES_PER_ELEMENT);
        byteView.set(bytes);
      };
    }

    Object.defineProperty(DataView.prototype, 'setUint8', {value: makeSetter(Uint8Array)});
    Object.defineProperty(DataView.prototype, 'setInt8', {value: makeSetter(Int8Array)});
    Object.defineProperty(DataView.prototype, 'setUint16', {value: makeSetter(Uint16Array)});
    Object.defineProperty(DataView.prototype, 'setInt16', {value: makeSetter(Int16Array)});
    Object.defineProperty(DataView.prototype, 'setUint32', {value: makeSetter(Uint32Array)});
    Object.defineProperty(DataView.prototype, 'setInt32', {value: makeSetter(Int32Array)});
    Object.defineProperty(DataView.prototype, 'setFloat32', {value: makeSetter(Float32Array)});
    Object.defineProperty(DataView.prototype, 'setFloat64', {value: makeSetter(Float64Array)});

    global.DataView = global.DataView || DataView;

  }());

}(self));

// https://github.com/ttaubert/node-arraybuffer-slice
// (c) 2014 Tim Taubert <tim@timtaubert.de>
// arraybuffer-slice may be freely distributed under the MIT license.

(function (undefined) {
  "use strict";

  function clamp(val, length) {
    val = (val|0) || 0;

    if (val < 0) {
      return Math.max(val + length, 0);
    }

    return Math.min(val, length);
  }

  if (!ArrayBuffer.prototype.slice) {
    ArrayBuffer.prototype.slice = function (from, to) {
      var length = this.byteLength;
      var begin = clamp(from, length);
      var end = length;

      if (to !== undefined) {
        end = clamp(to, length);
      }

      if (begin > end) {
        return new ArrayBuffer(0);
      }

      var num = end - begin;
      var target = new ArrayBuffer(num);
      var targetArray = new Uint8Array(target);

      var sourceArray = new Uint8Array(this, begin, num);
      targetArray.set(sourceArray);

      return target;
    };
  }
})();

// This is free and unencumbered software released into the public domain.
// See LICENSE.md for more information.

// If we're in node require encoding-indexes and attach it to the global.
/**
 * @fileoverview Global |this| required for resolving indexes in node.
 * @suppress {globalThis}
 */
if (typeof module !== "undefined" && module.exports) {
  this["encoding-indexes"] =
    require("./encoding-indexes.js")["encoding-indexes"];
}

(function(global) {
  'use strict';

  //
  // Utilities
  //

  /**
   * @param {number} a The number to test.
   * @param {number} min The minimum value in the range, inclusive.
   * @param {number} max The maximum value in the range, inclusive.
   * @return {boolean} True if a >= min and a <= max.
   */
  function inRange(a, min, max) {
    return min <= a && a <= max;
  }

  /**
   * @param {!Array.<*>} array The array to check.
   * @param {*} item The item to look for in the array.
   * @return {boolean} True if the item appears in the array.
   */
  function includes(array, item) {
    return array.indexOf(item) !== -1;
  }

  var floor = Math.floor;

  /**
   * @param {*} o
   * @return {Object}
   */
  function ToDictionary(o) {
    if (o === undefined) return {};
    if (o === Object(o)) return o;
    throw TypeError('Could not convert argument to dictionary');
  }

  /**
   * @param {string} string Input string of UTF-16 code units.
   * @return {!Array.<number>} Code points.
   */
  function stringToCodePoints(string) {
    // https://heycam.github.io/webidl/#dfn-obtain-unicode

    // 1. Let S be the DOMString value.
    var s = String(string);

    // 2. Let n be the length of S.
    var n = s.length;

    // 3. Initialize i to 0.
    var i = 0;

    // 4. Initialize U to be an empty sequence of Unicode characters.
    var u = [];

    // 5. While i < n:
    while (i < n) {

      // 1. Let c be the code unit in S at index i.
      var c = s.charCodeAt(i);

      // 2. Depending on the value of c:

      // c < 0xD800 or c > 0xDFFF
      if (c < 0xD800 || c > 0xDFFF) {
        // Append to U the Unicode character with code point c.
        u.push(c);
      }

      // 0xDC00 ? c ? 0xDFFF
      else if (0xDC00 <= c && c <= 0xDFFF) {
        // Append to U a U+FFFD REPLACEMENT CHARACTER.
        u.push(0xFFFD);
      }

      // 0xD800 ? c ? 0xDBFF
      else if (0xD800 <= c && c <= 0xDBFF) {
        // 1. If i = n?1, then append to U a U+FFFD REPLACEMENT
        // CHARACTER.
        if (i === n - 1) {
          u.push(0xFFFD);
        }
        // 2. Otherwise, i < n?1:
        else {
          // 1. Let d be the code unit in S at index i+1.
          var d = string.charCodeAt(i + 1);

          // 2. If 0xDC00 ? d ? 0xDFFF, then:
          if (0xDC00 <= d && d <= 0xDFFF) {
            // 1. Let a be c & 0x3FF.
            var a = c & 0x3FF;

            // 2. Let b be d & 0x3FF.
            var b = d & 0x3FF;

            // 3. Append to U the Unicode character with code point
            // 2^16+2^10*a+b.
            u.push(0x10000 + (a << 10) + b);

            // 4. Set i to i+1.
            i += 1;
          }

          // 3. Otherwise, d < 0xDC00 or d > 0xDFFF. Append to U a
          // U+FFFD REPLACEMENT CHARACTER.
          else  {
            u.push(0xFFFD);
          }
        }
      }

      // 3. Set i to i+1.
      i += 1;
    }

    // 6. Return U.
    return u;
  }

  /**
   * @param {!Array.<number>} code_points Array of code points.
   * @return {string} string String of UTF-16 code units.
   */
  function codePointsToString(code_points) {
    var s = '';
    for (var i = 0; i < code_points.length; ++i) {
      var cp = code_points[i];
      if (cp <= 0xFFFF) {
        s += String.fromCharCode(cp);
      } else {
        cp -= 0x10000;
        s += String.fromCharCode((cp >> 10) + 0xD800,
                                 (cp & 0x3FF) + 0xDC00);
      }
    }
    return s;
  }


  //
  // Implementation of Encoding specification
  // https://encoding.spec.whatwg.org/
  //

  //
  // 4. Terminology
  //

  /**
   * An ASCII byte is a byte in the range 0x00 to 0x7F, inclusive.
   * @param {number} a The number to test.
   * @return {boolean} True if a is in the range 0x00 to 0x7F, inclusive.
   */
  function isASCIIByte(a) {
    return 0x00 <= a && a <= 0x7F;
  }

  /**
   * An ASCII code point is a code point in the range U+0000 to
   * U+007F, inclusive.
   */
  var isASCIICodePoint = isASCIIByte;


  /**
   * End-of-stream is a special token that signifies no more tokens
   * are in the stream.
   * @const
   */ var end_of_stream = -1;

  /**
   * A stream represents an ordered sequence of tokens.
   *
   * @constructor
   * @param {!(Array.<number>|Uint8Array)} tokens Array of tokens that provide
   * the stream.
   */
  function Stream(tokens) {
    /** @type {!Array.<number>} */
    this.tokens = [].slice.call(tokens);
    // Reversed as push/pop is more efficient than shift/unshift.
    this.tokens.reverse();
  }

  Stream.prototype = {
    /**
     * @return {boolean} True if end-of-stream has been hit.
     */
    endOfStream: function() {
      return !this.tokens.length;
    },

    /**
     * When a token is read from a stream, the first token in the
     * stream must be returned and subsequently removed, and
     * end-of-stream must be returned otherwise.
     *
     * @return {number} Get the next token from the stream, or
     * end_of_stream.
     */
     read: function() {
      if (!this.tokens.length)
        return end_of_stream;
       return this.tokens.pop();
     },

    /**
     * When one or more tokens are prepended to a stream, those tokens
     * must be inserted, in given order, before the first token in the
     * stream.
     *
     * @param {(number|!Array.<number>)} token The token(s) to prepend to the
     * stream.
     */
    prepend: function(token) {
      if (Array.isArray(token)) {
        var tokens = /**@type {!Array.<number>}*/(token);
        while (tokens.length)
          this.tokens.push(tokens.pop());
      } else {
        this.tokens.push(token);
      }
    },

    /**
     * When one or more tokens are pushed to a stream, those tokens
     * must be inserted, in given order, after the last token in the
     * stream.
     *
     * @param {(number|!Array.<number>)} token The tokens(s) to push to the
     * stream.
     */
    push: function(token) {
      if (Array.isArray(token)) {
        var tokens = /**@type {!Array.<number>}*/(token);
        while (tokens.length)
          this.tokens.unshift(tokens.shift());
      } else {
        this.tokens.unshift(token);
      }
    }
  };

  //
  // 5. Encodings
  //

  // 5.1 Encoders and decoders

  /** @const */
  var finished = -1;

  /**
   * @param {boolean} fatal If true, decoding errors raise an exception.
   * @param {number=} opt_code_point Override the standard fallback code point.
   * @return {number} The code point to insert on a decoding error.
   */
  function decoderError(fatal, opt_code_point) {
    if (fatal)
      throw TypeError('Decoder error');
    return opt_code_point || 0xFFFD;
  }

  /**
   * @param {number} code_point The code point that could not be encoded.
   * @return {number} Always throws, no value is actually returned.
   */
  function encoderError(code_point) {
    throw TypeError('The code point ' + code_point + ' could not be encoded.');
  }

  /** @interface */
  function Decoder() {}
  Decoder.prototype = {
    /**
     * @param {Stream} stream The stream of bytes being decoded.
     * @param {number} bite The next byte read from the stream.
     * @return {?(number|!Array.<number>)} The next code point(s)
     *     decoded, or null if not enough data exists in the input
     *     stream to decode a complete code point, or |finished|.
     */
    handler: function(stream, bite) {}
  };

  /** @interface */
  function Encoder() {}
  Encoder.prototype = {
    /**
     * @param {Stream} stream The stream of code points being encoded.
     * @param {number} code_point Next code point read from the stream.
     * @return {(number|!Array.<number>)} Byte(s) to emit, or |finished|.
     */
    handler: function(stream, code_point) {}
  };

  // 5.2 Names and labels

  // TODO: Define @typedef for Encoding: {name:string,labels:Array.<string>}
  // https://github.com/google/closure-compiler/issues/247

  /**
   * @param {string} label The encoding label.
   * @return {?{name:string,labels:Array.<string>}}
   */
  function getEncoding(label) {
    // 1. Remove any leading and trailing ASCII whitespace from label.
    label = String(label).trim().toLowerCase();

    // 2. If label is an ASCII case-insensitive match for any of the
    // labels listed in the table below, return the corresponding
    // encoding, and failure otherwise.
    if (Object.prototype.hasOwnProperty.call(label_to_encoding, label)) {
      return label_to_encoding[label];
    }
    return null;
  }

  /**
   * Encodings table: https://encoding.spec.whatwg.org/encodings.json
   * @const
   * @type {!Array.<{
   *          heading: string,
   *          encodings: Array.<{name:string,labels:Array.<string>}>
   *        }>}
   */
  var encodings = [
    {
      "encodings": [
        {
          "labels": [
            "unicode-1-1-utf-8",
            "utf-8",
            "utf8"
          ],
          "name": "UTF-8"
        }
      ],
      "heading": "The Encoding"
    },
    {
      "encodings": [
        {
          "labels": [
            "866",
            "cp866",
            "csibm866",
            "ibm866"
          ],
          "name": "IBM866"
        },
        {
          "labels": [
            "csisolatin2",
            "iso-8859-2",
            "iso-ir-101",
            "iso8859-2",
            "iso88592",
            "iso_8859-2",
            "iso_8859-2:1987",
            "l2",
            "latin2"
          ],
          "name": "ISO-8859-2"
        },
        {
          "labels": [
            "csisolatin3",
            "iso-8859-3",
            "iso-ir-109",
            "iso8859-3",
            "iso88593",
            "iso_8859-3",
            "iso_8859-3:1988",
            "l3",
            "latin3"
          ],
          "name": "ISO-8859-3"
        },
        {
          "labels": [
            "csisolatin4",
            "iso-8859-4",
            "iso-ir-110",
            "iso8859-4",
            "iso88594",
            "iso_8859-4",
            "iso_8859-4:1988",
            "l4",
            "latin4"
          ],
          "name": "ISO-8859-4"
        },
        {
          "labels": [
            "csisolatincyrillic",
            "cyrillic",
            "iso-8859-5",
            "iso-ir-144",
            "iso8859-5",
            "iso88595",
            "iso_8859-5",
            "iso_8859-5:1988"
          ],
          "name": "ISO-8859-5"
        },
        {
          "labels": [
            "arabic",
            "asmo-708",
            "csiso88596e",
            "csiso88596i",
            "csisolatinarabic",
            "ecma-114",
            "iso-8859-6",
            "iso-8859-6-e",
            "iso-8859-6-i",
            "iso-ir-127",
            "iso8859-6",
            "iso88596",
            "iso_8859-6",
            "iso_8859-6:1987"
          ],
          "name": "ISO-8859-6"
        },
        {
          "labels": [
            "csisolatingreek",
            "ecma-118",
            "elot_928",
            "greek",
            "greek8",
            "iso-8859-7",
            "iso-ir-126",
            "iso8859-7",
            "iso88597",
            "iso_8859-7",
            "iso_8859-7:1987",
            "sun_eu_greek"
          ],
          "name": "ISO-8859-7"
        },
        {
          "labels": [
            "csiso88598e",
            "csisolatinhebrew",
            "hebrew",
            "iso-8859-8",
            "iso-8859-8-e",
            "iso-ir-138",
            "iso8859-8",
            "iso88598",
            "iso_8859-8",
            "iso_8859-8:1988",
            "visual"
          ],
          "name": "ISO-8859-8"
        },
        {
          "labels": [
            "csiso88598i",
            "iso-8859-8-i",
            "logical"
          ],
          "name": "ISO-8859-8-I"
        },
        {
          "labels": [
            "csisolatin6",
            "iso-8859-10",
            "iso-ir-157",
            "iso8859-10",
            "iso885910",
            "l6",
            "latin6"
          ],
          "name": "ISO-8859-10"
        },
        {
          "labels": [
            "iso-8859-13",
            "iso8859-13",
            "iso885913"
          ],
          "name": "ISO-8859-13"
        },
        {
          "labels": [
            "iso-8859-14",
            "iso8859-14",
            "iso885914"
          ],
          "name": "ISO-8859-14"
        },
        {
          "labels": [
            "csisolatin9",
            "iso-8859-15",
            "iso8859-15",
            "iso885915",
            "iso_8859-15",
            "l9"
          ],
          "name": "ISO-8859-15"
        },
        {
          "labels": [
            "iso-8859-16"
          ],
          "name": "ISO-8859-16"
        },
        {
          "labels": [
            "cskoi8r",
            "koi",
            "koi8",
            "koi8-r",
            "koi8_r"
          ],
          "name": "KOI8-R"
        },
        {
          "labels": [
            "koi8-ru",
            "koi8-u"
          ],
          "name": "KOI8-U"
        },
        {
          "labels": [
            "csmacintosh",
            "mac",
            "macintosh",
            "x-mac-roman"
          ],
          "name": "macintosh"
        },
        {
          "labels": [
            "dos-874",
            "iso-8859-11",
            "iso8859-11",
            "iso885911",
            "tis-620",
            "windows-874"
          ],
          "name": "windows-874"
        },
        {
          "labels": [
            "cp1250",
            "windows-1250",
            "x-cp1250"
          ],
          "name": "windows-1250"
        },
        {
          "labels": [
            "cp1251",
            "windows-1251",
            "x-cp1251"
          ],
          "name": "windows-1251"
        },
        {
          "labels": [
            "ansi_x3.4-1968",
            "ascii",
            "cp1252",
            "cp819",
            "csisolatin1",
            "ibm819",
            "iso-8859-1",
            "iso-ir-100",
            "iso8859-1",
            "iso88591",
            "iso_8859-1",
            "iso_8859-1:1987",
            "l1",
            "latin1",
            "us-ascii",
            "windows-1252",
            "x-cp1252"
          ],
          "name": "windows-1252"
        },
        {
          "labels": [
            "cp1253",
            "windows-1253",
            "x-cp1253"
          ],
          "name": "windows-1253"
        },
        {
          "labels": [
            "cp1254",
            "csisolatin5",
            "iso-8859-9",
            "iso-ir-148",
            "iso8859-9",
            "iso88599",
            "iso_8859-9",
            "iso_8859-9:1989",
            "l5",
            "latin5",
            "windows-1254",
            "x-cp1254"
          ],
          "name": "windows-1254"
        },
        {
          "labels": [
            "cp1255",
            "windows-1255",
            "x-cp1255"
          ],
          "name": "windows-1255"
        },
        {
          "labels": [
            "cp1256",
            "windows-1256",
            "x-cp1256"
          ],
          "name": "windows-1256"
        },
        {
          "labels": [
            "cp1257",
            "windows-1257",
            "x-cp1257"
          ],
          "name": "windows-1257"
        },
        {
          "labels": [
            "cp1258",
            "windows-1258",
            "x-cp1258"
          ],
          "name": "windows-1258"
        },
        {
          "labels": [
            "x-mac-cyrillic",
            "x-mac-ukrainian"
          ],
          "name": "x-mac-cyrillic"
        }
      ],
      "heading": "Legacy single-byte encodings"
    },
    {
      "encodings": [
        {
          "labels": [
            "chinese",
            "csgb2312",
            "csiso58gb231280",
            "gb2312",
            "gb_2312",
            "gb_2312-80",
            "gbk",
            "iso-ir-58",
            "x-gbk"
          ],
          "name": "GBK"
        },
        {
          "labels": [
            "gb18030"
          ],
          "name": "gb18030"
        }
      ],
      "heading": "Legacy multi-byte Chinese (simplified) encodings"
    },
    {
      "encodings": [
        {
          "labels": [
            "big5",
            "big5-hkscs",
            "cn-big5",
            "csbig5",
            "x-x-big5"
          ],
          "name": "Big5"
        }
      ],
      "heading": "Legacy multi-byte Chinese (traditional) encodings"
    },
    {
      "encodings": [
        {
          "labels": [
            "cseucpkdfmtjapanese",
            "euc-jp",
            "x-euc-jp"
          ],
          "name": "EUC-JP"
        },
        {
          "labels": [
            "csiso2022jp",
            "iso-2022-jp"
          ],
          "name": "ISO-2022-JP"
        },
        {
          "labels": [
            "csshiftjis",
            "ms932",
            "ms_kanji",
            "shift-jis",
            "shift_jis",
            "sjis",
            "windows-31j",
            "x-sjis"
          ],
          "name": "Shift_JIS"
        }
      ],
      "heading": "Legacy multi-byte Japanese encodings"
    },
    {
      "encodings": [
        {
          "labels": [
            "cseuckr",
            "csksc56011987",
            "euc-kr",
            "iso-ir-149",
            "korean",
            "ks_c_5601-1987",
            "ks_c_5601-1989",
            "ksc5601",
            "ksc_5601",
            "windows-949"
          ],
          "name": "EUC-KR"
        }
      ],
      "heading": "Legacy multi-byte Korean encodings"
    },
    {
      "encodings": [
        {
          "labels": [
            "csiso2022kr",
            "hz-gb-2312",
            "iso-2022-cn",
            "iso-2022-cn-ext",
            "iso-2022-kr"
          ],
          "name": "replacement"
        },
        {
          "labels": [
            "utf-16be"
          ],
          "name": "UTF-16BE"
        },
        {
          "labels": [
            "utf-16",
            "utf-16le"
          ],
          "name": "UTF-16LE"
        },
        {
          "labels": [
            "x-user-defined"
          ],
          "name": "x-user-defined"
        }
      ],
      "heading": "Legacy miscellaneous encodings"
    }
  ];

  // Label to encoding registry.
  /** @type {Object.<string,{name:string,labels:Array.<string>}>} */
  var label_to_encoding = {};
  encodings.forEach(function(category) {
    category.encodings.forEach(function(encoding) {
      encoding.labels.forEach(function(label) {
        label_to_encoding[label] = encoding;
      });
    });
  });

  // Registry of of encoder/decoder factories, by encoding name.
  /** @type {Object.<string, function({fatal:boolean}): Encoder>} */
  var encoders = {};
  /** @type {Object.<string, function({fatal:boolean}): Decoder>} */
  var decoders = {};

  //
  // 6. Indexes
  //

  /**
   * @param {number} pointer The |pointer| to search for.
   * @param {(!Array.<?number>|undefined)} index The |index| to search within.
   * @return {?number} The code point corresponding to |pointer| in |index|,
   *     or null if |code point| is not in |index|.
   */
  function indexCodePointFor(pointer, index) {
    if (!index) return null;
    return index[pointer] || null;
  }

  /**
   * @param {number} code_point The |code point| to search for.
   * @param {!Array.<?number>} index The |index| to search within.
   * @return {?number} The first pointer corresponding to |code point| in
   *     |index|, or null if |code point| is not in |index|.
   */
  function indexPointerFor(code_point, index) {
    var pointer = index.indexOf(code_point);
    return pointer === -1 ? null : pointer;
  }

  /**
   * @param {string} name Name of the index.
   * @return {(!Array.<number>|!Array.<Array.<number>>)}
   *  */
  function index(name) {
    if (!('encoding-indexes' in global)) {
      throw Error("Indexes missing." +
                  " Did you forget to include encoding-indexes.js?");
    }
    return global['encoding-indexes'][name];
  }

  /**
   * @param {number} pointer The |pointer| to search for in the gb18030 index.
   * @return {?number} The code point corresponding to |pointer| in |index|,
   *     or null if |code point| is not in the gb18030 index.
   */
  function indexGB18030RangesCodePointFor(pointer) {
    // 1. If pointer is greater than 39419 and less than 189000, or
    // pointer is greater than 1237575, return null.
    if ((pointer > 39419 && pointer < 189000) || (pointer > 1237575))
      return null;

    // 2. If pointer is 7457, return code point U+E7C7.
    if (pointer === 7457) return 0xE7C7;

    // 3. Let offset be the last pointer in index gb18030 ranges that
    // is equal to or less than pointer and let code point offset be
    // its corresponding code point.
    var offset = 0;
    var code_point_offset = 0;
    var idx = index('gb18030');
    var i;
    for (i = 0; i < idx.length; ++i) {
      /** @type {!Array.<number>} */
      var entry = idx[i];
      if (entry[0] <= pointer) {
        offset = entry[0];
        code_point_offset = entry[1];
      } else {
        break;
      }
    }

    // 4. Return a code point whose value is code point offset +
    // pointer ? offset.
    return code_point_offset + pointer - offset;
  }

  /**
   * @param {number} code_point The |code point| to locate in the gb18030 index.
   * @return {number} The first pointer corresponding to |code point| in the
   *     gb18030 index.
   */
  function indexGB18030RangesPointerFor(code_point) {
    // 1. If code point is U+E7C7, return pointer 7457.
    if (code_point === 0xE7C7) return 7457;

    // 2. Let offset be the last code point in index gb18030 ranges
    // that is equal to or less than code point and let pointer offset
    // be its corresponding pointer.
    var offset = 0;
    var pointer_offset = 0;
    var idx = index('gb18030');
    var i;
    for (i = 0; i < idx.length; ++i) {
      /** @type {!Array.<number>} */
      var entry = idx[i];
      if (entry[1] <= code_point) {
        offset = entry[1];
        pointer_offset = entry[0];
      } else {
        break;
      }
    }

    // 3. Return a pointer whose value is pointer offset + code point
    // ? offset.
    return pointer_offset + code_point - offset;
  }

  /**
   * @param {number} code_point The |code_point| to search for in the shift_jis
   *     index.
   * @return {?number} The code point corresponding to |pointer| in |index|,
   *     or null if |code point| is not in the shift_jis index.
   */
  function indexShiftJISPointerFor(code_point) {
    // 1. Let index be index jis0208 excluding all pointers in the
    // range 8272 to 8835.
    var pointer = indexPointerFor(code_point, index('jis0208'));
    if (pointer === null || inRange(pointer, 8272, 8835))
      return null;

    // 2. Return the index pointer for code point in index.
    return pointer;
  }

  /**
   * @param {number} code_point The |code_point| to search for in the big5
   *     index.
   * @return {?number} The code point corresponding to |pointer| in |index|,
   *     or null if |code point| is not in the big5 index.
   */
  function indexBig5PointerFor(code_point) {

    // 1. Let index be index big5.
    var index_ = index('big5');

    // 2. If code point is U+2550, U+255E, U+2561, U+256A, U+5341, or
    // U+5345, return the last pointer corresponding to code point in
    // index.
    if (code_point === 0x2550 || code_point === 0x255E ||
        code_point === 0x2561 || code_point === 0x256A ||
        code_point === 0x5341 || code_point === 0x5345) {
      return index_.lastIndexOf(code_point);
    }

    // 3. Return the index pointer for code point in index.
    return indexPointerFor(code_point, index_);
  }

  //
  // 8. API
  //

  /** @const */ var DEFAULT_ENCODING = 'utf-8';

  // 8.1 Interface TextDecoder

  /**
   * @constructor
   * @param {string=} label The label of the encoding;
   *     defaults to 'utf-8'.
   * @param {Object=} options
   */
  function TextDecoder(label, options) {
    // Web IDL conventions
    if (!(this instanceof TextDecoder))
      throw TypeError('Called as a function. Did you forget \'new\'?');
    label = label !== undefined ? String(label) : DEFAULT_ENCODING;
    options = ToDictionary(options);

    // A TextDecoder object has an associated encoding, decoder,
    // stream, ignore BOM flag (initially unset), BOM seen flag
    // (initially unset), error mode (initially replacement), and do
    // not flush flag (initially unset).

    /** @private */
    this._encoding = null;
    /** @private @type {?Decoder} */
    this._decoder = null;
    /** @private @type {boolean} */
    this._ignoreBOM = false;
    /** @private @type {boolean} */
    this._BOMseen = false;
    /** @private @type {string} */
    this._error_mode = 'replacement';
    /** @private @type {boolean} */
    this._do_not_flush = false;


    // 1. Let encoding be the result of getting an encoding from
    // label.
    var encoding = getEncoding(label);

    // 2. If encoding is failure or replacement, throw a RangeError.
    if (encoding === null || encoding.name === 'replacement')
      throw RangeError('Unknown encoding: ' + label);
    if (!decoders[encoding.name]) {
      throw Error('Decoder not present.' +
                  ' Did you forget to include encoding-indexes.js?');
    }

    // 3. Let dec be a new TextDecoder object.
    var dec = this;

    // 4. Set dec's encoding to encoding.
    dec._encoding = encoding;

    // 5. If options's fatal member is true, set dec's error mode to
    // fatal.
    if (Boolean(options['fatal']))
      dec._error_mode = 'fatal';

    // 6. If options's ignoreBOM member is true, set dec's ignore BOM
    // flag.
    if (Boolean(options['ignoreBOM']))
      dec._ignoreBOM = true;

    // For pre-ES5 runtimes:
    if (!Object.defineProperty) {
      this.encoding = dec._encoding.name.toLowerCase();
      this.fatal = dec._error_mode === 'fatal';
      this.ignoreBOM = dec._ignoreBOM;
    }

    // 7. Return dec.
    return dec;
  }

  if (Object.defineProperty) {
    // The encoding attribute's getter must return encoding's name.
    Object.defineProperty(TextDecoder.prototype, 'encoding', {
      /** @this {TextDecoder} */
      get: function() { return this._encoding.name.toLowerCase(); }
    });

    // The fatal attribute's getter must return true if error mode
    // is fatal, and false otherwise.
    Object.defineProperty(TextDecoder.prototype, 'fatal', {
      /** @this {TextDecoder} */
      get: function() { return this._error_mode === 'fatal'; }
    });

    // The ignoreBOM attribute's getter must return true if ignore
    // BOM flag is set, and false otherwise.
    Object.defineProperty(TextDecoder.prototype, 'ignoreBOM', {
      /** @this {TextDecoder} */
      get: function() { return this._ignoreBOM; }
    });
  }

  /**
   * @param {BufferSource=} input The buffer of bytes to decode.
   * @param {Object=} options
   * @return {string} The decoded string.
   */
  TextDecoder.prototype.decode = function decode(input, options) {
    var bytes;
    if (typeof input === 'object' && input instanceof ArrayBuffer) {
      bytes = new Uint8Array(input);
    } else if (typeof input === 'object' && 'buffer' in input &&
               input.buffer instanceof ArrayBuffer) {
      bytes = new Uint8Array(input.buffer,
                             input.byteOffset,
                             input.byteLength);
    } else {
      bytes = new Uint8Array(0);
    }

    options = ToDictionary(options);

    // 1. If the do not flush flag is unset, set decoder to a new
    // encoding's decoder, set stream to a new stream, and unset the
    // BOM seen flag.
    if (!this._do_not_flush) {
      this._decoder = decoders[this._encoding.name]({
        fatal: this._error_mode === 'fatal'});
      this._BOMseen = false;
    }

    // 2. If options's stream is true, set the do not flush flag, and
    // unset the do not flush flag otherwise.
    this._do_not_flush = Boolean(options['stream']);

    // 3. If input is given, push a copy of input to stream.
    // TODO: Align with spec algorithm - maintain stream on instance.
    var input_stream = new Stream(bytes);

    // 4. Let output be a new stream.
    var output = [];

    /** @type {?(number|!Array.<number>)} */
    var result;

    // 5. While true:
    while (true) {
      // 1. Let token be the result of reading from stream.
      var token = input_stream.read();

      // 2. If token is end-of-stream and the do not flush flag is
      // set, return output, serialized.
      // TODO: Align with spec algorithm.
      if (token === end_of_stream)
        break;

      // 3. Otherwise, run these subsubsteps:

      // 1. Let result be the result of processing token for decoder,
      // stream, output, and error mode.
      result = this._decoder.handler(input_stream, token);

      // 2. If result is finished, return output, serialized.
      if (result === finished)
        break;

      if (result !== null) {
        if (Array.isArray(result))
          output.push.apply(output, /**@type {!Array.<number>}*/(result));
        else
          output.push(result);
      }

      // 3. Otherwise, if result is error, throw a TypeError.
      // (Thrown in handler)

      // 4. Otherwise, do nothing.
    }
    // TODO: Align with spec algorithm.
    if (!this._do_not_flush) {
      do {
        result = this._decoder.handler(input_stream, input_stream.read());
        if (result === finished)
          break;
        if (result === null)
          continue;
        if (Array.isArray(result))
          output.push.apply(output, /**@type {!Array.<number>}*/(result));
        else
          output.push(result);
      } while (!input_stream.endOfStream());
      this._decoder = null;
    }

    // A TextDecoder object also has an associated serialize stream
    // algorithm...
    /**
     * @param {!Array.<number>} stream
     * @return {string}
     * @this {TextDecoder}
     */
    function serializeStream(stream) {
      // 1. Let token be the result of reading from stream.
      // (Done in-place on array, rather than as a stream)

      // 2. If encoding is UTF-8, UTF-16BE, or UTF-16LE, and ignore
      // BOM flag and BOM seen flag are unset, run these subsubsteps:
      if (includes(['UTF-8', 'UTF-16LE', 'UTF-16BE'], this._encoding.name) &&
          !this._ignoreBOM && !this._BOMseen) {
        if (stream.length > 0 && stream[0] === 0xFEFF) {
          // 1. If token is U+FEFF, set BOM seen flag.
          this._BOMseen = true;
          stream.shift();
        } else if (stream.length > 0) {
          // 2. Otherwise, if token is not end-of-stream, set BOM seen
          // flag and append token to stream.
          this._BOMseen = true;
        } else {
          // 3. Otherwise, if token is not end-of-stream, append token
          // to output.
          // (no-op)
        }
      }
      // 4. Otherwise, return output.
      return codePointsToString(stream);
    }

    return serializeStream.call(this, output);
  };

  // 8.2 Interface TextEncoder

  /**
   * @constructor
   * @param {string=} label The label of the encoding. NONSTANDARD.
   * @param {Object=} options NONSTANDARD.
   */
  function TextEncoder(label, options) {
    // Web IDL conventions
    if (!(this instanceof TextEncoder))
      throw TypeError('Called as a function. Did you forget \'new\'?');
    options = ToDictionary(options);

    // A TextEncoder object has an associated encoding and encoder.

    /** @private */
    this._encoding = null;
    /** @private @type {?Encoder} */
    this._encoder = null;

    // Non-standard
    /** @private @type {boolean} */
    this._do_not_flush = false;
    /** @private @type {string} */
    this._fatal = Boolean(options['fatal']) ? 'fatal' : 'replacement';

    // 1. Let enc be a new TextEncoder object.
    var enc = this;

    // 2. Set enc's encoding to UTF-8's encoder.
    if (Boolean(options['NONSTANDARD_allowLegacyEncoding'])) {
      // NONSTANDARD behavior.
      label = label !== undefined ? String(label) : DEFAULT_ENCODING;
      var encoding = getEncoding(label);
      if (encoding === null || encoding.name === 'replacement')
        throw RangeError('Unknown encoding: ' + label);
      if (!encoders[encoding.name]) {
        throw Error('Encoder not present.' +
                    ' Did you forget to include encoding-indexes.js?');
      }
      enc._encoding = encoding;
    } else {
      // Standard behavior.
      enc._encoding = getEncoding('utf-8');

      if (label !== undefined && 'console' in global) {
        console.warn('TextEncoder constructor called with encoding label, '
                     + 'which is ignored.');
      }
    }

    // For pre-ES5 runtimes:
    if (!Object.defineProperty)
      this.encoding = enc._encoding.name.toLowerCase();

    // 3. Return enc.
    return enc;
  }

  if (Object.defineProperty) {
    // The encoding attribute's getter must return encoding's name.
    Object.defineProperty(TextEncoder.prototype, 'encoding', {
      /** @this {TextEncoder} */
      get: function() { return this._encoding.name.toLowerCase(); }
    });
  }

  /**
   * @param {string=} opt_string The string to encode.
   * @param {Object=} options
   * @return {!Uint8Array} Encoded bytes, as a Uint8Array.
   */
  TextEncoder.prototype.encode = function encode(opt_string, options) {
    opt_string = opt_string ? String(opt_string) : '';
    options = ToDictionary(options);

    // NOTE: This option is nonstandard. None of the encodings
    // permitted for encoding (i.e. UTF-8, UTF-16) are stateful when
    // the input is a USVString so streaming is not necessary.
    if (!this._do_not_flush)
      this._encoder = encoders[this._encoding.name]({
        fatal: this._fatal === 'fatal'});
    this._do_not_flush = Boolean(options['stream']);

    // 1. Convert input to a stream.
    var input = new Stream(stringToCodePoints(opt_string));

    // 2. Let output be a new stream
    var output = [];

    /** @type {?(number|!Array.<number>)} */
    var result;
    // 3. While true, run these substeps:
    while (true) {
      // 1. Let token be the result of reading from input.
      var token = input.read();
      if (token === end_of_stream)
        break;
      // 2. Let result be the result of processing token for encoder,
      // input, output.
      result = this._encoder.handler(input, token);
      if (result === finished)
        break;
      if (Array.isArray(result))
        output.push.apply(output, /**@type {!Array.<number>}*/(result));
      else
        output.push(result);
    }
    // TODO: Align with spec algorithm.
    if (!this._do_not_flush) {
      while (true) {
        result = this._encoder.handler(input, input.read());
        if (result === finished)
          break;
        if (Array.isArray(result))
          output.push.apply(output, /**@type {!Array.<number>}*/(result));
        else
          output.push(result);
      }
      this._encoder = null;
    }
    // 3. If result is finished, convert output into a byte sequence,
    // and then return a Uint8Array object wrapping an ArrayBuffer
    // containing output.
    return new Uint8Array(output);
  };


  //
  // 9. The encoding
  //

  // 9.1 utf-8

  // 9.1.1 utf-8 decoder
  /**
   * @constructor
   * @implements {Decoder}
   * @param {{fatal: boolean}} options
   */
  function UTF8Decoder(options) {
    var fatal = options.fatal;

    // utf-8's decoder's has an associated utf-8 code point, utf-8
    // bytes seen, and utf-8 bytes needed (all initially 0), a utf-8
    // lower boundary (initially 0x80), and a utf-8 upper boundary
    // (initially 0xBF).
    var /** @type {number} */ utf8_code_point = 0,
        /** @type {number} */ utf8_bytes_seen = 0,
        /** @type {number} */ utf8_bytes_needed = 0,
        /** @type {number} */ utf8_lower_boundary = 0x80,
        /** @type {number} */ utf8_upper_boundary = 0xBF;

    /**
     * @param {Stream} stream The stream of bytes being decoded.
     * @param {number} bite The next byte read from the stream.
     * @return {?(number|!Array.<number>)} The next code point(s)
     *     decoded, or null if not enough data exists in the input
     *     stream to decode a complete code point.
     */
    this.handler = function(stream, bite) {
      // 1. If byte is end-of-stream and utf-8 bytes needed is not 0,
      // set utf-8 bytes needed to 0 and return error.
      if (bite === end_of_stream && utf8_bytes_needed !== 0) {
        utf8_bytes_needed = 0;
        return decoderError(fatal);
      }

      // 2. If byte is end-of-stream, return finished.
      if (bite === end_of_stream)
        return finished;

      // 3. If utf-8 bytes needed is 0, based on byte:
      if (utf8_bytes_needed === 0) {

        // 0x00 to 0x7F
        if (inRange(bite, 0x00, 0x7F)) {
          // Return a code point whose value is byte.
          return bite;
        }

        // 0xC2 to 0xDF
        if (inRange(bite, 0xC2, 0xDF)) {
          // Set utf-8 bytes needed to 1 and utf-8 code point to byte
          // ? 0xC0.
          utf8_bytes_needed = 1;
          utf8_code_point = bite - 0xC0;
        }

        // 0xE0 to 0xEF
        else if (inRange(bite, 0xE0, 0xEF)) {
          // 1. If byte is 0xE0, set utf-8 lower boundary to 0xA0.
          if (bite === 0xE0)
            utf8_lower_boundary = 0xA0;
          // 2. If byte is 0xED, set utf-8 upper boundary to 0x9F.
          if (bite === 0xED)
            utf8_upper_boundary = 0x9F;
          // 3. Set utf-8 bytes needed to 2 and utf-8 code point to
          // byte ? 0xE0.
          utf8_bytes_needed = 2;
          utf8_code_point = bite - 0xE0;
        }

        // 0xF0 to 0xF4
        else if (inRange(bite, 0xF0, 0xF4)) {
          // 1. If byte is 0xF0, set utf-8 lower boundary to 0x90.
          if (bite === 0xF0)
            utf8_lower_boundary = 0x90;
          // 2. If byte is 0xF4, set utf-8 upper boundary to 0x8F.
          if (bite === 0xF4)
            utf8_upper_boundary = 0x8F;
          // 3. Set utf-8 bytes needed to 3 and utf-8 code point to
          // byte ? 0xF0.
          utf8_bytes_needed = 3;
          utf8_code_point = bite - 0xF0;
        }

        // Otherwise
        else {
          // Return error.
          return decoderError(fatal);
        }

        // Then (byte is in the range 0xC2 to 0xF4, inclusive) set
        // utf-8 code point to utf-8 code point << (6 ? utf-8 bytes
        // needed) and return continue.
        utf8_code_point = utf8_code_point << (6 * utf8_bytes_needed);
        return null;
      }

      // 4. If byte is not in the range utf-8 lower boundary to utf-8
      // upper boundary, inclusive, run these substeps:
      if (!inRange(bite, utf8_lower_boundary, utf8_upper_boundary)) {

        // 1. Set utf-8 code point, utf-8 bytes needed, and utf-8
        // bytes seen to 0, set utf-8 lower boundary to 0x80, and set
        // utf-8 upper boundary to 0xBF.
        utf8_code_point = utf8_bytes_needed = utf8_bytes_seen = 0;
        utf8_lower_boundary = 0x80;
        utf8_upper_boundary = 0xBF;

        // 2. Prepend byte to stream.
        stream.prepend(bite);

        // 3. Return error.
        return decoderError(fatal);
      }

      // 5. Set utf-8 lower boundary to 0x80 and utf-8 upper boundary
      // to 0xBF.
      utf8_lower_boundary = 0x80;
      utf8_upper_boundary = 0xBF;

      // 6. Increase utf-8 bytes seen by one and set utf-8 code point
      // to utf-8 code point + (byte ? 0x80) << (6 ? (utf-8 bytes
      // needed ? utf-8 bytes seen)).
      utf8_bytes_seen += 1;
      utf8_code_point += (bite - 0x80) << (6 * (utf8_bytes_needed -
                                                utf8_bytes_seen));

      // 7. If utf-8 bytes seen is not equal to utf-8 bytes needed,
      // continue.
      if (utf8_bytes_seen !== utf8_bytes_needed)
        return null;

      // 8. Let code point be utf-8 code point.
      var code_point = utf8_code_point;

      // 9. Set utf-8 code point, utf-8 bytes needed, and utf-8 bytes
      // seen to 0.
      utf8_code_point = utf8_bytes_needed = utf8_bytes_seen = 0;

      // 10. Return a code point whose value is code point.
      return code_point;
    };
  }

  // 9.1.2 utf-8 encoder
  /**
   * @constructor
   * @implements {Encoder}
   * @param {{fatal: boolean}} options
   */
  function UTF8Encoder(options) {
    var fatal = options.fatal;
    /**
     * @param {Stream} stream Input stream.
     * @param {number} code_point Next code point read from the stream.
     * @return {(number|!Array.<number>)} Byte(s) to emit.
     */
    this.handler = function(stream, code_point) {
      // 1. If code point is end-of-stream, return finished.
      if (code_point === end_of_stream)
        return finished;

      // 2. If code point is in the range U+0000 to U+007F, return a
      // byte whose value is code point.
      if (inRange(code_point, 0x0000, 0x007f))
        return code_point;

      // 3. Set count and offset based on the range code point is in:
      var count, offset;
      // U+0080 to U+07FF, inclusive:
      if (inRange(code_point, 0x0080, 0x07FF)) {
        // 1 and 0xC0
        count = 1;
        offset = 0xC0;
      }
      // U+0800 to U+FFFF, inclusive:
      else if (inRange(code_point, 0x0800, 0xFFFF)) {
        // 2 and 0xE0
        count = 2;
        offset = 0xE0;
      }
      // U+10000 to U+10FFFF, inclusive:
      else if (inRange(code_point, 0x10000, 0x10FFFF)) {
        // 3 and 0xF0
        count = 3;
        offset = 0xF0;
      }

      // 4.Let bytes be a byte sequence whose first byte is (code
      // point >> (6 ? count)) + offset.
      var bytes = [(code_point >> (6 * count)) + offset];

      // 5. Run these substeps while count is greater than 0:
      while (count > 0) {

        // 1. Set temp to code point >> (6 ? (count ? 1)).
        var temp = code_point >> (6 * (count - 1));

        // 2. Append to bytes 0x80 | (temp & 0x3F).
        bytes.push(0x80 | (temp & 0x3F));

        // 3. Decrease count by one.
        count -= 1;
      }

      // 6. Return bytes bytes, in order.
      return bytes;
    };
  }

  /** @param {{fatal: boolean}} options */
  encoders['UTF-8'] = function(options) {
    return new UTF8Encoder(options);
  };
  /** @param {{fatal: boolean}} options */
  decoders['UTF-8'] = function(options) {
    return new UTF8Decoder(options);
  };

  //
  // 10. Legacy single-byte encodings
  //

  // 10.1 single-byte decoder
  /**
   * @constructor
   * @implements {Decoder}
   * @param {!Array.<number>} index The encoding index.
   * @param {{fatal: boolean}} options
   */
  function SingleByteDecoder(index, options) {
    var fatal = options.fatal;
    /**
     * @param {Stream} stream The stream of bytes being decoded.
     * @param {number} bite The next byte read from the stream.
     * @return {?(number|!Array.<number>)} The next code point(s)
     *     decoded, or null if not enough data exists in the input
     *     stream to decode a complete code point.
     */
    this.handler = function(stream, bite) {
      // 1. If byte is end-of-stream, return finished.
      if (bite === end_of_stream)
        return finished;

      // 2. If byte is an ASCII byte, return a code point whose value
      // is byte.
      if (isASCIIByte(bite))
        return bite;

      // 3. Let code point be the index code point for byte ? 0x80 in
      // index single-byte.
      var code_point = index[bite - 0x80];

      // 4. If code point is null, return error.
      if (code_point === null)
        return decoderError(fatal);

      // 5. Return a code point whose value is code point.
      return code_point;
    };
  }

  // 10.2 single-byte encoder
  /**
   * @constructor
   * @implements {Encoder}
   * @param {!Array.<?number>} index The encoding index.
   * @param {{fatal: boolean}} options
   */
  function SingleByteEncoder(index, options) {
    var fatal = options.fatal;
    /**
     * @param {Stream} stream Input stream.
     * @param {number} code_point Next code point read from the stream.
     * @return {(number|!Array.<number>)} Byte(s) to emit.
     */
    this.handler = function(stream, code_point) {
      // 1. If code point is end-of-stream, return finished.
      if (code_point === end_of_stream)
        return finished;

      // 2. If code point is an ASCII code point, return a byte whose
      // value is code point.
      if (isASCIICodePoint(code_point))
        return code_point;

      // 3. Let pointer be the index pointer for code point in index
      // single-byte.
      var pointer = indexPointerFor(code_point, index);

      // 4. If pointer is null, return error with code point.
      if (pointer === null)
        encoderError(code_point);

      // 5. Return a byte whose value is pointer + 0x80.
      return pointer + 0x80;
    };
  }

  (function() {
    if (!('encoding-indexes' in global))
      return;
    encodings.forEach(function(category) {
      if (category.heading !== 'Legacy single-byte encodings')
        return;
      category.encodings.forEach(function(encoding) {
        var name = encoding.name;
        var idx = index(name.toLowerCase());
        /** @param {{fatal: boolean}} options */
        decoders[name] = function(options) {
          return new SingleByteDecoder(idx, options);
        };
        /** @param {{fatal: boolean}} options */
        encoders[name] = function(options) {
          return new SingleByteEncoder(idx, options);
        };
      });
    });
  }());

  //
  // 11. Legacy multi-byte Chinese (simplified) encodings
  //

  // 11.1 gbk

  // 11.1.1 gbk decoder
  // gbk's decoder is gb18030's decoder.
  /** @param {{fatal: boolean}} options */
  decoders['GBK'] = function(options) {
    return new GB18030Decoder(options);
  };

  // 11.1.2 gbk encoder
  // gbk's encoder is gb18030's encoder with its gbk flag set.
  /** @param {{fatal: boolean}} options */
  encoders['GBK'] = function(options) {
    return new GB18030Encoder(options, true);
  };

  // 11.2 gb18030

  // 11.2.1 gb18030 decoder
  /**
   * @constructor
   * @implements {Decoder}
   * @param {{fatal: boolean}} options
   */
  function GB18030Decoder(options) {
    var fatal = options.fatal;
    // gb18030's decoder has an associated gb18030 first, gb18030
    // second, and gb18030 third (all initially 0x00).
    var /** @type {number} */ gb18030_first = 0x00,
        /** @type {number} */ gb18030_second = 0x00,
        /** @type {number} */ gb18030_third = 0x00;
    /**
     * @param {Stream} stream The stream of bytes being decoded.
     * @param {number} bite The next byte read from the stream.
     * @return {?(number|!Array.<number>)} The next code point(s)
     *     decoded, or null if not enough data exists in the input
     *     stream to decode a complete code point.
     */
    this.handler = function(stream, bite) {
      // 1. If byte is end-of-stream and gb18030 first, gb18030
      // second, and gb18030 third are 0x00, return finished.
      if (bite === end_of_stream && gb18030_first === 0x00 &&
          gb18030_second === 0x00 && gb18030_third === 0x00) {
        return finished;
      }
      // 2. If byte is end-of-stream, and gb18030 first, gb18030
      // second, or gb18030 third is not 0x00, set gb18030 first,
      // gb18030 second, and gb18030 third to 0x00, and return error.
      if (bite === end_of_stream &&
          (gb18030_first !== 0x00 || gb18030_second !== 0x00 ||
           gb18030_third !== 0x00)) {
        gb18030_first = 0x00;
        gb18030_second = 0x00;
        gb18030_third = 0x00;
        decoderError(fatal);
      }
      var code_point;
      // 3. If gb18030 third is not 0x00, run these substeps:
      if (gb18030_third !== 0x00) {
        // 1. Let code point be null.
        code_point = null;
        // 2. If byte is in the range 0x30 to 0x39, set code point to
        // the index gb18030 ranges code point for (((gb18030 first ?
        // 0x81) ? 10 + gb18030 second ? 0x30) ? 126 + gb18030 third ?
        // 0x81) ? 10 + byte ? 0x30.
        if (inRange(bite, 0x30, 0x39)) {
          code_point = indexGB18030RangesCodePointFor(
              (((gb18030_first - 0x81) * 10 + (gb18030_second - 0x30)) * 126 +
               (gb18030_third - 0x81)) * 10 + bite - 0x30);
        }

        // 3. Let buffer be a byte sequence consisting of gb18030
        // second, gb18030 third, and byte, in order.
        var buffer = [gb18030_second, gb18030_third, bite];

        // 4. Set gb18030 first, gb18030 second, and gb18030 third to
        // 0x00.
        gb18030_first = 0x00;
        gb18030_second = 0x00;
        gb18030_third = 0x00;

        // 5. If code point is null, prepend buffer to stream and
        // return error.
        if (code_point === null) {
          stream.prepend(buffer);
          return decoderError(fatal);
        }

        // 6. Return a code point whose value is code point.
        return code_point;
      }

      // 4. If gb18030 second is not 0x00, run these substeps:
      if (gb18030_second !== 0x00) {

        // 1. If byte is in the range 0x81 to 0xFE, set gb18030 third
        // to byte and return continue.
        if (inRange(bite, 0x81, 0xFE)) {
          gb18030_third = bite;
          return null;
        }

        // 2. Prepend gb18030 second followed by byte to stream, set
        // gb18030 first and gb18030 second to 0x00, and return error.
        stream.prepend([gb18030_second, bite]);
        gb18030_first = 0x00;
        gb18030_second = 0x00;
        return decoderError(fatal);
      }

      // 5. If gb18030 first is not 0x00, run these substeps:
      if (gb18030_first !== 0x00) {

        // 1. If byte is in the range 0x30 to 0x39, set gb18030 second
        // to byte and return continue.
        if (inRange(bite, 0x30, 0x39)) {
          gb18030_second = bite;
          return null;
        }

        // 2. Let lead be gb18030 first, let pointer be null, and set
        // gb18030 first to 0x00.
        var lead = gb18030_first;
        var pointer = null;
        gb18030_first = 0x00;

        // 3. Let offset be 0x40 if byte is less than 0x7F and 0x41
        // otherwise.
        var offset = bite < 0x7F ? 0x40 : 0x41;

        // 4. If byte is in the range 0x40 to 0x7E or 0x80 to 0xFE,
        // set pointer to (lead ? 0x81) ? 190 + (byte ? offset).
        if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0x80, 0xFE))
          pointer = (lead - 0x81) * 190 + (bite - offset);

        // 5. Let code point be null if pointer is null and the index
        // code point for pointer in index gb18030 otherwise.
        code_point = pointer === null ? null :
            indexCodePointFor(pointer, index('gb18030'));

        // 6. If code point is null and byte is an ASCII byte, prepend
        // byte to stream.
        if (code_point === null && isASCIIByte(bite))
          stream.prepend(bite);

        // 7. If code point is null, return error.
        if (code_point === null)
          return decoderError(fatal);

        // 8. Return a code point whose value is code point.
        return code_point;
      }

      // 6. If byte is an ASCII byte, return a code point whose value
      // is byte.
      if (isASCIIByte(bite))
        return bite;

      // 7. If byte is 0x80, return code point U+20AC.
      if (bite === 0x80)
        return 0x20AC;

      // 8. If byte is in the range 0x81 to 0xFE, set gb18030 first to
      // byte and return continue.
      if (inRange(bite, 0x81, 0xFE)) {
        gb18030_first = bite;
        return null;
      }

      // 9. Return error.
      return decoderError(fatal);
    };
  }

  // 11.2.2 gb18030 encoder
  /**
   * @constructor
   * @implements {Encoder}
   * @param {{fatal: boolean}} options
   * @param {boolean=} gbk_flag
   */
  function GB18030Encoder(options, gbk_flag) {
    var fatal = options.fatal;
    // gb18030's decoder has an associated gbk flag (initially unset).
    /**
     * @param {Stream} stream Input stream.
     * @param {number} code_point Next code point read from the stream.
     * @return {(number|!Array.<number>)} Byte(s) to emit.
     */
    this.handler = function(stream, code_point) {
      // 1. If code point is end-of-stream, return finished.
      if (code_point === end_of_stream)
        return finished;

      // 2. If code point is an ASCII code point, return a byte whose
      // value is code point.
      if (isASCIICodePoint(code_point))
        return code_point;

      // 3. If code point is U+E5E5, return error with code point.
      if (code_point === 0xE5E5)
        return encoderError(code_point);

      // 4. If the gbk flag is set and code point is U+20AC, return
      // byte 0x80.
      if (gbk_flag && code_point === 0x20AC)
        return 0x80;

      // 5. Let pointer be the index pointer for code point in index
      // gb18030.
      var pointer = indexPointerFor(code_point, index('gb18030'));

      // 6. If pointer is not null, run these substeps:
      if (pointer !== null) {

        // 1. Let lead be floor(pointer / 190) + 0x81.
        var lead = floor(pointer / 190) + 0x81;

        // 2. Let trail be pointer % 190.
        var trail = pointer % 190;

        // 3. Let offset be 0x40 if trail is less than 0x3F and 0x41 otherwise.
        var offset = trail < 0x3F ? 0x40 : 0x41;

        // 4. Return two bytes whose values are lead and trail + offset.
        return [lead, trail + offset];
      }

      // 7. If gbk flag is set, return error with code point.
      if (gbk_flag)
        return encoderError(code_point);

      // 8. Set pointer to the index gb18030 ranges pointer for code
      // point.
      pointer = indexGB18030RangesPointerFor(code_point);

      // 9. Let byte1 be floor(pointer / 10 / 126 / 10).
      var byte1 = floor(pointer / 10 / 126 / 10);

      // 10. Set pointer to pointer ? byte1 ? 10 ? 126 ? 10.
      pointer = pointer - byte1 * 10 * 126 * 10;

      // 11. Let byte2 be floor(pointer / 10 / 126).
      var byte2 = floor(pointer / 10 / 126);

      // 12. Set pointer to pointer ? byte2 ? 10 ? 126.
      pointer = pointer - byte2 * 10 * 126;

      // 13. Let byte3 be floor(pointer / 10).
      var byte3 = floor(pointer / 10);

      // 14. Let byte4 be pointer ? byte3 ? 10.
      var byte4 = pointer - byte3 * 10;

      // 15. Return four bytes whose values are byte1 + 0x81, byte2 +
      // 0x30, byte3 + 0x81, byte4 + 0x30.
      return [byte1 + 0x81,
              byte2 + 0x30,
              byte3 + 0x81,
              byte4 + 0x30];
    };
  }

  /** @param {{fatal: boolean}} options */
  encoders['gb18030'] = function(options) {
    return new GB18030Encoder(options);
  };
  /** @param {{fatal: boolean}} options */
  decoders['gb18030'] = function(options) {
    return new GB18030Decoder(options);
  };


  //
  // 12. Legacy multi-byte Chinese (traditional) encodings
  //

  // 12.1 big5

  // 12.1.1 big5 decoder
  /**
   * @constructor
   * @implements {Decoder}
   * @param {{fatal: boolean}} options
   */
  function Big5Decoder(options) {
    var fatal = options.fatal;
    // big5's decoder has an associated big5 lead (initially 0x00).
    var /** @type {number} */ big5_lead = 0x00;

    /**
     * @param {Stream} stream The stream of bytes being decoded.
     * @param {number} bite The next byte read from the stream.
     * @return {?(number|!Array.<number>)} The next code point(s)
     *     decoded, or null if not enough data exists in the input
     *     stream to decode a complete code point.
     */
    this.handler = function(stream, bite) {
      // 1. If byte is end-of-stream and big5 lead is not 0x00, set
      // big5 lead to 0x00 and return error.
      if (bite === end_of_stream && big5_lead !== 0x00) {
        big5_lead = 0x00;
        return decoderError(fatal);
      }

      // 2. If byte is end-of-stream and big5 lead is 0x00, return
      // finished.
      if (bite === end_of_stream && big5_lead === 0x00)
        return finished;

      // 3. If big5 lead is not 0x00, let lead be big5 lead, let
      // pointer be null, set big5 lead to 0x00, and then run these
      // substeps:
      if (big5_lead !== 0x00) {
        var lead = big5_lead;
        var pointer = null;
        big5_lead = 0x00;

        // 1. Let offset be 0x40 if byte is less than 0x7F and 0x62
        // otherwise.
        var offset = bite < 0x7F ? 0x40 : 0x62;

        // 2. If byte is in the range 0x40 to 0x7E or 0xA1 to 0xFE,
        // set pointer to (lead ? 0x81) ? 157 + (byte ? offset).
        if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0xA1, 0xFE))
          pointer = (lead - 0x81) * 157 + (bite - offset);

        // 3. If there is a row in the table below whose first column
        // is pointer, return the two code points listed in its second
        // column
        // Pointer | Code points
        // --------+--------------
        // 1133    | U+00CA U+0304
        // 1135    | U+00CA U+030C
        // 1164    | U+00EA U+0304
        // 1166    | U+00EA U+030C
        switch (pointer) {
          case 1133: return [0x00CA, 0x0304];
          case 1135: return [0x00CA, 0x030C];
          case 1164: return [0x00EA, 0x0304];
          case 1166: return [0x00EA, 0x030C];
        }

        // 4. Let code point be null if pointer is null and the index
        // code point for pointer in index big5 otherwise.
        var code_point = (pointer === null) ? null :
            indexCodePointFor(pointer, index('big5'));

        // 5. If code point is null and byte is an ASCII byte, prepend
        // byte to stream.
        if (code_point === null && isASCIIByte(bite))
          stream.prepend(bite);

        // 6. If code point is null, return error.
        if (code_point === null)
          return decoderError(fatal);

        // 7. Return a code point whose value is code point.
        return code_point;
      }

      // 4. If byte is an ASCII byte, return a code point whose value
      // is byte.
      if (isASCIIByte(bite))
        return bite;

      // 5. If byte is in the range 0x81 to 0xFE, set big5 lead to
      // byte and return continue.
      if (inRange(bite, 0x81, 0xFE)) {
        big5_lead = bite;
        return null;
      }

      // 6. Return error.
      return decoderError(fatal);
    };
  }

  // 12.1.2 big5 encoder
  /**
   * @constructor
   * @implements {Encoder}
   * @param {{fatal: boolean}} options
   */
  function Big5Encoder(options) {
    var fatal = options.fatal;
    /**
     * @param {Stream} stream Input stream.
     * @param {number} code_point Next code point read from the stream.
     * @return {(number|!Array.<number>)} Byte(s) to emit.
     */
    this.handler = function(stream, code_point) {
      // 1. If code point is end-of-stream, return finished.
      if (code_point === end_of_stream)
        return finished;

      // 2. If code point is an ASCII code point, return a byte whose
      // value is code point.
      if (isASCIICodePoint(code_point))
        return code_point;

      // 3. Let pointer be the index big5 pointer for code point.
      var pointer = indexBig5PointerFor(code_point);

      // 4. If pointer is null, return error with code point.
      if (pointer === null)
        return encoderError(code_point);

      // 5. Let lead be floor(pointer / 157) + 0x81.
      var lead = floor(pointer / 157) + 0x81;

      // 6. If lead is less than 0xA1, return error with code point.
      if (lead < 0xA1)
        return encoderError(code_point);

      // 7. Let trail be pointer % 157.
      var trail = pointer % 157;

      // 8. Let offset be 0x40 if trail is less than 0x3F and 0x62
      // otherwise.
      var offset = trail < 0x3F ? 0x40 : 0x62;

      // Return two bytes whose values are lead and trail + offset.
      return [lead, trail + offset];
    };
  }

  /** @param {{fatal: boolean}} options */
  encoders['Big5'] = function(options) {
    return new Big5Encoder(options);
  };
  /** @param {{fatal: boolean}} options */
  decoders['Big5'] = function(options) {
    return new Big5Decoder(options);
  };


  //
  // 13. Legacy multi-byte Japanese encodings
  //

  // 13.1 euc-jp

  // 13.1.1 euc-jp decoder
  /**
   * @constructor
   * @implements {Decoder}
   * @param {{fatal: boolean}} options
   */
  function EUCJPDecoder(options) {
    var fatal = options.fatal;

    // euc-jp's decoder has an associated euc-jp jis0212 flag
    // (initially unset) and euc-jp lead (initially 0x00).
    var /** @type {boolean} */ eucjp_jis0212_flag = false,
        /** @type {number} */ eucjp_lead = 0x00;

    /**
     * @param {Stream} stream The stream of bytes being decoded.
     * @param {number} bite The next byte read from the stream.
     * @return {?(number|!Array.<number>)} The next code point(s)
     *     decoded, or null if not enough data exists in the input
     *     stream to decode a complete code point.
     */
    this.handler = function(stream, bite) {
      // 1. If byte is end-of-stream and euc-jp lead is not 0x00, set
      // euc-jp lead to 0x00, and return error.
      if (bite === end_of_stream && eucjp_lead !== 0x00) {
        eucjp_lead = 0x00;
        return decoderError(fatal);
      }

      // 2. If byte is end-of-stream and euc-jp lead is 0x00, return
      // finished.
      if (bite === end_of_stream && eucjp_lead === 0x00)
        return finished;

      // 3. If euc-jp lead is 0x8E and byte is in the range 0xA1 to
      // 0xDF, set euc-jp lead to 0x00 and return a code point whose
      // value is 0xFF61 + byte ? 0xA1.
      if (eucjp_lead === 0x8E && inRange(bite, 0xA1, 0xDF)) {
        eucjp_lead = 0x00;
        return 0xFF61 + bite - 0xA1;
      }

      // 4. If euc-jp lead is 0x8F and byte is in the range 0xA1 to
      // 0xFE, set the euc-jp jis0212 flag, set euc-jp lead to byte,
      // and return continue.
      if (eucjp_lead === 0x8F && inRange(bite, 0xA1, 0xFE)) {
        eucjp_jis0212_flag = true;
        eucjp_lead = bite;
        return null;
      }

      // 5. If euc-jp lead is not 0x00, let lead be euc-jp lead, set
      // euc-jp lead to 0x00, and run these substeps:
      if (eucjp_lead !== 0x00) {
        var lead = eucjp_lead;
        eucjp_lead = 0x00;

        // 1. Let code point be null.
        var code_point = null;

        // 2. If lead and byte are both in the range 0xA1 to 0xFE, set
        // code point to the index code point for (lead ? 0xA1) ? 94 +
        // byte ? 0xA1 in index jis0208 if the euc-jp jis0212 flag is
        // unset and in index jis0212 otherwise.
        if (inRange(lead, 0xA1, 0xFE) && inRange(bite, 0xA1, 0xFE)) {
          code_point = indexCodePointFor(
            (lead - 0xA1) * 94 + (bite - 0xA1),
            index(!eucjp_jis0212_flag ? 'jis0208' : 'jis0212'));
        }

        // 3. Unset the euc-jp jis0212 flag.
        eucjp_jis0212_flag = false;

        // 4. If byte is not in the range 0xA1 to 0xFE, prepend byte
        // to stream.
        if (!inRange(bite, 0xA1, 0xFE))
          stream.prepend(bite);

        // 5. If code point is null, return error.
        if (code_point === null)
          return decoderError(fatal);

        // 6. Return a code point whose value is code point.
        return code_point;
      }

      // 6. If byte is an ASCII byte, return a code point whose value
      // is byte.
      if (isASCIIByte(bite))
        return bite;

      // 7. If byte is 0x8E, 0x8F, or in the range 0xA1 to 0xFE, set
      // euc-jp lead to byte and return continue.
      if (bite === 0x8E || bite === 0x8F || inRange(bite, 0xA1, 0xFE)) {
        eucjp_lead = bite;
        return null;
      }

      // 8. Return error.
      return decoderError(fatal);
    };
  }

  // 13.1.2 euc-jp encoder
  /**
   * @constructor
   * @implements {Encoder}
   * @param {{fatal: boolean}} options
   */
  function EUCJPEncoder(options) {
    var fatal = options.fatal;
    /**
     * @param {Stream} stream Input stream.
     * @param {number} code_point Next code point read from the stream.
     * @return {(number|!Array.<number>)} Byte(s) to emit.
     */
    this.handler = function(stream, code_point) {
      // 1. If code point is end-of-stream, return finished.
      if (code_point === end_of_stream)
        return finished;

      // 2. If code point is an ASCII code point, return a byte whose
      // value is code point.
      if (isASCIICodePoint(code_point))
        return code_point;

      // 3. If code point is U+00A5, return byte 0x5C.
      if (code_point === 0x00A5)
        return 0x5C;

      // 4. If code point is U+203E, return byte 0x7E.
      if (code_point === 0x203E)
        return 0x7E;

      // 5. If code point is in the range U+FF61 to U+FF9F, return two
      // bytes whose values are 0x8E and code point ? 0xFF61 + 0xA1.
      if (inRange(code_point, 0xFF61, 0xFF9F))
        return [0x8E, code_point - 0xFF61 + 0xA1];

      // 6. If code point is U+2212, set it to U+FF0D.
      if (code_point === 0x2212)
        code_point = 0xFF0D;

      // 7. Let pointer be the index pointer for code point in index
      // jis0208.
      var pointer = indexPointerFor(code_point, index('jis0208'));

      // 8. If pointer is null, return error with code point.
      if (pointer === null)
        return encoderError(code_point);

      // 9. Let lead be floor(pointer / 94) + 0xA1.
      var lead = floor(pointer / 94) + 0xA1;

      // 10. Let trail be pointer % 94 + 0xA1.
      var trail = pointer % 94 + 0xA1;

      // 11. Return two bytes whose values are lead and trail.
      return [lead, trail];
    };
  }

  /** @param {{fatal: boolean}} options */
  encoders['EUC-JP'] = function(options) {
    return new EUCJPEncoder(options);
  };
  /** @param {{fatal: boolean}} options */
  decoders['EUC-JP'] = function(options) {
    return new EUCJPDecoder(options);
  };

  // 13.2 iso-2022-jp

  // 13.2.1 iso-2022-jp decoder
  /**
   * @constructor
   * @implements {Decoder}
   * @param {{fatal: boolean}} options
   */
  function ISO2022JPDecoder(options) {
    var fatal = options.fatal;
    /** @enum */
    var states = {
      ASCII: 0,
      Roman: 1,
      Katakana: 2,
      LeadByte: 3,
      TrailByte: 4,
      EscapeStart: 5,
      Escape: 6
    };
    // iso-2022-jp's decoder has an associated iso-2022-jp decoder
    // state (initially ASCII), iso-2022-jp decoder output state
    // (initially ASCII), iso-2022-jp lead (initially 0x00), and
    // iso-2022-jp output flag (initially unset).
    var /** @type {number} */ iso2022jp_decoder_state = states.ASCII,
        /** @type {number} */ iso2022jp_decoder_output_state = states.ASCII,
        /** @type {number} */ iso2022jp_lead = 0x00,
        /** @type {boolean} */ iso2022jp_output_flag = false;
    /**
     * @param {Stream} stream The stream of bytes being decoded.
     * @param {number} bite The next byte read from the stream.
     * @return {?(number|!Array.<number>)} The next code point(s)
     *     decoded, or null if not enough data exists in the input
     *     stream to decode a complete code point.
     */
    this.handler = function(stream, bite) {
      // switching on iso-2022-jp decoder state:
      switch (iso2022jp_decoder_state) {
      default:
      case states.ASCII:
        // ASCII
        // Based on byte:

        // 0x1B
        if (bite === 0x1B) {
          // Set iso-2022-jp decoder state to escape start and return
          // continue.
          iso2022jp_decoder_state = states.EscapeStart;
          return null;
        }

        // 0x00 to 0x7F, excluding 0x0E, 0x0F, and 0x1B
        if (inRange(bite, 0x00, 0x7F) && bite !== 0x0E
            && bite !== 0x0F && bite !== 0x1B) {
          // Unset the iso-2022-jp output flag and return a code point
          // whose value is byte.
          iso2022jp_output_flag = false;
          return bite;
        }

        // end-of-stream
        if (bite === end_of_stream) {
          // Return finished.
          return finished;
        }

        // Otherwise
        // Unset the iso-2022-jp output flag and return error.
        iso2022jp_output_flag = false;
        return decoderError(fatal);

      case states.Roman:
        // Roman
        // Based on byte:

        // 0x1B
        if (bite === 0x1B) {
          // Set iso-2022-jp decoder state to escape start and return
          // continue.
          iso2022jp_decoder_state = states.EscapeStart;
          return null;
        }

        // 0x5C
        if (bite === 0x5C) {
          // Unset the iso-2022-jp output flag and return code point
          // U+00A5.
          iso2022jp_output_flag = false;
          return 0x00A5;
        }

        // 0x7E
        if (bite === 0x7E) {
          // Unset the iso-2022-jp output flag and return code point
          // U+203E.
          iso2022jp_output_flag = false;
          return 0x203E;
        }

        // 0x00 to 0x7F, excluding 0x0E, 0x0F, 0x1B, 0x5C, and 0x7E
        if (inRange(bite, 0x00, 0x7F) && bite !== 0x0E && bite !== 0x0F
            && bite !== 0x1B && bite !== 0x5C && bite !== 0x7E) {
          // Unset the iso-2022-jp output flag and return a code point
          // whose value is byte.
          iso2022jp_output_flag = false;
          return bite;
        }

        // end-of-stream
        if (bite === end_of_stream) {
          // Return finished.
          return finished;
        }

        // Otherwise
        // Unset the iso-2022-jp output flag and return error.
        iso2022jp_output_flag = false;
        return decoderError(fatal);

      case states.Katakana:
        // Katakana
        // Based on byte:

        // 0x1B
        if (bite === 0x1B) {
          // Set iso-2022-jp decoder state to escape start and return
          // continue.
          iso2022jp_decoder_state = states.EscapeStart;
          return null;
        }

        // 0x21 to 0x5F
        if (inRange(bite, 0x21, 0x5F)) {
          // Unset the iso-2022-jp output flag and return a code point
          // whose value is 0xFF61 + byte ? 0x21.
          iso2022jp_output_flag = false;
          return 0xFF61 + bite - 0x21;
        }

        // end-of-stream
        if (bite === end_of_stream) {
          // Return finished.
          return finished;
        }

        // Otherwise
        // Unset the iso-2022-jp output flag and return error.
        iso2022jp_output_flag = false;
        return decoderError(fatal);

      case states.LeadByte:
        // Lead byte
        // Based on byte:

        // 0x1B
        if (bite === 0x1B) {
          // Set iso-2022-jp decoder state to escape start and return
          // continue.
          iso2022jp_decoder_state = states.EscapeStart;
          return null;
        }

        // 0x21 to 0x7E
        if (inRange(bite, 0x21, 0x7E)) {
          // Unset the iso-2022-jp output flag, set iso-2022-jp lead
          // to byte, iso-2022-jp decoder state to trail byte, and
          // return continue.
          iso2022jp_output_flag = false;
          iso2022jp_lead = bite;
          iso2022jp_decoder_state = states.TrailByte;
          return null;
        }

        // end-of-stream
        if (bite === end_of_stream) {
          // Return finished.
          return finished;
        }

        // Otherwise
        // Unset the iso-2022-jp output flag and return error.
        iso2022jp_output_flag = false;
        return decoderError(fatal);

      case states.TrailByte:
        // Trail byte
        // Based on byte:

        // 0x1B
        if (bite === 0x1B) {
          // Set iso-2022-jp decoder state to escape start and return
          // continue.
          iso2022jp_decoder_state = states.EscapeStart;
          return decoderError(fatal);
        }

        // 0x21 to 0x7E
        if (inRange(bite, 0x21, 0x7E)) {
          // 1. Set the iso-2022-jp decoder state to lead byte.
          iso2022jp_decoder_state = states.LeadByte;

          // 2. Let pointer be (iso-2022-jp lead ? 0x21) ? 94 + byte ? 0x21.
          var pointer = (iso2022jp_lead - 0x21) * 94 + bite - 0x21;

          // 3. Let code point be the index code point for pointer in
          // index jis0208.
          var code_point = indexCodePointFor(pointer, index('jis0208'));

          // 4. If code point is null, return error.
          if (code_point === null)
            return decoderError(fatal);

          // 5. Return a code point whose value is code point.
          return code_point;
        }

        // end-of-stream
        if (bite === end_of_stream) {
          // Set the iso-2022-jp decoder state to lead byte, prepend
          // byte to stream, and return error.
          iso2022jp_decoder_state = states.LeadByte;
          stream.prepend(bite);
          return decoderError(fatal);
        }

        // Otherwise
        // Set iso-2022-jp decoder state to lead byte and return
        // error.
        iso2022jp_decoder_state = states.LeadByte;
        return decoderError(fatal);

      case states.EscapeStart:
        // Escape start

        // 1. If byte is either 0x24 or 0x28, set iso-2022-jp lead to
        // byte, iso-2022-jp decoder state to escape, and return
        // continue.
        if (bite === 0x24 || bite === 0x28) {
          iso2022jp_lead = bite;
          iso2022jp_decoder_state = states.Escape;
          return null;
        }

        // 2. Prepend byte to stream.
        stream.prepend(bite);

        // 3. Unset the iso-2022-jp output flag, set iso-2022-jp
        // decoder state to iso-2022-jp decoder output state, and
        // return error.
        iso2022jp_output_flag = false;
        iso2022jp_decoder_state = iso2022jp_decoder_output_state;
        return decoderError(fatal);

      case states.Escape:
        // Escape

        // 1. Let lead be iso-2022-jp lead and set iso-2022-jp lead to
        // 0x00.
        var lead = iso2022jp_lead;
        iso2022jp_lead = 0x00;

        // 2. Let state be null.
        var state = null;

        // 3. If lead is 0x28 and byte is 0x42, set state to ASCII.
        if (lead === 0x28 && bite === 0x42)
          state = states.ASCII;

        // 4. If lead is 0x28 and byte is 0x4A, set state to Roman.
        if (lead === 0x28 && bite === 0x4A)
          state = states.Roman;

        // 5. If lead is 0x28 and byte is 0x49, set state to Katakana.
        if (lead === 0x28 && bite === 0x49)
          state = states.Katakana;

        // 6. If lead is 0x24 and byte is either 0x40 or 0x42, set
        // state to lead byte.
        if (lead === 0x24 && (bite === 0x40 || bite === 0x42))
          state = states.LeadByte;

        // 7. If state is non-null, run these substeps:
        if (state !== null) {
          // 1. Set iso-2022-jp decoder state and iso-2022-jp decoder
          // output state to states.
          iso2022jp_decoder_state = iso2022jp_decoder_state = state;

          // 2. Let output flag be the iso-2022-jp output flag.
          var output_flag = iso2022jp_output_flag;

          // 3. Set the iso-2022-jp output flag.
          iso2022jp_output_flag = true;

          // 4. Return continue, if output flag is unset, and error
          // otherwise.
          return !output_flag ? null : decoderError(fatal);
        }

        // 8. Prepend lead and byte to stream.
        stream.prepend([lead, bite]);

        // 9. Unset the iso-2022-jp output flag, set iso-2022-jp
        // decoder state to iso-2022-jp decoder output state and
        // return error.
        iso2022jp_output_flag = false;
        iso2022jp_decoder_state = iso2022jp_decoder_output_state;
        return decoderError(fatal);
      }
    };
  }

  // 13.2.2 iso-2022-jp encoder
  /**
   * @constructor
   * @implements {Encoder}
   * @param {{fatal: boolean}} options
   */
  function ISO2022JPEncoder(options) {
    var fatal = options.fatal;
    // iso-2022-jp's encoder has an associated iso-2022-jp encoder
    // state which is one of ASCII, Roman, and jis0208 (initially
    // ASCII).
    /** @enum */
    var states = {
      ASCII: 0,
      Roman: 1,
      jis0208: 2
    };
    var /** @type {number} */ iso2022jp_state = states.ASCII;
    /**
     * @param {Stream} stream Input stream.
     * @param {number} code_point Next code point read from the stream.
     * @return {(number|!Array.<number>)} Byte(s) to emit.
     */
    this.handler = function(stream, code_point) {
      // 1. If code point is end-of-stream and iso-2022-jp encoder
      // state is not ASCII, prepend code point to stream, set
      // iso-2022-jp encoder state to ASCII, and return three bytes
      // 0x1B 0x28 0x42.
      if (code_point === end_of_stream &&
          iso2022jp_state !== states.ASCII) {
        stream.prepend(code_point);
        iso2022jp_state = states.ASCII;
        return [0x1B, 0x28, 0x42];
      }

      // 2. If code point is end-of-stream and iso-2022-jp encoder
      // state is ASCII, return finished.
      if (code_point === end_of_stream && iso2022jp_state === states.ASCII)
        return finished;

      // 3. If ISO-2022-JP encoder state is ASCII or Roman, and code
      // point is U+000E, U+000F, or U+001B, return error with U+FFFD.
      if ((iso2022jp_state === states.ASCII ||
           iso2022jp_state === states.Roman) &&
          (code_point === 0x000E || code_point === 0x000F ||
           code_point === 0x001B)) {
        return encoderError(0xFFFD);
      }

      // 4. If iso-2022-jp encoder state is ASCII and code point is an
      // ASCII code point, return a byte whose value is code point.
      if (iso2022jp_state === states.ASCII &&
          isASCIICodePoint(code_point))
        return code_point;

      // 5. If iso-2022-jp encoder state is Roman and code point is an
      // ASCII code point, excluding U+005C and U+007E, or is U+00A5
      // or U+203E, run these substeps:
      if (iso2022jp_state === states.Roman &&
          ((isASCIICodePoint(code_point) &&
           code_point !== 0x005C && code_point !== 0x007E) ||
          (code_point == 0x00A5 || code_point == 0x203E))) {

        // 1. If code point is an ASCII code point, return a byte
        // whose value is code point.
        if (isASCIICodePoint(code_point))
          return code_point;

        // 2. If code point is U+00A5, return byte 0x5C.
        if (code_point === 0x00A5)
          return 0x5C;

        // 3. If code point is U+203E, return byte 0x7E.
        if (code_point === 0x203E)
          return 0x7E;
      }

      // 6. If code point is an ASCII code point, and iso-2022-jp
      // encoder state is not ASCII, prepend code point to stream, set
      // iso-2022-jp encoder state to ASCII, and return three bytes
      // 0x1B 0x28 0x42.
      if (isASCIICodePoint(code_point) &&
          iso2022jp_state !== states.ASCII) {
        stream.prepend(code_point);
        iso2022jp_state = states.ASCII;
        return [0x1B, 0x28, 0x42];
      }

      // 7. If code point is either U+00A5 or U+203E, and iso-2022-jp
      // encoder state is not Roman, prepend code point to stream, set
      // iso-2022-jp encoder state to Roman, and return three bytes
      // 0x1B 0x28 0x4A.
      if ((code_point === 0x00A5 || code_point === 0x203E) &&
          iso2022jp_state !== states.Roman) {
        stream.prepend(code_point);
        iso2022jp_state = states.Roman;
        return [0x1B, 0x28, 0x4A];
      }

      // 8. If code point is U+2212, set it to U+FF0D.
      if (code_point === 0x2212)
        code_point = 0xFF0D;

      // 9. Let pointer be the index pointer for code point in index
      // jis0208.
      var pointer = indexPointerFor(code_point, index('jis0208'));

      // 10. If pointer is null, return error with code point.
      if (pointer === null)
        return encoderError(code_point);

      // 11. If iso-2022-jp encoder state is not jis0208, prepend code
      // point to stream, set iso-2022-jp encoder state to jis0208,
      // and return three bytes 0x1B 0x24 0x42.
      if (iso2022jp_state !== states.jis0208) {
        stream.prepend(code_point);
        iso2022jp_state = states.jis0208;
        return [0x1B, 0x24, 0x42];
      }

      // 12. Let lead be floor(pointer / 94) + 0x21.
      var lead = floor(pointer / 94) + 0x21;

      // 13. Let trail be pointer % 94 + 0x21.
      var trail = pointer % 94 + 0x21;

      // 14. Return two bytes whose values are lead and trail.
      return [lead, trail];
    };
  }

  /** @param {{fatal: boolean}} options */
  encoders['ISO-2022-JP'] = function(options) {
    return new ISO2022JPEncoder(options);
  };
  /** @param {{fatal: boolean}} options */
  decoders['ISO-2022-JP'] = function(options) {
    return new ISO2022JPDecoder(options);
  };

  // 13.3 shift_jis

  // 13.3.1 shift_jis decoder
  /**
   * @constructor
   * @implements {Decoder}
   * @param {{fatal: boolean}} options
   */
  function ShiftJISDecoder(options) {
    var fatal = options.fatal;
    // shift_jis's decoder has an associated shift_jis lead (initially
    // 0x00).
    var /** @type {number} */ shiftjis_lead = 0x00;
    /**
     * @param {Stream} stream The stream of bytes being decoded.
     * @param {number} bite The next byte read from the stream.
     * @return {?(number|!Array.<number>)} The next code point(s)
     *     decoded, or null if not enough data exists in the input
     *     stream to decode a complete code point.
     */
    this.handler = function(stream, bite) {
      // 1. If byte is end-of-stream and shift_jis lead is not 0x00,
      // set shift_jis lead to 0x00 and return error.
      if (bite === end_of_stream && shiftjis_lead !== 0x00) {
        shiftjis_lead = 0x00;
        return decoderError(fatal);
      }

      // 2. If byte is end-of-stream and shift_jis lead is 0x00,
      // return finished.
      if (bite === end_of_stream && shiftjis_lead === 0x00)
        return finished;

      // 3. If shift_jis lead is not 0x00, let lead be shift_jis lead,
      // let pointer be null, set shift_jis lead to 0x00, and then run
      // these substeps:
      if (shiftjis_lead !== 0x00) {
        var lead = shiftjis_lead;
        var pointer = null;
        shiftjis_lead = 0x00;

        // 1. Let offset be 0x40, if byte is less than 0x7F, and 0x41
        // otherwise.
        var offset = (bite < 0x7F) ? 0x40 : 0x41;

        // 2. Let lead offset be 0x81, if lead is less than 0xA0, and
        // 0xC1 otherwise.
        var lead_offset = (lead < 0xA0) ? 0x81 : 0xC1;

        // 3. If byte is in the range 0x40 to 0x7E or 0x80 to 0xFC,
        // set pointer to (lead ? lead offset) ? 188 + byte ? offset.
        if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0x80, 0xFC))
          pointer = (lead - lead_offset) * 188 + bite - offset;

        // 4. Let code point be null, if pointer is null, and the
        // index code point for pointer in index jis0208 otherwise.
        var code_point = (pointer === null) ? null :
              indexCodePointFor(pointer, index('jis0208'));

        // 5. If code point is null and pointer is in the range 8836
        // to 10528, return a code point whose value is 0xE000 +
        // pointer ? 8836.
        if (code_point === null && pointer !== null &&
            inRange(pointer, 8836, 10528))
          return 0xE000 + pointer - 8836;

        // 6. If code point is null and byte is an ASCII byte, prepend
        // byte to stream.
        if (code_point === null && isASCIIByte(bite))
          stream.prepend(bite);

        // 7. If code point is null, return error.
        if (code_point === null)
          return decoderError(fatal);

        // 8. Return a code point whose value is code point.
        return code_point;
      }

      // 4. If byte is an ASCII byte or 0x80, return a code point
      // whose value is byte.
      if (isASCIIByte(bite) || bite === 0x80)
        return bite;

      // 5. If byte is in the range 0xA1 to 0xDF, return a code point
      // whose value is 0xFF61 + byte ? 0xA1.
      if (inRange(bite, 0xA1, 0xDF))
        return 0xFF61 + bite - 0xA1;

      // 6. If byte is in the range 0x81 to 0x9F or 0xE0 to 0xFC, set
      // shift_jis lead to byte and return continue.
      if (inRange(bite, 0x81, 0x9F) || inRange(bite, 0xE0, 0xFC)) {
        shiftjis_lead = bite;
        return null;
      }

      // 7. Return error.
      return decoderError(fatal);
    };
  }

  // 13.3.2 shift_jis encoder
  /**
   * @constructor
   * @implements {Encoder}
   * @param {{fatal: boolean}} options
   */
  function ShiftJISEncoder(options) {
    var fatal = options.fatal;
    /**
     * @param {Stream} stream Input stream.
     * @param {number} code_point Next code point read from the stream.
     * @return {(number|!Array.<number>)} Byte(s) to emit.
     */
    this.handler = function(stream, code_point) {
      // 1. If code point is end-of-stream, return finished.
      if (code_point === end_of_stream)
        return finished;

      // 2. If code point is an ASCII code point or U+0080, return a
      // byte whose value is code point.
      if (isASCIICodePoint(code_point) || code_point === 0x0080)
        return code_point;

      // 3. If code point is U+00A5, return byte 0x5C.
      if (code_point === 0x00A5)
        return 0x5C;

      // 4. If code point is U+203E, return byte 0x7E.
      if (code_point === 0x203E)
        return 0x7E;

      // 5. If code point is in the range U+FF61 to U+FF9F, return a
      // byte whose value is code point ? 0xFF61 + 0xA1.
      if (inRange(code_point, 0xFF61, 0xFF9F))
        return code_point - 0xFF61 + 0xA1;

      // 6. If code point is U+2212, set it to U+FF0D.
      if (code_point === 0x2212)
        code_point = 0xFF0D;

      // 7. Let pointer be the index shift_jis pointer for code point.
      var pointer = indexShiftJISPointerFor(code_point);

      // 8. If pointer is null, return error with code point.
      if (pointer === null)
        return encoderError(code_point);

      // 9. Let lead be floor(pointer / 188).
      var lead = floor(pointer / 188);

      // 10. Let lead offset be 0x81, if lead is less than 0x1F, and
      // 0xC1 otherwise.
      var lead_offset = (lead < 0x1F) ? 0x81 : 0xC1;

      // 11. Let trail be pointer % 188.
      var trail = pointer % 188;

      // 12. Let offset be 0x40, if trail is less than 0x3F, and 0x41
      // otherwise.
      var offset = (trail < 0x3F) ? 0x40 : 0x41;

      // 13. Return two bytes whose values are lead + lead offset and
      // trail + offset.
      return [lead + lead_offset, trail + offset];
    };
  }

  /** @param {{fatal: boolean}} options */
  encoders['Shift_JIS'] = function(options) {
    return new ShiftJISEncoder(options);
  };
  /** @param {{fatal: boolean}} options */
  decoders['Shift_JIS'] = function(options) {
    return new ShiftJISDecoder(options);
  };

  //
  // 14. Legacy multi-byte Korean encodings
  //

  // 14.1 euc-kr

  // 14.1.1 euc-kr decoder
  /**
   * @constructor
   * @implements {Decoder}
   * @param {{fatal: boolean}} options
   */
  function EUCKRDecoder(options) {
    var fatal = options.fatal;

    // euc-kr's decoder has an associated euc-kr lead (initially 0x00).
    var /** @type {number} */ euckr_lead = 0x00;
    /**
     * @param {Stream} stream The stream of bytes being decoded.
     * @param {number} bite The next byte read from the stream.
     * @return {?(number|!Array.<number>)} The next code point(s)
     *     decoded, or null if not enough data exists in the input
     *     stream to decode a complete code point.
     */
    this.handler = function(stream, bite) {
      // 1. If byte is end-of-stream and euc-kr lead is not 0x00, set
      // euc-kr lead to 0x00 and return error.
      if (bite === end_of_stream && euckr_lead !== 0) {
        euckr_lead = 0x00;
        return decoderError(fatal);
      }

      // 2. If byte is end-of-stream and euc-kr lead is 0x00, return
      // finished.
      if (bite === end_of_stream && euckr_lead === 0)
        return finished;

      // 3. If euc-kr lead is not 0x00, let lead be euc-kr lead, let
      // pointer be null, set euc-kr lead to 0x00, and then run these
      // substeps:
      if (euckr_lead !== 0x00) {
        var lead = euckr_lead;
        var pointer = null;
        euckr_lead = 0x00;

        // 1. If byte is in the range 0x41 to 0xFE, set pointer to
        // (lead ? 0x81) ? 190 + (byte ? 0x41).
        if (inRange(bite, 0x41, 0xFE))
          pointer = (lead - 0x81) * 190 + (bite - 0x41);

        // 2. Let code point be null, if pointer is null, and the
        // index code point for pointer in index euc-kr otherwise.
        var code_point = (pointer === null)
              ? null : indexCodePointFor(pointer, index('euc-kr'));

        // 3. If code point is null and byte is an ASCII byte, prepend
        // byte to stream.
        if (pointer === null && isASCIIByte(bite))
          stream.prepend(bite);

        // 4. If code point is null, return error.
        if (code_point === null)
          return decoderError(fatal);

        // 5. Return a code point whose value is code point.
        return code_point;
      }

      // 4. If byte is an ASCII byte, return a code point whose value
      // is byte.
      if (isASCIIByte(bite))
        return bite;

      // 5. If byte is in the range 0x81 to 0xFE, set euc-kr lead to
      // byte and return continue.
      if (inRange(bite, 0x81, 0xFE)) {
        euckr_lead = bite;
        return null;
      }

      // 6. Return error.
      return decoderError(fatal);
    };
  }

  // 14.1.2 euc-kr encoder
  /**
   * @constructor
   * @implements {Encoder}
   * @param {{fatal: boolean}} options
   */
  function EUCKREncoder(options) {
    var fatal = options.fatal;
    /**
     * @param {Stream} stream Input stream.
     * @param {number} code_point Next code point read from the stream.
     * @return {(number|!Array.<number>)} Byte(s) to emit.
     */
    this.handler = function(stream, code_point) {
      // 1. If code point is end-of-stream, return finished.
      if (code_point === end_of_stream)
        return finished;

      // 2. If code point is an ASCII code point, return a byte whose
      // value is code point.
      if (isASCIICodePoint(code_point))
        return code_point;

      // 3. Let pointer be the index pointer for code point in index
      // euc-kr.
      var pointer = indexPointerFor(code_point, index('euc-kr'));

      // 4. If pointer is null, return error with code point.
      if (pointer === null)
        return encoderError(code_point);

      // 5. Let lead be floor(pointer / 190) + 0x81.
      var lead = floor(pointer / 190) + 0x81;

      // 6. Let trail be pointer % 190 + 0x41.
      var trail = (pointer % 190) + 0x41;

      // 7. Return two bytes whose values are lead and trail.
      return [lead, trail];
    };
  }

  /** @param {{fatal: boolean}} options */
  encoders['EUC-KR'] = function(options) {
    return new EUCKREncoder(options);
  };
  /** @param {{fatal: boolean}} options */
  decoders['EUC-KR'] = function(options) {
    return new EUCKRDecoder(options);
  };


  //
  // 15. Legacy miscellaneous encodings
  //

  // 15.1 replacement

  // Not needed - API throws RangeError

  // 15.2 Common infrastructure for utf-16be and utf-16le

  /**
   * @param {number} code_unit
   * @param {boolean} utf16be
   * @return {!Array.<number>} bytes
   */
  function convertCodeUnitToBytes(code_unit, utf16be) {
    // 1. Let byte1 be code unit >> 8.
    var byte1 = code_unit >> 8;

    // 2. Let byte2 be code unit & 0x00FF.
    var byte2 = code_unit & 0x00FF;

    // 3. Then return the bytes in order:
        // utf-16be flag is set: byte1, then byte2.
    if (utf16be)
      return [byte1, byte2];
    // utf-16be flag is unset: byte2, then byte1.
    return [byte2, byte1];
  }

  // 15.2.1 shared utf-16 decoder
  /**
   * @constructor
   * @implements {Decoder}
   * @param {boolean} utf16_be True if big-endian, false if little-endian.
   * @param {{fatal: boolean}} options
   */
  function UTF16Decoder(utf16_be, options) {
    var fatal = options.fatal;
    var /** @type {?number} */ utf16_lead_byte = null,
        /** @type {?number} */ utf16_lead_surrogate = null;
    /**
     * @param {Stream} stream The stream of bytes being decoded.
     * @param {number} bite The next byte read from the stream.
     * @return {?(number|!Array.<number>)} The next code point(s)
     *     decoded, or null if not enough data exists in the input
     *     stream to decode a complete code point.
     */
    this.handler = function(stream, bite) {
      // 1. If byte is end-of-stream and either utf-16 lead byte or
      // utf-16 lead surrogate is not null, set utf-16 lead byte and
      // utf-16 lead surrogate to null, and return error.
      if (bite === end_of_stream && (utf16_lead_byte !== null ||
                                utf16_lead_surrogate !== null)) {
        return decoderError(fatal);
      }

      // 2. If byte is end-of-stream and utf-16 lead byte and utf-16
      // lead surrogate are null, return finished.
      if (bite === end_of_stream && utf16_lead_byte === null &&
          utf16_lead_surrogate === null) {
        return finished;
      }

      // 3. If utf-16 lead byte is null, set utf-16 lead byte to byte
      // and return continue.
      if (utf16_lead_byte === null) {
        utf16_lead_byte = bite;
        return null;
      }

      // 4. Let code unit be the result of:
      var code_unit;
      if (utf16_be) {
        // utf-16be decoder flag is set
        //   (utf-16 lead byte << 8) + byte.
        code_unit = (utf16_lead_byte << 8) + bite;
      } else {
        // utf-16be decoder flag is unset
        //   (byte << 8) + utf-16 lead byte.
        code_unit = (bite << 8) + utf16_lead_byte;
      }
      // Then set utf-16 lead byte to null.
      utf16_lead_byte = null;

      // 5. If utf-16 lead surrogate is not null, let lead surrogate
      // be utf-16 lead surrogate, set utf-16 lead surrogate to null,
      // and then run these substeps:
      if (utf16_lead_surrogate !== null) {
        var lead_surrogate = utf16_lead_surrogate;
        utf16_lead_surrogate = null;

        // 1. If code unit is in the range U+DC00 to U+DFFF, return a
        // code point whose value is 0x10000 + ((lead surrogate ?
        // 0xD800) << 10) + (code unit ? 0xDC00).
        if (inRange(code_unit, 0xDC00, 0xDFFF)) {
          return 0x10000 + (lead_surrogate - 0xD800) * 0x400 +
              (code_unit - 0xDC00);
        }

        // 2. Prepend the sequence resulting of converting code unit
        // to bytes using utf-16be decoder flag to stream and return
        // error.
        stream.prepend(convertCodeUnitToBytes(code_unit, utf16_be));
        return decoderError(fatal);
      }

      // 6. If code unit is in the range U+D800 to U+DBFF, set utf-16
      // lead surrogate to code unit and return continue.
      if (inRange(code_unit, 0xD800, 0xDBFF)) {
        utf16_lead_surrogate = code_unit;
        return null;
      }

      // 7. If code unit is in the range U+DC00 to U+DFFF, return
      // error.
      if (inRange(code_unit, 0xDC00, 0xDFFF))
        return decoderError(fatal);

      // 8. Return code point code unit.
      return code_unit;
    };
  }

  // 15.2.2 shared utf-16 encoder
  /**
   * @constructor
   * @implements {Encoder}
   * @param {boolean} utf16_be True if big-endian, false if little-endian.
   * @param {{fatal: boolean}} options
   */
  function UTF16Encoder(utf16_be, options) {
    var fatal = options.fatal;
    /**
     * @param {Stream} stream Input stream.
     * @param {number} code_point Next code point read from the stream.
     * @return {(number|!Array.<number>)} Byte(s) to emit.
     */
    this.handler = function(stream, code_point) {
      // 1. If code point is end-of-stream, return finished.
      if (code_point === end_of_stream)
        return finished;

      // 2. If code point is in the range U+0000 to U+FFFF, return the
      // sequence resulting of converting code point to bytes using
      // utf-16be encoder flag.
      if (inRange(code_point, 0x0000, 0xFFFF))
        return convertCodeUnitToBytes(code_point, utf16_be);

      // 3. Let lead be ((code point ? 0x10000) >> 10) + 0xD800,
      // converted to bytes using utf-16be encoder flag.
      var lead = convertCodeUnitToBytes(
        ((code_point - 0x10000) >> 10) + 0xD800, utf16_be);

      // 4. Let trail be ((code point ? 0x10000) & 0x3FF) + 0xDC00,
      // converted to bytes using utf-16be encoder flag.
      var trail = convertCodeUnitToBytes(
        ((code_point - 0x10000) & 0x3FF) + 0xDC00, utf16_be);

      // 5. Return a byte sequence of lead followed by trail.
      return lead.concat(trail);
    };
  }

  // 15.3 utf-16be
  // 15.3.1 utf-16be decoder
  /** @param {{fatal: boolean}} options */
  encoders['UTF-16BE'] = function(options) {
    return new UTF16Encoder(true, options);
  };
  // 15.3.2 utf-16be encoder
  /** @param {{fatal: boolean}} options */
  decoders['UTF-16BE'] = function(options) {
    return new UTF16Decoder(true, options);
  };

  // 15.4 utf-16le
  // 15.4.1 utf-16le decoder
  /** @param {{fatal: boolean}} options */
  encoders['UTF-16LE'] = function(options) {
    return new UTF16Encoder(false, options);
  };
  // 15.4.2 utf-16le encoder
  /** @param {{fatal: boolean}} options */
  decoders['UTF-16LE'] = function(options) {
    return new UTF16Decoder(false, options);
  };

  // 15.5 x-user-defined

  // 15.5.1 x-user-defined decoder
  /**
   * @constructor
   * @implements {Decoder}
   * @param {{fatal: boolean}} options
   */
  function XUserDefinedDecoder(options) {
    var fatal = options.fatal;
    /**
     * @param {Stream} stream The stream of bytes being decoded.
     * @param {number} bite The next byte read from the stream.
     * @return {?(number|!Array.<number>)} The next code point(s)
     *     decoded, or null if not enough data exists in the input
     *     stream to decode a complete code point.
     */
    this.handler = function(stream, bite) {
      // 1. If byte is end-of-stream, return finished.
      if (bite === end_of_stream)
        return finished;

      // 2. If byte is an ASCII byte, return a code point whose value
      // is byte.
      if (isASCIIByte(bite))
        return bite;

      // 3. Return a code point whose value is 0xF780 + byte ? 0x80.
      return 0xF780 + bite - 0x80;
    };
  }

  // 15.5.2 x-user-defined encoder
  /**
   * @constructor
   * @implements {Encoder}
   * @param {{fatal: boolean}} options
   */
  function XUserDefinedEncoder(options) {
    var fatal = options.fatal;
    /**
     * @param {Stream} stream Input stream.
     * @param {number} code_point Next code point read from the stream.
     * @return {(number|!Array.<number>)} Byte(s) to emit.
     */
    this.handler = function(stream, code_point) {
      // 1.If code point is end-of-stream, return finished.
      if (code_point === end_of_stream)
        return finished;

      // 2. If code point is an ASCII code point, return a byte whose
      // value is code point.
      if (isASCIICodePoint(code_point))
        return code_point;

      // 3. If code point is in the range U+F780 to U+F7FF, return a
      // byte whose value is code point ? 0xF780 + 0x80.
      if (inRange(code_point, 0xF780, 0xF7FF))
        return code_point - 0xF780 + 0x80;

      // 4. Return error with code point.
      return encoderError(code_point);
    };
  }

  /** @param {{fatal: boolean}} options */
  encoders['x-user-defined'] = function(options) {
    return new XUserDefinedEncoder(options);
  };
  /** @param {{fatal: boolean}} options */
  decoders['x-user-defined'] = function(options) {
    return new XUserDefinedDecoder(options);
  };

  if (!global['TextEncoder'])
    global['TextEncoder'] = TextEncoder;
  if (!global['TextDecoder'])
    global['TextDecoder'] = TextDecoder;

  if (typeof module !== "undefined" && module.exports) {
    module.exports = {
      TextEncoder: global['TextEncoder'],
      TextDecoder: global['TextDecoder'],
      EncodingIndexes: global["encoding-indexes"]
    };
  }
}(this));
//
// this is a patch that provides fetch with keep alive support when making
// synchronous requests during unload that finish in a 0 status, a problem
// introduced by Chrome 80
//
(function () {
    var origInitByEnvironment;

    //
    // only apply the patch if this browser has the capability to do that type
    // of request
    //
    // have to support fetch() and keepalive flag to continue
    if (typeof fetch !== "function" || ! ("keepalive" in new Request(""))) {
        return;
    }

    //
    // because the type of request used for a particular LRS depends on the
    // URL (CORS, etc.) of the endpoint we can't determine the type of request
    // and whether we can do anything about it until construction of the LRS
    // itself which does an Environment initialization procedure, so this
    // patch is overloading that initialization so that we can wrap the request
    // method that is assigned into the LRS object with a function that can
    // detect and handle the specific case we're after, and passthrough any
    // case we're not attempting to fix
    //
    origInitByEnvironment = TinCan.LRS.prototype._initByEnvironment;

    TinCan.LRS.prototype._initByEnvironment = function (cfg) {
        // _makeRequest is apparently not called with `this` bound correctly
        var lrs = this,
            origMakeRequest;

        // call the original to set up the LRS object (`this`) as it would normally
        origInitByEnvironment.call(this, cfg);

        //
        // after the original call the `_makeRequest` function will have been set up
        // to do what it needs to do, that function will return the XHR object when
        // it is called in a synchronous manner, which gives us a way to override
        // the behavior when we detect the condition we care about, specifically a
        // return status of 0
        //
        origMakeRequest = this._makeRequest;

        this._makeRequest = function (_fullUrl, headers, cfg) {
            var origResult = origMakeRequest.apply(lrs, arguments),
                pairs = [],
                fullUrl = _fullUrl,
                fetchRequestCfg;

            // not sure how, but we got an async request which this patch isn't
            // trying to solve for, so just return
            if (typeof cfg.callback !== "undefined") {
                return origResult;
            }

            // original request is unrecognized, succeeded, or doesn't indicate
            // a network error (zero status) so nothing else to do here
            //
            // the `err` property of the result indicates the HTTP status returned
            // from the request, a 0 status code is returned for the condition we're
            // attempting to fix with this patch, but could be returned in other cases
            // where trying a `fetch` is fine, but isn't likely to work either (like
            // network failures, etc.)
            //
            if (typeof origResult.err === "undefined" || origResult.err === null || origResult.err !== 0) {
                return origResult;
            }

            /*
            The straightforward way to achieve this should be to just construct
            a request using effectively the same values as we sent for the native
            request, but formatted in such a way that we can pass it to fetch which
            is what the original attempt, still shown here, was which mostly worked,
            but owing to another bug in Chrome that prevents CORS requests to be
            sent in the manner below (specifically with `keepalive`) we couldn't use
            this method.

            //
            // unfortunately logic in the original `nativeRequest` needs to be
            // duplicated here to achieve the same request
            //
            for (prop in cfg.params) {
                if (cfg.params.hasOwnProperty(prop)) {
                    pairs.push(prop + "=" + encodeURIComponent(cfg.params[prop]));
                }
            }

            if (pairs.length > 0) {
                fullUrl += "?" + pairs.join("&");
            }

            //
            // the rest of this is our patch to send the newer type of request
            //
            fetchRequestCfg = {
                mode: "cors",
                cache: "no-cache",
                credentials: "include",
                keepalive: true,

                method: cfg.method,
                headers: headers
            }
            if (cfg.data) {
                fetchRequestCfg.body = cfg.data;
            }
            */

            /*
            This is the alternate method being used as described above, to avoid
            the CORS problem switch to an "IE Mode request" set up and use that
            but still within the confines of a keep alive fetch request
            */

            //
            // unfortunately logic in the original `nativeRequest` needs to be
            // duplicated here to achieve the same request
            //
            for (prop in cfg.params) {
                if (cfg.params.hasOwnProperty(prop)) {
                    pairs.push(prop + "=" + encodeURIComponent(cfg.params[prop]));
                }
            }

            fullUrl = lrs._IEModeConversion(fullUrl, headers, pairs, cfg);


            //
            // the rest of this is our patch to send the newer type of request
            //
            fetchRequestCfg = {
                mode: "cors",
                cache: "no-cache",
                credentials: "include",
                keepalive: true,

                method: cfg.method,
                headers: {
                    //
                    // TinCanJS' IE mode conversion routine alters the `headers` object
                    // and copies the existing headers into the body content, but doesn't
                    // remove the copied headers from the object so we can't wholesale
                    // use what is left, so only pull out the Content-Type since it was
                    // changed by that function, any other header would trigger a CORS
                    // pre-flight request here which isn't currently (as of Chrome 79)
                    // supported for `fetch` requests
                    //
                    "Content-Type": headers["Content-Type"]
                },
                body: cfg.data
            }

            try {
                //
                // we have promise handlers here but in the primary case that this code targets,
                // synchronous requests during exit, those handlers will never get hit so these
                // log messages are likely not to be seen
                //
                fetch(fullUrl, fetchRequestCfg).then(
                    function (response) {
                        lrs.log("Overridden request Fetch with KeepAlive finished with status " + response.status + ":" + response.statusText);
                    }
                )
                ["catch"]( // IE8 has .catch as reserved, so use bracket notation
                    function (e) {
                        lrs.log("Overridden request Fetch with KeepAlive returned error: " + e.message);
                    }
                );

                //
                // We're making an assumption here that if we can fire this request, it
                // will complete successfully. Since we couldn't deal with a failure
                // anyway, there's no point in handling the Promise returned by the
                // fetch call.
                //
                // Not returning an object that is normally recognized does prevent
                // subsequent requests from not happening, so even though we can't detect
                // success/failure, this needs to at least act like success so that the
                // other requests are made
                //
                return {
                    err: null
                };
            }
            catch (e) {
                // This will likely never get hit, but we don't want any error to bubble up from this method
                lrs.log("Overridden request Fetch with KeepAlive threw error: " + e.message);

                return {
                    err: e.message,
                    xhr: null
                };
            }
        };
    };
}());
/*! http://mths.be/punycode v1.2.3 by @mathias */
;(function(root) {

	/** Detect free variables */
	var freeExports = typeof exports == 'object' && exports;
	var freeModule = typeof module == 'object' && module &&
		module.exports == freeExports && module;
	var freeGlobal = typeof global == 'object' && global;
	if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) {
		root = freeGlobal;
	}

	/**
	 * The `punycode` object.
	 * @name punycode
	 * @type Object
	 */
	var punycode,

	/** Highest positive signed 32-bit float value */
	maxInt = 2147483647, // aka. 0x7FFFFFFF or 2^31-1

	/** Bootstring parameters */
	base = 36,
	tMin = 1,
	tMax = 26,
	skew = 38,
	damp = 700,
	initialBias = 72,
	initialN = 128, // 0x80
	delimiter = '-', // '\x2D'

	/** Regular expressions */
	regexPunycode = /^xn--/,
	regexNonASCII = /[^ -~]/, // unprintable ASCII chars + non-ASCII chars
	regexSeparators = /\x2E|\u3002|\uFF0E|\uFF61/g, // RFC 3490 separators

	/** Error messages */
	errors = {
		'overflow': 'Overflow: input needs wider integers to process',
		'not-basic': 'Illegal input >= 0x80 (not a basic code point)',
		'invalid-input': 'Invalid input'
	},

	/** Convenience shortcuts */
	baseMinusTMin = base - tMin,
	floor = Math.floor,
	stringFromCharCode = String.fromCharCode,

	/** Temporary variable */
	key;

	/*--------------------------------------------------------------------------*/

	/**
	 * A generic error utility function.
	 * @private
	 * @param {String} type The error type.
	 * @returns {Error} Throws a `RangeError` with the applicable error message.
	 */
	function error(type) {
		throw RangeError(errors[type]);
	}

	/**
	 * A generic `Array#map` utility function.
	 * @private
	 * @param {Array} array The array to iterate over.
	 * @param {Function} callback The function that gets called for every array
	 * item.
	 * @returns {Array} A new array of values returned by the callback function.
	 */
	function map(array, fn) {
		var length = array.length;
		while (length--) {
			array[length] = fn(array[length]);
		}
		return array;
	}

	/**
	 * A simple `Array#map`-like wrapper to work with domain name strings.
	 * @private
	 * @param {String} domain The domain name.
	 * @param {Function} callback The function that gets called for every
	 * character.
	 * @returns {Array} A new string of characters returned by the callback
	 * function.
	 */
	function mapDomain(string, fn) {
		return map(string.split(regexSeparators), fn).join('.');
	}

	/**
	 * Creates an array containing the numeric code points of each Unicode
	 * character in the string. While JavaScript uses UCS-2 internally,
	 * this function will convert a pair of surrogate halves (each of which
	 * UCS-2 exposes as separate characters) into a single code point,
	 * matching UTF-16.
	 * @see `punycode.ucs2.encode`
	 * @see <http://mathiasbynens.be/notes/javascript-encoding>
	 * @memberOf punycode.ucs2
	 * @name decode
	 * @param {String} string The Unicode input string (UCS-2).
	 * @returns {Array} The new array of code points.
	 */
	function ucs2decode(string) {
		var output = [],
		    counter = 0,
		    length = string.length,
		    value,
		    extra;
		while (counter < length) {
			value = string.charCodeAt(counter++);
			if (value >= 0xD800 && value <= 0xDBFF && counter < length) {
				// high surrogate, and there is a next character
				extra = string.charCodeAt(counter++);
				if ((extra & 0xFC00) == 0xDC00) { // low surrogate
					output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000);
				} else {
					// unmatched surrogate; only append this code unit, in case the next
					// code unit is the high surrogate of a surrogate pair
					output.push(value);
					counter--;
				}
			} else {
				output.push(value);
			}
		}
		return output;
	}

	/**
	 * Creates a string based on an array of numeric code points.
	 * @see `punycode.ucs2.decode`
	 * @memberOf punycode.ucs2
	 * @name encode
	 * @param {Array} codePoints The array of numeric code points.
	 * @returns {String} The new Unicode string (UCS-2).
	 */
	function ucs2encode(array) {
		return map(array, function(value) {
			var output = '';
			if (value > 0xFFFF) {
				value -= 0x10000;
				output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800);
				value = 0xDC00 | value & 0x3FF;
			}
			output += stringFromCharCode(value);
			return output;
		}).join('');
	}

	/**
	 * Converts a basic code point into a digit/integer.
	 * @see `digitToBasic()`
	 * @private
	 * @param {Number} codePoint The basic numeric code point value.
	 * @returns {Number} The numeric value of a basic code point (for use in
	 * representing integers) in the range `0` to `base - 1`, or `base` if
	 * the code point does not represent a value.
	 */
	function basicToDigit(codePoint) {
		if (codePoint - 48 < 10) {
			return codePoint - 22;
		}
		if (codePoint - 65 < 26) {
			return codePoint - 65;
		}
		if (codePoint - 97 < 26) {
			return codePoint - 97;
		}
		return base;
	}

	/**
	 * Converts a digit/integer into a basic code point.
	 * @see `basicToDigit()`
	 * @private
	 * @param {Number} digit The numeric value of a basic code point.
	 * @returns {Number} The basic code point whose value (when used for
	 * representing integers) is `digit`, which needs to be in the range
	 * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is
	 * used; else, the lowercase form is used. The behavior is undefined
	 * if `flag` is non-zero and `digit` has no uppercase form.
	 */
	function digitToBasic(digit, flag) {
		//  0..25 map to ASCII a..z or A..Z
		// 26..35 map to ASCII 0..9
		return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5);
	}

	/**
	 * Bias adaptation function as per section 3.4 of RFC 3492.
	 * http://tools.ietf.org/html/rfc3492#section-3.4
	 * @private
	 */
	function adapt(delta, numPoints, firstTime) {
		var k = 0;
		delta = firstTime ? floor(delta / damp) : delta >> 1;
		delta += floor(delta / numPoints);
		for (/* no initialization */; delta > baseMinusTMin * tMax >> 1; k += base) {
			delta = floor(delta / baseMinusTMin);
		}
		return floor(k + (baseMinusTMin + 1) * delta / (delta + skew));
	}

	/**
	 * Converts a Punycode string of ASCII-only symbols to a string of Unicode
	 * symbols.
	 * @memberOf punycode
	 * @param {String} input The Punycode string of ASCII-only symbols.
	 * @returns {String} The resulting string of Unicode symbols.
	 */
	function decode(input) {
		// Don't use UCS-2
		var output = [],
		    inputLength = input.length,
		    out,
		    i = 0,
		    n = initialN,
		    bias = initialBias,
		    basic,
		    j,
		    index,
		    oldi,
		    w,
		    k,
		    digit,
		    t,
		    length,
		    /** Cached calculation results */
		    baseMinusT;

		// Handle the basic code points: let `basic` be the number of input code
		// points before the last delimiter, or `0` if there is none, then copy
		// the first basic code points to the output.

		basic = input.lastIndexOf(delimiter);
		if (basic < 0) {
			basic = 0;
		}

		for (j = 0; j < basic; ++j) {
			// if it's not a basic code point
			if (input.charCodeAt(j) >= 0x80) {
				error('not-basic');
			}
			output.push(input.charCodeAt(j));
		}

		// Main decoding loop: start just after the last delimiter if any basic code
		// points were copied; start at the beginning otherwise.

		for (index = basic > 0 ? basic + 1 : 0; index < inputLength; /* no final expression */) {

			// `index` is the index of the next character to be consumed.
			// Decode a generalized variable-length integer into `delta`,
			// which gets added to `i`. The overflow checking is easier
			// if we increase `i` as we go, then subtract off its starting
			// value at the end to obtain `delta`.
			for (oldi = i, w = 1, k = base; /* no condition */; k += base) {

				if (index >= inputLength) {
					error('invalid-input');
				}

				digit = basicToDigit(input.charCodeAt(index++));

				if (digit >= base || digit > floor((maxInt - i) / w)) {
					error('overflow');
				}

				i += digit * w;
				t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias);

				if (digit < t) {
					break;
				}

				baseMinusT = base - t;
				if (w > floor(maxInt / baseMinusT)) {
					error('overflow');
				}

				w *= baseMinusT;

			}

			out = output.length + 1;
			bias = adapt(i - oldi, out, oldi == 0);

			// `i` was supposed to wrap around from `out` to `0`,
			// incrementing `n` each time, so we'll fix that now:
			if (floor(i / out) > maxInt - n) {
				error('overflow');
			}

			n += floor(i / out);
			i %= out;

			// Insert `n` at position `i` of the output
			output.splice(i++, 0, n);

		}

		return ucs2encode(output);
	}

	/**
	 * Converts a string of Unicode symbols to a Punycode string of ASCII-only
	 * symbols.
	 * @memberOf punycode
	 * @param {String} input The string of Unicode symbols.
	 * @returns {String} The resulting Punycode string of ASCII-only symbols.
	 */
	function encode(input) {
		var n,
		    delta,
		    handledCPCount,
		    basicLength,
		    bias,
		    j,
		    m,
		    q,
		    k,
		    t,
		    currentValue,
		    output = [],
		    /** `inputLength` will hold the number of code points in `input`. */
		    inputLength,
		    /** Cached calculation results */
		    handledCPCountPlusOne,
		    baseMinusT,
		    qMinusT;

		// Convert the input in UCS-2 to Unicode
		input = ucs2decode(input);

		// Cache the length
		inputLength = input.length;

		// Initialize the state
		n = initialN;
		delta = 0;
		bias = initialBias;

		// Handle the basic code points
		for (j = 0; j < inputLength; ++j) {
			currentValue = input[j];
			if (currentValue < 0x80) {
				output.push(stringFromCharCode(currentValue));
			}
		}

		handledCPCount = basicLength = output.length;

		// `handledCPCount` is the number of code points that have been handled;
		// `basicLength` is the number of basic code points.

		// Finish the basic string - if it is not empty - with a delimiter
		if (basicLength) {
			output.push(delimiter);
		}

		// Main encoding loop:
		while (handledCPCount < inputLength) {

			// All non-basic code points < n have been handled already. Find the next
			// larger one:
			for (m = maxInt, j = 0; j < inputLength; ++j) {
				currentValue = input[j];
				if (currentValue >= n && currentValue < m) {
					m = currentValue;
				}
			}

			// Increase `delta` enough to advance the decoder's <n,i> state to <m,0>,
			// but guard against overflow
			handledCPCountPlusOne = handledCPCount + 1;
			if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) {
				error('overflow');
			}

			delta += (m - n) * handledCPCountPlusOne;
			n = m;

			for (j = 0; j < inputLength; ++j) {
				currentValue = input[j];

				if (currentValue < n && ++delta > maxInt) {
					error('overflow');
				}

				if (currentValue == n) {
					// Represent delta as a generalized variable-length integer
					for (q = delta, k = base; /* no condition */; k += base) {
						t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias);
						if (q < t) {
							break;
						}
						qMinusT = q - t;
						baseMinusT = base - t;
						output.push(
							stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0))
						);
						q = floor(qMinusT / baseMinusT);
					}

					output.push(stringFromCharCode(digitToBasic(q, 0)));
					bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength);
					delta = 0;
					++handledCPCount;
				}
			}

			++delta;
			++n;

		}
		return output.join('');
	}

	/**
	 * Converts a Punycode string representing a domain name to Unicode. Only the
	 * Punycoded parts of the domain name will be converted, i.e. it doesn't
	 * matter if you call it on a string that has already been converted to
	 * Unicode.
	 * @memberOf punycode
	 * @param {String} domain The Punycode domain name to convert to Unicode.
	 * @returns {String} The Unicode representation of the given Punycode
	 * string.
	 */
	function toUnicode(domain) {
		return mapDomain(domain, function(string) {
			return regexPunycode.test(string)
				? decode(string.slice(4).toLowerCase())
				: string;
		});
	}

	/**
	 * Converts a Unicode string representing a domain name to Punycode. Only the
	 * non-ASCII parts of the domain name will be converted, i.e. it doesn't
	 * matter if you call it with a domain that's already in ASCII.
	 * @memberOf punycode
	 * @param {String} domain The domain name to convert, as a Unicode string.
	 * @returns {String} The Punycode representation of the given domain name.
	 */
	function toASCII(domain) {
		return mapDomain(domain, function(string) {
			return regexNonASCII.test(string)
				? 'xn--' + encode(string)
				: string;
		});
	}

	/*--------------------------------------------------------------------------*/

	/** Define the public API */
	punycode = {
		/**
		 * A string representing the current Punycode.js version number.
		 * @memberOf punycode
		 * @type String
		 */
		'version': '1.2.3',
		/**
		 * An object of methods to convert from JavaScript's internal character
		 * representation (UCS-2) to Unicode code points, and back.
		 * @see <http://mathiasbynens.be/notes/javascript-encoding>
		 * @memberOf punycode
		 * @type Object
		 */
		'ucs2': {
			'decode': ucs2decode,
			'encode': ucs2encode
		},
		'decode': decode,
		'encode': encode,
		'toASCII': toASCII,
		'toUnicode': toUnicode
	};

	/** Expose `punycode` */
	// Some AMD build optimizers, like r.js, check for specific condition patterns
	// like the following:
	if (
		typeof define == 'function' &&
		typeof define.amd == 'object' &&
		define.amd
	) {
		define(function() {
			return punycode;
		});
	}	else if (freeExports && !freeExports.nodeType) {
		if (freeModule) { // in Node.js or RingoJS v0.8.0+
			freeModule.exports = punycode;
		} else { // in Narwhal or RingoJS v0.7.0-
			for (key in punycode) {
				punycode.hasOwnProperty(key) && (freeExports[key] = punycode[key]);
			}
		}
	} else { // in Rhino or a web browser
		root.punycode = punycode;
	}

}(this));
/*!
 * URI.js - Mutating URLs
 *
 * Version: 1.14.2
 *
 * Author: Rodney Rehm
 * Web: http://medialize.github.io/URI.js/
 *
 * Licensed under
 *   MIT License http://www.opensource.org/licenses/mit-license
 *   GPL v3 http://opensource.org/licenses/GPL-3.0
 *
 */
(function (root, factory) {
  'use strict';
  // https://github.com/umdjs/umd/blob/master/returnExports.js
  if (typeof exports === 'object') {
    // Node
    module.exports = factory(require('./punycode'), require('./IPv6'), require('./SecondLevelDomains'));
  } else if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define(['./punycode', './IPv6', './SecondLevelDomains'], factory);
  } else {
    // Browser globals (root is window)
    root.URI = factory(root.punycode, root.IPv6, root.SecondLevelDomains, root);
  }
}(this, function (punycode, IPv6, SLD, root) {
  'use strict';
  /*global location, escape, unescape */
  // FIXME: v2.0.0 renamce non-camelCase properties to uppercase
  /*jshint camelcase: false */

  // save current URI variable, if any
  var _URI = root && root.URI;

  function URI(url, base) {
    // Allow instantiation without the 'new' keyword
    if (!(this instanceof URI)) {
      return new URI(url, base);
    }

    if (url === undefined) {
      if (arguments.length) {
        throw new TypeError('undefined is not a valid argument for URI');
      }

      if (typeof location !== 'undefined') {
        url = location.href + '';
      } else {
        url = '';
      }
    }

    this.href(url);

    // resolve to base according to http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#constructor
    if (base !== undefined) {
      return this.absoluteTo(base);
    }

    return this;
  }

  URI.version = '1.14.2';

  var p = URI.prototype;
  var hasOwn = Object.prototype.hasOwnProperty;

  function escapeRegEx(string) {
    // https://github.com/medialize/URI.js/commit/85ac21783c11f8ccab06106dba9735a31a86924d#commitcomment-821963
    return string.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
  }

  function getType(value) {
    // IE8 doesn't return [Object Undefined] but [Object Object] for undefined value
    if (value === undefined) {
      return 'Undefined';
    }

    return String(Object.prototype.toString.call(value)).slice(8, -1);
  }

  function isArray(obj) {
    return getType(obj) === 'Array';
  }

  function filterArrayValues(data, value) {
    var lookup = {};
    var i, length;

    if (isArray(value)) {
      for (i = 0, length = value.length; i < length; i++) {
        lookup[value[i]] = true;
      }
    } else {
      lookup[value] = true;
    }

    for (i = 0, length = data.length; i < length; i++) {
      if (lookup[data[i]] !== undefined) {
        data.splice(i, 1);
        length--;
        i--;
      }
    }

    return data;
  }

  function arrayContains(list, value) {
    var i, length;

    // value may be string, number, array, regexp
    if (isArray(value)) {
      // Note: this can be optimized to O(n) (instead of current O(m * n))
      for (i = 0, length = value.length; i < length; i++) {
        if (!arrayContains(list, value[i])) {
          return false;
        }
      }

      return true;
    }

    var _type = getType(value);
    for (i = 0, length = list.length; i < length; i++) {
      if (_type === 'RegExp') {
        if (typeof list[i] === 'string' && list[i].match(value)) {
          return true;
        }
      } else if (list[i] === value) {
        return true;
      }
    }

    return false;
  }

  function arraysEqual(one, two) {
    if (!isArray(one) || !isArray(two)) {
      return false;
    }

    // arrays can't be equal if they have different amount of content
    if (one.length !== two.length) {
      return false;
    }

    one.sort();
    two.sort();

    for (var i = 0, l = one.length; i < l; i++) {
      if (one[i] !== two[i]) {
        return false;
      }
    }

    return true;
  }

  URI._parts = function() {
    return {
      protocol: null,
      username: null,
      password: null,
      hostname: null,
      urn: null,
      port: null,
      path: null,
      query: null,
      fragment: null,
      // state
      duplicateQueryParameters: URI.duplicateQueryParameters,
      escapeQuerySpace: URI.escapeQuerySpace
    };
  };
  // state: allow duplicate query parameters (a=1&a=1)
  URI.duplicateQueryParameters = false;
  // state: replaces + with %20 (space in query strings)
  URI.escapeQuerySpace = true;
  // static properties
  URI.protocol_expression = /^[a-z][a-z0-9.+-]*$/i;
  URI.idn_expression = /[^a-z0-9\.-]/i;
  URI.punycode_expression = /(xn--)/i;
  // well, 333.444.555.666 matches, but it sure ain't no IPv4 - do we care?
  URI.ip4_expression = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
  // credits to Rich Brown
  // source: http://forums.intermapper.com/viewtopic.php?p=1096#1096
  // specification: http://www.ietf.org/rfc/rfc4291.txt
  URI.ip6_expression = /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/;
  // expression used is "gruber revised" (@gruber v2) determined to be the
  // best solution in a regex-golf we did a couple of ages ago at
  // * http://mathiasbynens.be/demo/url-regex
  // * http://rodneyrehm.de/t/url-regex.html
  URI.find_uri_expression = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>???]))/ig;
  URI.findUri = {
    // valid "scheme://" or "www."
    start: /\b(?:([a-z][a-z0-9.+-]*:\/\/)|www\.)/gi,
    // everything up to the next whitespace
    end: /[\s\r\n]|$/,
    // trim trailing punctuation captured by end RegExp
    trim: /[`!()\[\]{};:'".,<>???]+$/
  };
  // http://www.iana.org/assignments/uri-schemes.html
  // http://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers#Well-known_ports
  URI.defaultPorts = {
    http: '80',
    https: '443',
    ftp: '21',
    gopher: '70',
    ws: '80',
    wss: '443'
  };
  // allowed hostname characters according to RFC 3986
  // ALPHA DIGIT "-" "." "_" "~" "!" "$" "&" "'" "(" ")" "*" "+" "," ";" "=" %encoded
  // I've never seen a (non-IDN) hostname other than: ALPHA DIGIT . -
  URI.invalid_hostname_characters = /[^a-zA-Z0-9\.-]/;
  // map DOM Elements to their URI attribute
  URI.domAttributes = {
    'a': 'href',
    'blockquote': 'cite',
    'link': 'href',
    'base': 'href',
    'script': 'src',
    'form': 'action',
    'img': 'src',
    'area': 'href',
    'iframe': 'src',
    'embed': 'src',
    'source': 'src',
    'track': 'src',
    'input': 'src', // but only if type="image"
    'audio': 'src',
    'video': 'src'
  };
  URI.getDomAttribute = function(node) {
    if (!node || !node.nodeName) {
      return undefined;
    }

    var nodeName = node.nodeName.toLowerCase();
    // <input> should only expose src for type="image"
    if (nodeName === 'input' && node.type !== 'image') {
      return undefined;
    }

    return URI.domAttributes[nodeName];
  };

  function escapeForDumbFirefox36(value) {
    // https://github.com/medialize/URI.js/issues/91
    return escape(value);
  }

  // encoding / decoding according to RFC3986
  function strictEncodeURIComponent(string) {
    // see https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/encodeURIComponent
    return encodeURIComponent(string)
      .replace(/[!'()*]/g, escapeForDumbFirefox36)
      .replace(/\*/g, '%2A');
  }

  // Drawn from https://www.ietf.org/rfc/rfc3987.txt
  function _isIriCodePoint(point) {
    // Unless otherwise noted, all these hex ranges are defined in RFC 3987 under the
    // ucschar portion of the grammar.
    return (
        point === 0x00002d || point === 0x0002E || // -, .
        point === 0x00005F || point === 0x0007E || // _, ~
        (point >= 0x000030 && point < 0x000040) || // ASCII Digits
        (point >= 0x000041 && point < 0x00005B) || // ASCII Uppercase
        (point >= 0x000061 && point < 0x00007B) || // ASCII Lowercase
        (point >= 0x0000A0 && point < 0x00D800) ||
        (point >= 0x00E000 && point < 0x00F8FF) || // iprivate
        (point >= 0x00F900 && point < 0x00FDD0) ||
        (point >= 0x00FDF0 && point < 0x00FFF0) ||
        (point >= 0x010000 && point < 0x01FFFE) ||
        (point >= 0x020000 && point < 0x02FFFE) ||
        (point >= 0x030000 && point < 0x03FFFE) ||
        (point >= 0x040000 && point < 0x04FFFE) ||
        (point >= 0x050000 && point < 0x05FFFE) ||
        (point >= 0x060000 && point < 0x06FFFE) ||
        (point >= 0x070000 && point < 0x07FFFE) ||
        (point >= 0x080000 && point < 0x08FFFE) ||
        (point >= 0x090000 && point < 0x09FFFE) ||
        (point >= 0x0A0000 && point < 0x0AFFFE) ||
        (point >= 0x0B0000 && point < 0x0BFFFE) ||
        (point >= 0x0C0000 && point < 0x0CFFFE) ||
        (point >= 0x0D0000 && point < 0x0DFFFE) ||
        (point >= 0x0E0000 && point < 0x0EFFFE) ||
        (point >= 0x0F0000 && point < 0x0FFFFE) || // iprivate
        (point >= 0x100000 && point < 0x10FFFE)    // iprivate
      );
  }

  // IRIs are "unicode" (i.e., they use UTF-8 for %-encoding) URIs, but with a far greater range of
  // non-ASCII characters that are allowed. Note that ISO-8859 IRIs are an impossibility; ISO-8859
  // cannot represent the full range of unicode characters.
  function encodeIRIComponent(string) {
    var inputCodePoints = punycode.ucs2.decode(string);
    var output = '';
    for (var i = 0; i < inputCodePoints.length; i++) {
      var codePoint = inputCodePoints[i];
      if (_isIriCodePoint(codePoint)) {
        output += punycode.ucs2.encode([codePoint]);
      } else {
        var asString = punycode.ucs2.encode([codePoint]);
        output += strictEncodeURIComponent(asString);
      }
    }
    return output;
  }

  function recodeIRIHostname(string) {
    if (URI.punycode_expression.test(string))
    {
      string = punycode.toUnicode(string);
    }
    return encodeIRIComponent(string);
  }

  URI._defaultRecodeHostname = punycode ? punycode.toASCII : function(string) { return string; };
  URI.iso8859 = function() {
    URI.recodeHostname = URI._defaultRecodeHostname;
    URI.encode = escape;
    URI.decode = unescape;
  };
  URI.unicode = function() {
    URI.recodeHostname = URI._defaultRecodeHostname;
    URI.encode = strictEncodeURIComponent;
    URI.decode = decodeURIComponent;
  };
  URI.iri = function() {
    URI.recodeHostname = recodeIRIHostname;
    URI.encode = encodeIRIComponent;
    URI.decode = decodeURIComponent;
  };
  // By default, start in unicode mode.
  URI.unicode();
  URI.characters = {
    pathname: {
      encode: {
        // RFC3986 2.1: For consistency, URI producers and normalizers should
        // use uppercase hexadecimal digits for all percent-encodings.
        expression: /%(24|26|2B|2C|3B|3D|3A|40)/ig,
        map: {
          // -._~!'()*
          '%24': '$',
          '%26': '&',
          '%2B': '+',
          '%2C': ',',
          '%3B': ';',
          '%3D': '=',
          '%3A': ':',
          '%40': '@'
        }
      },
      decode: {
        expression: /[\/\?#]/g,
        map: {
          '/': '%2F',
          '?': '%3F',
          '#': '%23'
        }
      }
    },
    reserved: {
      encode: {
        // RFC3986 2.1: For consistency, URI producers and normalizers should
        // use uppercase hexadecimal digits for all percent-encodings.
        expression: /%(21|23|24|26|27|28|29|2A|2B|2C|2F|3A|3B|3D|3F|40|5B|5D)/ig,
        map: {
          // gen-delims
          '%3A': ':',
          '%2F': '/',
          '%3F': '?',
          '%23': '#',
          '%5B': '[',
          '%5D': ']',
          '%40': '@',
          // sub-delims
          '%21': '!',
          '%24': '$',
          '%26': '&',
          '%27': '\'',
          '%28': '(',
          '%29': ')',
          '%2A': '*',
          '%2B': '+',
          '%2C': ',',
          '%3B': ';',
          '%3D': '='
        }
      }
    },
    urnpath: {
      // The characters under `encode` are the characters called out by RFC 2141 as being acceptable
      // for usage in a URN. RFC2141 also calls out "-", ".", and "_" as acceptable characters, but
      // these aren't encoded by encodeURIComponent, so we don't have to call them out here. Also
      // note that the colon character is not featured in the encoding map; this is because URI.js
      // gives the colons in URNs semantic meaning as the delimiters of path segements, and so it
      // should not appear unencoded in a segment itself.
      // See also the note above about RFC3986 and capitalalized hex digits.
      encode: {
        expression: /%(21|24|27|28|29|2A|2B|2C|3B|3D|40)/ig,
        map: {
          '%21': '!',
          '%24': '$',
          '%27': '\'',
          '%28': '(',
          '%29': ')',
          '%2A': '*',
          '%2B': '+',
          '%2C': ',',
          '%3B': ';',
          '%3D': '=',
          '%40': '@'
        }
      },
      // These characters are the characters called out by RFC2141 as "reserved" characters that
      // should never appear in a URN, plus the colon character (see note above).
      decode: {
        expression: /[\/\?#:]/g,
        map: {
          '/': '%2F',
          '?': '%3F',
          '#': '%23',
          ':': '%3A'
        }
      }
    }
  };
  URI.encodeQuery = function(string, escapeQuerySpace) {
    var escaped = URI.encode(string + '');
    if (escapeQuerySpace === undefined) {
      escapeQuerySpace = URI.escapeQuerySpace;
    }

    return escapeQuerySpace ? escaped.replace(/%20/g, '+') : escaped;
  };
  URI.decodeQuery = function(string, escapeQuerySpace) {
    string += '';
    if (escapeQuerySpace === undefined) {
      escapeQuerySpace = URI.escapeQuerySpace;
    }

    try {
      return URI.decode(escapeQuerySpace ? string.replace(/\+/g, '%20') : string);
    } catch(e) {
      // we're not going to mess with weird encodings,
      // give up and return the undecoded original string
      // see https://github.com/medialize/URI.js/issues/87
      // see https://github.com/medialize/URI.js/issues/92
      return string;
    }
  };
  // generate encode/decode path functions
  var _parts = {'encode':'encode', 'decode':'decode'};
  var _part;
  var generateAccessor = function(_group, _part) {
    return function(string) {
      try {
        return URI[_part](string + '').replace(URI.characters[_group][_part].expression, function(c) {
          return URI.characters[_group][_part].map[c];
        });
      } catch (e) {
        // we're not going to mess with weird encodings,
        // give up and return the undecoded original string
        // see https://github.com/medialize/URI.js/issues/87
        // see https://github.com/medialize/URI.js/issues/92
        return string;
      }
    };
  };

  for (_part in _parts) {
    URI[_part + 'PathSegment'] = generateAccessor('pathname', _parts[_part]);
    URI[_part + 'UrnPathSegment'] = generateAccessor('urnpath', _parts[_part]);
  }

  var generateSegmentedPathFunction = function(_sep, _codingFuncName, _innerCodingFuncName) {
    return function(string) {
      // Why pass in names of functions, rather than the function objects themselves? The
      // definitions of some functions (but in particular, URI.decode) will occasionally change due
      // to URI.js having ISO8859 and Unicode modes. Passing in the name and getting it will ensure
      // that the functions we use here are "fresh".
      var actualCodingFunc;
      if (!_innerCodingFuncName) {
        actualCodingFunc = URI[_codingFuncName];
      } else {
        actualCodingFunc = function(string) {
          return URI[_codingFuncName](URI[_innerCodingFuncName](string));
        };
      }

      var segments = (string + '').split(_sep);

      for (var i = 0, length = segments.length; i < length; i++) {
        segments[i] = actualCodingFunc(segments[i]);
      }

      return segments.join(_sep);
    };
  };

  // This takes place outside the above loop because we don't want, e.g., encodeUrnPath functions.
  URI.decodePath = generateSegmentedPathFunction('/', 'decodePathSegment');
  URI.decodeUrnPath = generateSegmentedPathFunction(':', 'decodeUrnPathSegment');
  URI.recodePath = generateSegmentedPathFunction('/', 'encodePathSegment', 'decode');
  URI.recodeUrnPath = generateSegmentedPathFunction(':', 'encodeUrnPathSegment', 'decode');

  URI.encodeReserved = generateAccessor('reserved', 'encode');

  URI.parse = function(string, parts) {
    var pos;
    if (!parts) {
      parts = {};
    }
    // [protocol"://"[username[":"password]"@"]hostname[":"port]"/"?][path]["?"querystring]["#"fragment]

    // extract fragment
    pos = string.indexOf('#');
    if (pos > -1) {
      // escaping?
      parts.fragment = string.substring(pos + 1) || null;
      string = string.substring(0, pos);
    }

    // extract query
    pos = string.indexOf('?');
    if (pos > -1) {
      // escaping?
      parts.query = string.substring(pos + 1) || null;
      string = string.substring(0, pos);
    }

    // extract protocol
    if (string.substring(0, 2) === '//') {
      // relative-scheme
      parts.protocol = null;
      string = string.substring(2);
      // extract "user:pass@host:port"
      string = URI.parseAuthority(string, parts);
    } else {
      pos = string.indexOf(':');
      if (pos > -1) {
        parts.protocol = string.substring(0, pos) || null;
        if (parts.protocol && !parts.protocol.match(URI.protocol_expression)) {
          // : may be within the path
          parts.protocol = undefined;
        } else if (string.substring(pos + 1, pos + 3) === '//') {
          string = string.substring(pos + 3);

          // extract "user:pass@host:port"
          string = URI.parseAuthority(string, parts);
        } else {
          string = string.substring(pos + 1);
          parts.urn = true;
        }
      }
    }

    // what's left must be the path
    parts.path = string;

    // and we're done
    return parts;
  };
  URI.parseHost = function(string, parts) {
    // extract host:port
    var pos = string.indexOf('/');
    var bracketPos;
    var t;

    if (pos === -1) {
      pos = string.length;
    }

    if (string.charAt(0) === '[') {
      // IPv6 host - http://tools.ietf.org/html/draft-ietf-6man-text-addr-representation-04#section-6
      // I claim most client software breaks on IPv6 anyways. To simplify things, URI only accepts
      // IPv6+port in the format [2001:db8::1]:80 (for the time being)
      bracketPos = string.indexOf(']');
      parts.hostname = string.substring(1, bracketPos) || null;
      parts.port = string.substring(bracketPos + 2, pos) || null;
      if (parts.port === '/') {
        parts.port = null;
      }
    } else {
      var firstColon = string.indexOf(':');
      var firstSlash = string.indexOf('/');
      var nextColon = string.indexOf(':', firstColon + 1);
      if (nextColon !== -1 && (firstSlash === -1 || nextColon < firstSlash)) {
        // IPv6 host contains multiple colons - but no port
        // this notation is actually not allowed by RFC 3986, but we're a liberal parser
        parts.hostname = string.substring(0, pos) || null;
        parts.port = null;
      } else {
        t = string.substring(0, pos).split(':');
        parts.hostname = t[0] || null;
        parts.port = t[1] || null;
      }
    }

    if (parts.hostname && string.substring(pos).charAt(0) !== '/') {
      pos++;
      string = '/' + string;
    }

    return string.substring(pos) || '/';
  };
  URI.parseAuthority = function(string, parts) {
    string = URI.parseUserinfo(string, parts);
    return URI.parseHost(string, parts);
  };
  URI.parseUserinfo = function(string, parts) {
    // extract username:password
    var firstSlash = string.indexOf('/');
    var pos = string.lastIndexOf('@', firstSlash > -1 ? firstSlash : string.length - 1);
    var t;

    // authority@ must come before /path
    if (pos > -1 && (firstSlash === -1 || pos < firstSlash)) {
      t = string.substring(0, pos).split(':');
      parts.username = t[0] ? URI.decode(t[0]) : null;
      t.shift();
      parts.password = t[0] ? URI.decode(t.join(':')) : null;
      string = string.substring(pos + 1);
    } else {
      parts.username = null;
      parts.password = null;
    }

    return string;
  };
  URI.parseQuery = function(string, escapeQuerySpace) {
    if (!string) {
      return {};
    }

    // throw out the funky business - "?"[name"="value"&"]+
    string = string.replace(/&+/g, '&').replace(/^\?*&*|&+$/g, '');

    if (!string) {
      return {};
    }

    var items = {};
    var splits = string.split('&');
    var length = splits.length;
    var v, name, value;

    for (var i = 0; i < length; i++) {
      v = splits[i].split('=');
      name = URI.decodeQuery(v.shift(), escapeQuerySpace);
      // no "=" is null according to http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#collect-url-parameters
      value = v.length ? URI.decodeQuery(v.join('='), escapeQuerySpace) : null;

      if (hasOwn.call(items, name)) {
        if (typeof items[name] === 'string') {
          items[name] = [items[name]];
        }

        items[name].push(value);
      } else {
        items[name] = value;
      }
    }

    return items;
  };

  URI.build = function(parts) {
    var t = '';

    if (parts.protocol) {
      t += parts.protocol + ':';
    }

    if (!parts.urn && (t || parts.hostname)) {
      t += '//';
    }

    t += (URI.buildAuthority(parts) || '');

    if (typeof parts.path === 'string') {
      if (parts.path.charAt(0) !== '/' && typeof parts.hostname === 'string') {
        t += '/';
      }

      t += parts.path;
    }

    if (typeof parts.query === 'string' && parts.query) {
      t += '?' + parts.query;
    }

    if (typeof parts.fragment === 'string' && parts.fragment) {
      t += '#' + parts.fragment;
    }
    return t;
  };
  URI.buildHost = function(parts) {
    var t = '';

    if (!parts.hostname) {
      return '';
    } else if (URI.ip6_expression.test(parts.hostname)) {
      t += '[' + parts.hostname + ']';
    } else {
      t += parts.hostname;
    }

    if (parts.port) {
      t += ':' + parts.port;
    }

    return t;
  };
  URI.buildAuthority = function(parts) {
    return URI.buildUserinfo(parts) + URI.buildHost(parts);
  };
  URI.buildUserinfo = function(parts) {
    var t = '';

    if (parts.username) {
      t += URI.encode(parts.username);

      if (parts.password) {
        t += ':' + URI.encode(parts.password);
      }

      t += '@';
    }

    return t;
  };
  URI.buildQuery = function(data, duplicateQueryParameters, escapeQuerySpace) {
    // according to http://tools.ietf.org/html/rfc3986 or http://labs.apache.org/webarch/uri/rfc/rfc3986.html
    // being ?-._~!$&'()*+,;=:@/?? %HEX and alnum are allowed
    // the RFC explicitly states ?/foo being a valid use case, no mention of parameter syntax!
    // URI.js treats the query string as being application/x-www-form-urlencoded
    // see http://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type

    var t = '';
    var unique, key, i, length;
    for (key in data) {
      if (hasOwn.call(data, key) && key) {
        if (isArray(data[key])) {
          unique = {};
          for (i = 0, length = data[key].length; i < length; i++) {
            if (data[key][i] !== undefined && unique[data[key][i] + ''] === undefined) {
              t += '&' + URI.buildQueryParameter(key, data[key][i], escapeQuerySpace);
              if (duplicateQueryParameters !== true) {
                unique[data[key][i] + ''] = true;
              }
            }
          }
        } else if (data[key] !== undefined) {
          t += '&' + URI.buildQueryParameter(key, data[key], escapeQuerySpace);
        }
      }
    }

    return t.substring(1);
  };
  URI.buildQueryParameter = function(name, value, escapeQuerySpace) {
    // http://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type -- application/x-www-form-urlencoded
    // don't append "=" for null values, according to http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#url-parameter-serialization
    return URI.encodeQuery(name, escapeQuerySpace) + (value !== null ? '=' + URI.encodeQuery(value, escapeQuerySpace) : '');
  };

  URI.addQuery = function(data, name, value) {
    if (typeof name === 'object') {
      for (var key in name) {
        if (hasOwn.call(name, key)) {
          URI.addQuery(data, key, name[key]);
        }
      }
    } else if (typeof name === 'string') {
      if (data[name] === undefined) {
        data[name] = value;
        return;
      } else if (typeof data[name] === 'string') {
        data[name] = [data[name]];
      }

      if (!isArray(value)) {
        value = [value];
      }

      data[name] = (data[name] || []).concat(value);
    } else {
      throw new TypeError('URI.addQuery() accepts an object, string as the name parameter');
    }
  };
  URI.removeQuery = function(data, name, value) {
    var i, length, key;

    if (isArray(name)) {
      for (i = 0, length = name.length; i < length; i++) {
        data[name[i]] = undefined;
      }
    } else if (typeof name === 'object') {
      for (key in name) {
        if (hasOwn.call(name, key)) {
          URI.removeQuery(data, key, name[key]);
        }
      }
    } else if (typeof name === 'string') {
      if (value !== undefined) {
        if (data[name] === value) {
          data[name] = undefined;
        } else if (isArray(data[name])) {
          data[name] = filterArrayValues(data[name], value);
        }
      } else {
        data[name] = undefined;
      }
    } else {
      throw new TypeError('URI.removeQuery() accepts an object, string as the first parameter');
    }
  };
  URI.hasQuery = function(data, name, value, withinArray) {
    if (typeof name === 'object') {
      for (var key in name) {
        if (hasOwn.call(name, key)) {
          if (!URI.hasQuery(data, key, name[key])) {
            return false;
          }
        }
      }

      return true;
    } else if (typeof name !== 'string') {
      throw new TypeError('URI.hasQuery() accepts an object, string as the name parameter');
    }

    switch (getType(value)) {
      case 'Undefined':
        // true if exists (but may be empty)
        return name in data; // data[name] !== undefined;

      case 'Boolean':
        // true if exists and non-empty
        var _booly = Boolean(isArray(data[name]) ? data[name].length : data[name]);
        return value === _booly;

      case 'Function':
        // allow complex comparison
        return !!value(data[name], name, data);

      case 'Array':
        if (!isArray(data[name])) {
          return false;
        }

        var op = withinArray ? arrayContains : arraysEqual;
        return op(data[name], value);

      case 'RegExp':
        if (!isArray(data[name])) {
          return Boolean(data[name] && data[name].match(value));
        }

        if (!withinArray) {
          return false;
        }

        return arrayContains(data[name], value);

      case 'Number':
        value = String(value);
        /* falls through */
      case 'String':
        if (!isArray(data[name])) {
          return data[name] === value;
        }

        if (!withinArray) {
          return false;
        }

        return arrayContains(data[name], value);

      default:
        throw new TypeError('URI.hasQuery() accepts undefined, boolean, string, number, RegExp, Function as the value parameter');
    }
  };


  URI.commonPath = function(one, two) {
    var length = Math.min(one.length, two.length);
    var pos;

    // find first non-matching character
    for (pos = 0; pos < length; pos++) {
      if (one.charAt(pos) !== two.charAt(pos)) {
        pos--;
        break;
      }
    }

    if (pos < 1) {
      return one.charAt(0) === two.charAt(0) && one.charAt(0) === '/' ? '/' : '';
    }

    // revert to last /
    if (one.charAt(pos) !== '/' || two.charAt(pos) !== '/') {
      pos = one.substring(0, pos).lastIndexOf('/');
    }

    return one.substring(0, pos + 1);
  };

  URI.withinString = function(string, callback, options) {
    options || (options = {});
    var _start = options.start || URI.findUri.start;
    var _end = options.end || URI.findUri.end;
    var _trim = options.trim || URI.findUri.trim;
    var _attributeOpen = /[a-z0-9-]=["']?$/i;

    _start.lastIndex = 0;
    while (true) {
      var match = _start.exec(string);
      if (!match) {
        break;
      }

      var start = match.index;
      if (options.ignoreHtml) {
        // attribut(e=["']?$)
        var attributeOpen = string.slice(Math.max(start - 3, 0), start);
        if (attributeOpen && _attributeOpen.test(attributeOpen)) {
          continue;
        }
      }

      var end = start + string.slice(start).search(_end);
      var slice = string.slice(start, end).replace(_trim, '');
      if (options.ignore && options.ignore.test(slice)) {
        continue;
      }

      end = start + slice.length;
      var result = callback(slice, start, end, string);
      string = string.slice(0, start) + result + string.slice(end);
      _start.lastIndex = start + result.length;
    }

    _start.lastIndex = 0;
    return string;
  };

  URI.ensureValidHostname = function(v) {
    // Theoretically URIs allow percent-encoding in Hostnames (according to RFC 3986)
    // they are not part of DNS and therefore ignored by URI.js

    if (v.match(URI.invalid_hostname_characters)) {
      // test punycode
      if (!punycode) {
        throw new TypeError('Hostname "' + v + '" contains characters other than [A-Z0-9.-] and Punycode.js is not available');
      }

      if (punycode.toASCII(v).match(URI.invalid_hostname_characters)) {
        throw new TypeError('Hostname "' + v + '" contains characters other than [A-Z0-9.-]');
      }
    }
  };

  // noConflict
  URI.noConflict = function(removeAll) {
    if (removeAll) {
      var unconflicted = {
        URI: this.noConflict()
      };

      if (root.URITemplate && typeof root.URITemplate.noConflict === 'function') {
        unconflicted.URITemplate = root.URITemplate.noConflict();
      }

      if (root.IPv6 && typeof root.IPv6.noConflict === 'function') {
        unconflicted.IPv6 = root.IPv6.noConflict();
      }

      if (root.SecondLevelDomains && typeof root.SecondLevelDomains.noConflict === 'function') {
        unconflicted.SecondLevelDomains = root.SecondLevelDomains.noConflict();
      }

      return unconflicted;
    } else if (root.URI === this) {
      root.URI = _URI;
    }

    return this;
  };

  p.build = function(deferBuild) {
    if (deferBuild === true) {
      this._deferred_build = true;
    } else if (deferBuild === undefined || this._deferred_build) {
      this._string = URI.build(this._parts);
      this._deferred_build = false;
    }

    return this;
  };

  p.clone = function() {
    return new URI(this);
  };

  p.valueOf = p.toString = function() {
    return this.build(false)._string;
  };


  function generateSimpleAccessor(_part){
    return function(v, build) {
      if (v === undefined) {
        return this._parts[_part] || '';
      } else {
        this._parts[_part] = v || null;
        this.build(!build);
        return this;
      }
    };
  }

  function generatePrefixAccessor(_part, _key){
    return function(v, build) {
      if (v === undefined) {
        return this._parts[_part] || '';
      } else {
        if (v !== null) {
          v = v + '';
          if (v.charAt(0) === _key) {
            v = v.substring(1);
          }
        }

        this._parts[_part] = v;
        this.build(!build);
        return this;
      }
    };
  }

  p.protocol = generateSimpleAccessor('protocol');
  p.username = generateSimpleAccessor('username');
  p.password = generateSimpleAccessor('password');
  p.hostname = generateSimpleAccessor('hostname');
  p.port = generateSimpleAccessor('port');
  p.query = generatePrefixAccessor('query', '?');
  p.fragment = generatePrefixAccessor('fragment', '#');

  p.search = function(v, build) {
    var t = this.query(v, build);
    return typeof t === 'string' && t.length ? ('?' + t) : t;
  };
  p.hash = function(v, build) {
    var t = this.fragment(v, build);
    return typeof t === 'string' && t.length ? ('#' + t) : t;
  };

  p.pathname = function(v, build) {
    if (v === undefined || v === true) {
      var res = this._parts.path || (this._parts.hostname ? '/' : '');
      return v ? (this._parts.urn ? URI.decodeUrnPath : URI.decodePath)(res) : res;
    } else {
      if (this._parts.urn) {
        this._parts.path = v ? URI.recodeUrnPath(v) : '';
      } else {
        this._parts.path = v ? URI.recodePath(v) : '/';
      }
      this.build(!build);
      return this;
    }
  };
  p.path = p.pathname;
  p.href = function(href, build) {
    var key;

    if (href === undefined) {
      return this.toString();
    }

    this._string = '';
    this._parts = URI._parts();

    var _URI = href instanceof URI;
    var _object = typeof href === 'object' && (href.hostname || href.path || href.pathname);
    if (href.nodeName) {
      var attribute = URI.getDomAttribute(href);
      href = href[attribute] || '';
      _object = false;
    }

    // window.location is reported to be an object, but it's not the sort
    // of object we're looking for:
    // * location.protocol ends with a colon
    // * location.query != object.search
    // * location.hash != object.fragment
    // simply serializing the unknown object should do the trick
    // (for location, not for everything...)
    if (!_URI && _object && href.pathname !== undefined) {
      href = href.toString();
    }

    if (typeof href === 'string' || href instanceof String) {
      this._parts = URI.parse(String(href), this._parts);
    } else if (_URI || _object) {
      var src = _URI ? href._parts : href;
      for (key in src) {
        if (hasOwn.call(this._parts, key)) {
          this._parts[key] = src[key];
        }
      }
    } else {
      throw new TypeError('invalid input');
    }

    this.build(!build);
    return this;
  };

  // identification accessors
  p.is = function(what) {
    var ip = false;
    var ip4 = false;
    var ip6 = false;
    var name = false;
    var sld = false;
    var idn = false;
    var punycode = false;
    var relative = !this._parts.urn;

    if (this._parts.hostname) {
      relative = false;
      ip4 = URI.ip4_expression.test(this._parts.hostname);
      ip6 = URI.ip6_expression.test(this._parts.hostname);
      ip = ip4 || ip6;
      name = !ip;
      sld = name && SLD && SLD.has(this._parts.hostname);
      idn = name && URI.idn_expression.test(this._parts.hostname);
      punycode = name && URI.punycode_expression.test(this._parts.hostname);
    }

    switch (what.toLowerCase()) {
      case 'relative':
        return relative;

      case 'absolute':
        return !relative;

      // hostname identification
      case 'domain':
      case 'name':
        return name;

      case 'sld':
        return sld;

      case 'ip':
        return ip;

      case 'ip4':
      case 'ipv4':
      case 'inet4':
        return ip4;

      case 'ip6':
      case 'ipv6':
      case 'inet6':
        return ip6;

      case 'idn':
        return idn;

      case 'url':
        return !this._parts.urn;

      case 'urn':
        return !!this._parts.urn;

      case 'punycode':
        return punycode;
    }

    return null;
  };

  // component specific input validation
  var _protocol = p.protocol;
  var _port = p.port;
  var _hostname = p.hostname;

  p.protocol = function(v, build) {
    if (v !== undefined) {
      if (v) {
        // accept trailing ://
        v = v.replace(/:(\/\/)?$/, '');

        if (!v.match(URI.protocol_expression)) {
          throw new TypeError('Protocol "' + v + '" contains characters other than [A-Z0-9.+-] or doesn\'t start with [A-Z]');
        }
      }
    }
    return _protocol.call(this, v, build);
  };
  p.scheme = p.protocol;
  p.port = function(v, build) {
    if (this._parts.urn) {
      return v === undefined ? '' : this;
    }

    if (v !== undefined) {
      if (v === 0) {
        v = null;
      }

      if (v) {
        v += '';
        if (v.charAt(0) === ':') {
          v = v.substring(1);
        }

        if (v.match(/[^0-9]/)) {
          throw new TypeError('Port "' + v + '" contains characters other than [0-9]');
        }
      }
    }
    return _port.call(this, v, build);
  };
  p.hostname = function(v, build) {
    if (this._parts.urn) {
      return v === undefined ? '' : this;
    }

    if (v !== undefined) {
      var x = {};
      URI.parseHost(v, x);
      v = x.hostname;
    }
    return _hostname.call(this, v, build);
  };

  // compound accessors
  p.host = function(v, build) {
    if (this._parts.urn) {
      return v === undefined ? '' : this;
    }

    if (v === undefined) {
      return this._parts.hostname ? URI.buildHost(this._parts) : '';
    } else {
      URI.parseHost(v, this._parts);
      this.build(!build);
      return this;
    }
  };
  p.authority = function(v, build) {
    if (this._parts.urn) {
      return v === undefined ? '' : this;
    }

    if (v === undefined) {
      return this._parts.hostname ? URI.buildAuthority(this._parts) : '';
    } else {
      URI.parseAuthority(v, this._parts);
      this.build(!build);
      return this;
    }
  };
  p.userinfo = function(v, build) {
    if (this._parts.urn) {
      return v === undefined ? '' : this;
    }

    if (v === undefined) {
      if (!this._parts.username) {
        return '';
      }

      var t = URI.buildUserinfo(this._parts);
      return t.substring(0, t.length -1);
    } else {
      if (v[v.length-1] !== '@') {
        v += '@';
      }

      URI.parseUserinfo(v, this._parts);
      this.build(!build);
      return this;
    }
  };
  p.resource = function(v, build) {
    var parts;

    if (v === undefined) {
      return this.path() + this.search() + this.hash();
    }

    parts = URI.parse(v);
    this._parts.path = parts.path;
    this._parts.query = parts.query;
    this._parts.fragment = parts.fragment;
    this.build(!build);
    return this;
  };

  // fraction accessors
  p.subdomain = function(v, build) {
    if (this._parts.urn) {
      return v === undefined ? '' : this;
    }

    // convenience, return "www" from "www.example.org"
    if (v === undefined) {
      if (!this._parts.hostname || this.is('IP')) {
        return '';
      }

      // grab domain and add another segment
      var end = this._parts.hostname.length - this.domain().length - 1;
      return this._parts.hostname.substring(0, end) || '';
    } else {
      var e = this._parts.hostname.length - this.domain().length;
      var sub = this._parts.hostname.substring(0, e);
      var replace = new RegExp('^' + escapeRegEx(sub));

      if (v && v.charAt(v.length - 1) !== '.') {
        v += '.';
      }

      if (v) {
        URI.ensureValidHostname(v);
      }

      this._parts.hostname = this._parts.hostname.replace(replace, v);
      this.build(!build);
      return this;
    }
  };
  p.domain = function(v, build) {
    if (this._parts.urn) {
      return v === undefined ? '' : this;
    }

    if (typeof v === 'boolean') {
      build = v;
      v = undefined;
    }

    // convenience, return "example.org" from "www.example.org"
    if (v === undefined) {
      if (!this._parts.hostname || this.is('IP')) {
        return '';
      }

      // if hostname consists of 1 or 2 segments, it must be the domain
      var t = this._parts.hostname.match(/\./g);
      if (t && t.length < 2) {
        return this._parts.hostname;
      }

      // grab tld and add another segment
      var end = this._parts.hostname.length - this.tld(build).length - 1;
      end = this._parts.hostname.lastIndexOf('.', end -1) + 1;
      return this._parts.hostname.substring(end) || '';
    } else {
      if (!v) {
        throw new TypeError('cannot set domain empty');
      }

      URI.ensureValidHostname(v);

      if (!this._parts.hostname || this.is('IP')) {
        this._parts.hostname = v;
      } else {
        var replace = new RegExp(escapeRegEx(this.domain()) + '$');
        this._parts.hostname = this._parts.hostname.replace(replace, v);
      }

      this.build(!build);
      return this;
    }
  };
  p.tld = function(v, build) {
    if (this._parts.urn) {
      return v === undefined ? '' : this;
    }

    if (typeof v === 'boolean') {
      build = v;
      v = undefined;
    }

    // return "org" from "www.example.org"
    if (v === undefined) {
      if (!this._parts.hostname || this.is('IP')) {
        return '';
      }

      var pos = this._parts.hostname.lastIndexOf('.');
      var tld = this._parts.hostname.substring(pos + 1);

      if (build !== true && SLD && SLD.list[tld.toLowerCase()]) {
        return SLD.get(this._parts.hostname) || tld;
      }

      return tld;
    } else {
      var replace;

      if (!v) {
        throw new TypeError('cannot set TLD empty');
      } else if (v.match(/[^a-zA-Z0-9-]/)) {
        if (SLD && SLD.is(v)) {
          replace = new RegExp(escapeRegEx(this.tld()) + '$');
          this._parts.hostname = this._parts.hostname.replace(replace, v);
        } else {
          throw new TypeError('TLD "' + v + '" contains characters other than [A-Z0-9]');
        }
      } else if (!this._parts.hostname || this.is('IP')) {
        throw new ReferenceError('cannot set TLD on non-domain host');
      } else {
        replace = new RegExp(escapeRegEx(this.tld()) + '$');
        this._parts.hostname = this._parts.hostname.replace(replace, v);
      }

      this.build(!build);
      return this;
    }
  };
  p.directory = function(v, build) {
    if (this._parts.urn) {
      return v === undefined ? '' : this;
    }

    if (v === undefined || v === true) {
      if (!this._parts.path && !this._parts.hostname) {
        return '';
      }

      if (this._parts.path === '/') {
        return '/';
      }

      var end = this._parts.path.length - this.filename().length - 1;
      var res = this._parts.path.substring(0, end) || (this._parts.hostname ? '/' : '');

      return v ? URI.decodePath(res) : res;

    } else {
      var e = this._parts.path.length - this.filename().length;
      var directory = this._parts.path.substring(0, e);
      var replace = new RegExp('^' + escapeRegEx(directory));

      // fully qualifier directories begin with a slash
      if (!this.is('relative')) {
        if (!v) {
          v = '/';
        }

        if (v.charAt(0) !== '/') {
          v = '/' + v;
        }
      }

      // directories always end with a slash
      if (v && v.charAt(v.length - 1) !== '/') {
        v += '/';
      }

      v = URI.recodePath(v);
      this._parts.path = this._parts.path.replace(replace, v);
      this.build(!build);
      return this;
    }
  };
  p.filename = function(v, build) {
    if (this._parts.urn) {
      return v === undefined ? '' : this;
    }

    if (v === undefined || v === true) {
      if (!this._parts.path || this._parts.path === '/') {
        return '';
      }

      var pos = this._parts.path.lastIndexOf('/');
      var res = this._parts.path.substring(pos+1);

      return v ? URI.decodePathSegment(res) : res;
    } else {
      var mutatedDirectory = false;

      if (v.charAt(0) === '/') {
        v = v.substring(1);
      }

      if (v.match(/\.?\//)) {
        mutatedDirectory = true;
      }

      var replace = new RegExp(escapeRegEx(this.filename()) + '$');
      v = URI.recodePath(v);
      this._parts.path = this._parts.path.replace(replace, v);

      if (mutatedDirectory) {
        this.normalizePath(build);
      } else {
        this.build(!build);
      }

      return this;
    }
  };
  p.suffix = function(v, build) {
    if (this._parts.urn) {
      return v === undefined ? '' : this;
    }

    if (v === undefined || v === true) {
      if (!this._parts.path || this._parts.path === '/') {
        return '';
      }

      var filename = this.filename();
      var pos = filename.lastIndexOf('.');
      var s, res;

      if (pos === -1) {
        return '';
      }

      // suffix may only contain alnum characters (yup, I made this up.)
      s = filename.substring(pos+1);
      res = (/^[a-z0-9%]+$/i).test(s) ? s : '';
      return v ? URI.decodePathSegment(res) : res;
    } else {
      if (v.charAt(0) === '.') {
        v = v.substring(1);
      }

      var suffix = this.suffix();
      var replace;

      if (!suffix) {
        if (!v) {
          return this;
        }

        this._parts.path += '.' + URI.recodePath(v);
      } else if (!v) {
        replace = new RegExp(escapeRegEx('.' + suffix) + '$');
      } else {
        replace = new RegExp(escapeRegEx(suffix) + '$');
      }

      if (replace) {
        v = URI.recodePath(v);
        this._parts.path = this._parts.path.replace(replace, v);
      }

      this.build(!build);
      return this;
    }
  };
  p.segment = function(segment, v, build) {
    var separator = this._parts.urn ? ':' : '/';
    var path = this.path();
    var absolute = path.substring(0, 1) === '/';
    var segments = path.split(separator);

    if (segment !== undefined && typeof segment !== 'number') {
      build = v;
      v = segment;
      segment = undefined;
    }

    if (segment !== undefined && typeof segment !== 'number') {
      throw new Error('Bad segment "' + segment + '", must be 0-based integer');
    }

    if (absolute) {
      segments.shift();
    }

    if (segment < 0) {
      // allow negative indexes to address from the end
      segment = Math.max(segments.length + segment, 0);
    }

    if (v === undefined) {
      /*jshint laxbreak: true */
      return segment === undefined
        ? segments
        : segments[segment];
      /*jshint laxbreak: false */
    } else if (segment === null || segments[segment] === undefined) {
      if (isArray(v)) {
        segments = [];
        // collapse empty elements within array
        for (var i=0, l=v.length; i < l; i++) {
          if (!v[i].length && (!segments.length || !segments[segments.length -1].length)) {
            continue;
          }

          if (segments.length && !segments[segments.length -1].length) {
            segments.pop();
          }

          segments.push(v[i]);
        }
      } else if (v || typeof v === 'string') {
        if (segments[segments.length -1] === '') {
          // empty trailing elements have to be overwritten
          // to prevent results such as /foo//bar
          segments[segments.length -1] = v;
        } else {
          segments.push(v);
        }
      }
    } else {
      if (v) {
        segments[segment] = v;
      } else {
        segments.splice(segment, 1);
      }
    }

    if (absolute) {
      segments.unshift('');
    }

    return this.path(segments.join(separator), build);
  };
  p.segmentCoded = function(segment, v, build) {
    var segments, i, l;

    if (typeof segment !== 'number') {
      build = v;
      v = segment;
      segment = undefined;
    }

    if (v === undefined) {
      segments = this.segment(segment, v, build);
      if (!isArray(segments)) {
        segments = segments !== undefined ? URI.decode(segments) : undefined;
      } else {
        for (i = 0, l = segments.length; i < l; i++) {
          segments[i] = URI.decode(segments[i]);
        }
      }

      return segments;
    }

    if (!isArray(v)) {
      v = (typeof v === 'string' || v instanceof String) ? URI.encode(v) : v;
    } else {
      for (i = 0, l = v.length; i < l; i++) {
        v[i] = URI.decode(v[i]);
      }
    }

    return this.segment(segment, v, build);
  };

  // mutating query string
  var q = p.query;
  p.query = function(v, build) {
    if (v === true) {
      return URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace);
    } else if (typeof v === 'function') {
      var data = URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace);
      var result = v.call(this, data);
      this._parts.query = URI.buildQuery(result || data, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace);
      this.build(!build);
      return this;
    } else if (v !== undefined && typeof v !== 'string') {
      this._parts.query = URI.buildQuery(v, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace);
      this.build(!build);
      return this;
    } else {
      return q.call(this, v, build);
    }
  };
  p.setQuery = function(name, value, build) {
    var data = URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace);

    if (typeof name === 'string' || name instanceof String) {
      data[name] = value !== undefined ? value : null;
    } else if (typeof name === 'object') {
      for (var key in name) {
        if (hasOwn.call(name, key)) {
          data[key] = name[key];
        }
      }
    } else {
      throw new TypeError('URI.addQuery() accepts an object, string as the name parameter');
    }

    this._parts.query = URI.buildQuery(data, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace);
    if (typeof name !== 'string') {
      build = value;
    }

    this.build(!build);
    return this;
  };
  p.addQuery = function(name, value, build) {
    var data = URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace);
    URI.addQuery(data, name, value === undefined ? null : value);
    this._parts.query = URI.buildQuery(data, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace);
    if (typeof name !== 'string') {
      build = value;
    }

    this.build(!build);
    return this;
  };
  p.removeQuery = function(name, value, build) {
    var data = URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace);
    URI.removeQuery(data, name, value);
    this._parts.query = URI.buildQuery(data, this._parts.duplicateQueryParameters, this._parts.escapeQuerySpace);
    if (typeof name !== 'string') {
      build = value;
    }

    this.build(!build);
    return this;
  };
  p.hasQuery = function(name, value, withinArray) {
    var data = URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace);
    return URI.hasQuery(data, name, value, withinArray);
  };
  p.setSearch = p.setQuery;
  p.addSearch = p.addQuery;
  p.removeSearch = p.removeQuery;
  p.hasSearch = p.hasQuery;

  // sanitizing URLs
  p.normalize = function() {
    if (this._parts.urn) {
      return this
        .normalizeProtocol(false)
        .normalizePath(false)
        .normalizeQuery(false)
        .normalizeFragment(false)
        .build();
    }

    return this
      .normalizeProtocol(false)
      .normalizeHostname(false)
      .normalizePort(false)
      .normalizePath(false)
      .normalizeQuery(false)
      .normalizeFragment(false)
      .build();
  };
  p.normalizeProtocol = function(build) {
    if (typeof this._parts.protocol === 'string') {
      this._parts.protocol = this._parts.protocol.toLowerCase();
      this.build(!build);
    }

    return this;
  };
  p.normalizeHostname = function(build) {
    if (this._parts.hostname) {
      // If this is an international domain name, or if it was an IDN before it was punycoded
      if (this.is('IDN') || this.is('punycode')) {
        this._parts.hostname = URI.recodeHostname(this._parts.hostname);
      } else if (this.is('IPv6') && IPv6) {
        this._parts.hostname = IPv6.best(this._parts.hostname);
      }

      this._parts.hostname = this._parts.hostname.toLowerCase();
      this.build(!build);
    }

    return this;
  };
  p.normalizePort = function(build) {
    // remove port of it's the protocol's default
    if (typeof this._parts.protocol === 'string' && this._parts.port === URI.defaultPorts[this._parts.protocol]) {
      this._parts.port = null;
      this.build(!build);
    }

    return this;
  };
  p.normalizePath = function(build) {
    var _path = this._parts.path;
    if (!_path) {
      return this;
    }

    if (this._parts.urn) {
      this._parts.path = URI.recodeUrnPath(this._parts.path);
      this.build(!build);
      return this;
    }

    if (this._parts.path === '/') {
      return this;
    }

    var _was_relative;
    var _leadingParents = '';
    var _parent, _pos;

    // handle relative paths
    if (_path.charAt(0) !== '/') {
      _was_relative = true;
      _path = '/' + _path;
    }

    // resolve simples
    _path = _path
      .replace(/(\/(\.\/)+)|(\/\.$)/g, '/')
      .replace(/\/{2,}/g, '/');

    // remember leading parents
    if (_was_relative) {
      _leadingParents = _path.substring(1).match(/^(\.\.\/)+/) || '';
      if (_leadingParents) {
        _leadingParents = _leadingParents[0];
      }
    }

    // resolve parents
    while (true) {
      _parent = _path.indexOf('/..');
      if (_parent === -1) {
        // no more ../ to resolve
        break;
      } else if (_parent === 0) {
        // top level cannot be relative, skip it
        _path = _path.substring(3);
        continue;
      }

      _pos = _path.substring(0, _parent).lastIndexOf('/');
      if (_pos === -1) {
        _pos = _parent;
      }
      _path = _path.substring(0, _pos) + _path.substring(_parent + 3);
    }

    // revert to relative
    if (_was_relative && this.is('relative')) {
      _path = _leadingParents + _path.substring(1);
    }

    _path = URI.recodePath(_path);
    this._parts.path = _path;
    this.build(!build);
    return this;
  };
  p.normalizePathname = p.normalizePath;
  p.normalizeQuery = function(build) {
    if (typeof this._parts.query === 'string') {
      if (!this._parts.query.length) {
        this._parts.query = null;
      } else {
        this.query(URI.parseQuery(this._parts.query, this._parts.escapeQuerySpace));
      }

      this.build(!build);
    }

    return this;
  };
  p.normalizeFragment = function(build) {
    if (!this._parts.fragment) {
      this._parts.fragment = null;
      this.build(!build);
    }

    return this;
  };
  p.normalizeSearch = p.normalizeQuery;
  p.normalizeHash = p.normalizeFragment;

  function _generateNormalizer(hostnameRecoder, encoder, decoder) {
    return function() {
      var r = URI.recodeHostname
      var e = URI.encode;
      var d = URI.decode;

      URI.encode = encoder;
      URI.decode = decoder;
      try {
        this.normalize();
      } finally {
        URI.recodeHostname = r;
        URI.encode = e;
        URI.decode = d;
      }
      return this;
    };
  }

  // expect unicode input, iso8859 output
  p.iso8859 = _generateNormalizer(URI._defaultRecodeHostname, escape, decodeURIComponent);
  // expect iso8859 input, unicode output
  p.unicode = _generateNormalizer(URI._defaultRecodeHostname, strictEncodeURIComponent, unescape);
  // expect unicode input, IRI output
  p.iri = _generateNormalizer(recodeIRIHostname, encodeIRIComponent, decodeURIComponent);

  p.readable = function() {
    var uri = this.clone();
    // removing username, password, because they shouldn't be displayed according to RFC 3986
    uri.username('').password('').normalize();
    var t = '';
    if (uri._parts.protocol) {
      t += uri._parts.protocol + (uri._parts.urn ? ':' : '://');
    }

    if (uri._parts.hostname) {
      if (uri.is('punycode') && punycode) {
        t += punycode.toUnicode(uri._parts.hostname);
        if (uri._parts.port) {
          t += ':' + uri._parts.port;
        }
      } else {
        t += uri.host();
      }
    }

    if (uri._parts.hostname && uri._parts.path && uri._parts.path.charAt(0) !== '/') {
      t += '/';
    }

    t += uri.path(true);
    if (uri._parts.query) {
      var q = '';
      for (var i = 0, qp = uri._parts.query.split('&'), l = qp.length; i < l; i++) {
        var kv = (qp[i] || '').split('=');
        q += '&' + URI.decodeQuery(kv[0], this._parts.escapeQuerySpace)
          .replace(/&/g, '%26');

        if (kv[1] !== undefined) {
          q += '=' + URI.decodeQuery(kv[1], this._parts.escapeQuerySpace)
            .replace(/&/g, '%26');
        }
      }
      t += '?' + q.substring(1);
    }

    t += URI.decodeQuery(uri.hash(), true);
    return t;
  };

  // resolving relative and absolute URLs
  p.absoluteTo = function(base) {
    var resolved = this.clone();
    var properties = ['protocol', 'username', 'password', 'hostname', 'port'];
    var basedir, i, p;

    if (this._parts.urn) {
      throw new Error('URNs do not have any generally defined hierarchical components');
    }

    if (!(base instanceof URI)) {
      base = new URI(base);
    }

    if (!resolved._parts.protocol) {
      resolved._parts.protocol = base._parts.protocol;
    }

    if (this._parts.hostname) {
      return resolved;
    }

    for (i = 0; (p = properties[i]); i++) {
      resolved._parts[p] = base._parts[p];
    }

    if (!resolved._parts.path) {
      resolved._parts.path = base._parts.path;
      if (!resolved._parts.query) {
        resolved._parts.query = base._parts.query;
      }
    } else if (resolved._parts.path.substring(-2) === '..') {
      resolved._parts.path += '/';
    }

    if (resolved.path().charAt(0) !== '/') {
      basedir = base.directory();
      resolved._parts.path = (basedir ? (basedir + '/') : '') + resolved._parts.path;
      resolved.normalizePath();
    }

    resolved.build();
    return resolved;
  };
  p.relativeTo = function(base) {
    var relative = this.clone().normalize();
    var relativeParts, baseParts, common, relativePath, basePath;

    if (relative._parts.urn) {
      throw new Error('URNs do not have any generally defined hierarchical components');
    }

    base = new URI(base).normalize();
    relativeParts = relative._parts;
    baseParts = base._parts;
    relativePath = relative.path();
    basePath = base.path();

    if (relativePath.charAt(0) !== '/') {
      throw new Error('URI is already relative');
    }

    if (basePath.charAt(0) !== '/') {
      throw new Error('Cannot calculate a URI relative to another relative URI');
    }

    if (relativeParts.protocol === baseParts.protocol) {
      relativeParts.protocol = null;
    }

    if (relativeParts.username !== baseParts.username || relativeParts.password !== baseParts.password) {
      return relative.build();
    }

    if (relativeParts.protocol !== null || relativeParts.username !== null || relativeParts.password !== null) {
      return relative.build();
    }

    if (relativeParts.hostname === baseParts.hostname && relativeParts.port === baseParts.port) {
      relativeParts.hostname = null;
      relativeParts.port = null;
    } else {
      return relative.build();
    }

    if (relativePath === basePath) {
      relativeParts.path = '';
      return relative.build();
    }

    // determine common sub path
    common = URI.commonPath(relative.path(), base.path());

    // If the paths have nothing in common, return a relative URL with the absolute path.
    if (!common) {
      return relative.build();
    }

    var parents = baseParts.path
      .substring(common.length)
      .replace(/[^\/]*$/, '')
      .replace(/.*?\//g, '../');

    relativeParts.path = parents + relativeParts.path.substring(common.length);

    return relative.build();
  };

  // comparing URIs
  p.equals = function(uri) {
    var one = this.clone();
    var two = new URI(uri);
    var one_map = {};
    var two_map = {};
    var checked = {};
    var one_query, two_query, key;

    one.normalize();
    two.normalize();

    // exact match
    if (one.toString() === two.toString()) {
      return true;
    }

    // extract query string
    one_query = one.query();
    two_query = two.query();
    one.query('');
    two.query('');

    // definitely not equal if not even non-query parts match
    if (one.toString() !== two.toString()) {
      return false;
    }

    // query parameters have the same length, even if they're permuted
    if (one_query.length !== two_query.length) {
      return false;
    }

    one_map = URI.parseQuery(one_query, this._parts.escapeQuerySpace);
    two_map = URI.parseQuery(two_query, this._parts.escapeQuerySpace);

    for (key in one_map) {
      if (hasOwn.call(one_map, key)) {
        if (!isArray(one_map[key])) {
          if (one_map[key] !== two_map[key]) {
            return false;
          }
        } else if (!arraysEqual(one_map[key], two_map[key])) {
          return false;
        }

        checked[key] = true;
      }
    }

    for (key in two_map) {
      if (hasOwn.call(two_map, key)) {
        if (!checked[key]) {
          // two contains a parameter not present in one
          return false;
        }
      }
    }

    return true;
  };

  // state
  p.duplicateQueryParameters = function(v) {
    this._parts.duplicateQueryParameters = !!v;
    return this;
  };

  p.escapeQuerySpace = function(v) {
    this._parts.escapeQuerySpace = !!v;
    return this;
  };

  return URI;
}));
/*
    Copyright 2017 Rustici Software

    See the LICENSE.md, you may not use this file except in compliance with the License.

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

/**
cmi5.js AU runtime library

@module Cmi5
*/
var Cmi5;

(function () {
    "use strict";
    var THIS_LIBRARY = {
            // set by the build step
            VERSION: "<%= pkg.version %>",
            NAME: "<%= pkg.name %>",
            DESCRIPTION: "<%= pkg.description %>"
        },
        nativeRequest,
        xdrRequest,
        requestComplete,
        __delay,
        env = {},
        STATE_LMS_LAUNCHDATA = "LMS.LaunchData",
        LAUNCH_MODE_NORMAL = "Normal",
        AGENT_PROFILE_LEARNER_PREFS = "cmi5LearnerPreferences",
        CATEGORY_ACTIVITY_CMI5 = new TinCan.Activity(
            {
                id: "https://w3id.org/xapi/cmi5/context/categories/cmi5"
            }
        ),
        CATEGORY_ACTIVITY_MOVEON = new TinCan.Activity(
            {
                id: "https://w3id.org/xapi/cmi5/context/categories/moveon"
            }
        ),
        OTHER_ACTIVITY_CMI5JS = new TinCan.Activity(
            {
                id: "http://id.tincanapi.com/activity/software/" + THIS_LIBRARY.NAME + "/" + THIS_LIBRARY.VERSION,
                definition: {
                    name: {
                        und: THIS_LIBRARY.NAME + " (" + THIS_LIBRARY.VERSION + ")"
                    },
                    description: {
                        "en": THIS_LIBRARY.DESCRIPTION
                    },
                    type: "http://id.tincanapi.com/activitytype/source"
                }
            }
        ),
        EXTENSION_SESSION_ID = "https://w3id.org/xapi/cmi5/context/extensions/sessionid",
        EXTENSION_MASTERY_SCORE = "https://w3id.org/xapi/cmi5/context/extensions/masteryscore",
        VERB_INITIALIZED_ID = "http://adlnet.gov/expapi/verbs/initialized",
        VERB_TERMINATED_ID = "http://adlnet.gov/expapi/verbs/terminated",
        VERB_COMPLETED_ID = "http://adlnet.gov/expapi/verbs/completed",
        VERB_PASSED_ID = "http://adlnet.gov/expapi/verbs/passed",
        VERB_FAILED_ID = "http://adlnet.gov/expapi/verbs/failed",
        verbDisplay = {},
        launchParameters = [
            "endpoint",
            "fetch",
            "actor",
            "activityId",
            "registration"
        ],
        isInteger;

    // polyfill for Number.isInteger from MDN
    isInteger = function (value) {
        return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
    };

    verbDisplay[VERB_INITIALIZED_ID] = {
        "en": "initialized"
    };
    verbDisplay[VERB_TERMINATED_ID] = {
        "en": "terminated"
    };

    //
    // Detect CORS and XDR support
    //
    env.hasCORS = false;
    env.useXDR = false;

    if (typeof XMLHttpRequest !== "undefined" && typeof (new XMLHttpRequest()).withCredentials !== "undefined") {
        env.hasCORS = true;
    }
    else if (typeof XDomainRequest !== "undefined") {
        env.hasCORS = true;
        env.useXDR = true;
    }

    /**
        Top level interface constructor.

        It is highly recommended to use asynchronous calls to methods supporting a callback.

        @class Cmi5
        @constructor
        @param {String} [launchString] AU Launch URL providing configuration options
        @throws {Error} Invalid launch string
    */
    Cmi5 = function (launchString) {
        this.log("constructor", launchString);
        var url,
            cfg,
            i;

        if (typeof launchString !== "undefined") {
            url = new URI(launchString);
            cfg = url.search(true);

            for (i = 0; i < launchParameters.length; i += 1) {
                if (typeof cfg[launchParameters[i]] === "undefined" || cfg[launchParameters[i]] === "") {
                    throw new Error("Invalid launch string missing or empty parameter: " + launchParameters[i]);
                }
            }

            this.setFetch(cfg.fetch);
            this.setLRS(cfg.endpoint);
            this.setActor(cfg.actor);
            this.setActivity(cfg.activityId);
            this.setRegistration(cfg.registration);
        }
    };

    /**
        Version of this library

        @property VERSION
        @static
        @type String
    */
    Cmi5.VERSION = THIS_LIBRARY.VERSION;

    /**
        Whether or not to enable debug logging

        @property DEBUG
        @static
        @type Boolean
        @default false
    */
    Cmi5.DEBUG = false;

    Cmi5.prototype = {
        _fetch: null,
        _endpoint: null,
        _actor: null,
        _registration: null,
        _activity: null,

        _lrs: null,
        _fetchRequest: null,
        _fetchContent: null,
        _lmsLaunchData: null,
        _contextTemplate: null,
        _learnerPrefs: null,
        _isActive: false,
        _initialized: null,
        _passed: null,
        _failed: null,
        _completed: null,
        _terminated: null,
        _durationStart: null,
        _progress: null,
        _includeSourceActivity: true,

        /**
            Method to call to start the AU runtime

            This is a simplified "boot" sequence for the AU that will call the individual parts of the start up sequence that would otherwise need to be called in order sequentially.

            @method start
            @param {Function} callback Function to call on error or success
            @param {Object} [events] Functions to run at specific execution points
                @param {Function} [events.postFetch] Function to run after retrieving fetchUrl result
                @param {Function} [events.launchData] Function to run after retrieving launch data
                @param {Function} [events.learnerPrefs] Function to run after retrieving learner preferences
                @param {Function} [events.initializeStatement] Function to run after saving initialization statement
        */
        start: function (callback, events) {
            this.log("start");
            var self = this;

            events = events || {};

            self.postFetch(
                function (err) {
                    var prefix = "Failed to start AU - ";

                    if (typeof events.postFetch !== "undefined") {
                        events.postFetch.apply(this, arguments);
                    }
                    if (err !== null) {
                        callback(new Error(prefix + " POST to fetch: " + err));
                        return;
                    }

                    self.loadLMSLaunchData(
                        function (err) {
                            if (typeof events.launchData !== "undefined") {
                                events.launchData.apply(this, arguments);
                            }
                            if (err !== null) {
                                callback(new Error(prefix + " load LMS LaunchData: " + err));
                                return;
                            }

                            self.loadLearnerPrefs(
                                function (err) {
                                    if (typeof events.learnerPrefs !== "undefined") {
                                        events.learnerPrefs.apply(this, arguments);
                                    }
                                    if (err !== null) {
                                        callback(new Error(prefix + " load learner preferences: " + err));
                                        return;
                                    }

                                    self.initialize(
                                        function (err) {
                                            if (typeof events.initializeStatement !== "undefined") {
                                                events.initializeStatement.apply(this, arguments);
                                            }
                                            if (err !== null) {
                                                callback(new Error(prefix + " send initialized statement: " + err));
                                                return;
                                            }

                                            callback(null);
                                        }
                                    );
                                }
                            );
                        }
                    );
                }
            );
        },

        /**
            Method to POST to the fetchUrl to retrieve the LRS credentials

            `setFetch` has to be called first and is called by the constructor if the launch string was provided to it.

            @method postFetch
            @param {Function} [callback] Function to call on error or success
        */
        postFetch: function (callback) {
            this.log("postFetch");
            var self = this,
                cbWrapper;

            if (this._fetch === null) {
                callback(new Error("Can't POST to fetch URL without setFetch"));
                return;
            }

            if (callback) {
                cbWrapper = function (err, xhr) {
                    self.log("postFetch::cbWrapper");
                    self.log("postFetch::cbWrapper", err);
                    self.log("postFetch::cbWrapper", xhr);
                    var parsed,
                        responseContent = xhr.responseText,
                        responseContentType;

                    if (err !== null) {
                        if (err === 0) {
                            err = "Aborted, offline, or invalid CORS endpoint";
                        }
                        else if (/^\d+$/.test(err)) {
                            if (typeof xhr.getResponseHeader !== "undefined") {
                                responseContentType = xhr.getResponseHeader("Content-Type");
                            }
                            else if (typeof xhr.contentType !== "undefined") {
                                responseContentType = xhr.contentType;
                            }
                            if (TinCan.Utils.isApplicationJSON(responseContentType)) {
                                try {
                                    parsed = JSON.parse(responseContent);

                                    if (typeof parsed["error-text"] !== "undefined") {
                                        err = parsed["error-text"] + " (" + parsed["error-code"] + ")";
                                    }
                                    else {
                                        err = "Failed to detect 'error-text' property in JSON error response";
                                    }
                                }
                                catch (ex) {
                                    err = "Failed to parse JSON error response: " + ex;
                                }
                            }
                            else {
                                err = xhr.responseText;
                            }
                        }
                        else {
                            err = xhr.responseText;
                        }
                        callback(new Error(err), xhr, parsed);
                        return;
                    }

                    try {
                        parsed = JSON.parse(responseContent);
                    }
                    catch (ex) {
                        self.log("postFetch::cbWrapper - failed to parse JSON response: " + ex);
                        callback(new Error("Post fetch response malformed: failed to parse JSON response (" + ex + ")"), xhr);
                        return;
                    }

                    if (parsed === null || typeof parsed !== "object" || typeof parsed["auth-token"] === "undefined") {
                        self.log("postFetch::cbWrapper - failed to access 'auth-token' property");
                        callback(new Error("Post fetch response malformed: failed to access 'auth-token' in (" + responseContent + ")"), xhr, parsed);
                        return;
                    }

                    self._fetchContent = parsed;
                    self._lrs.auth = "Basic " + parsed["auth-token"];

                    callback(err, xhr, parsed);
                };
            }

            return this._fetchRequest(
                this._fetch,
                {
                    method: "POST"
                },
                cbWrapper
            );
        },

        /**
            Method to load the LMS.LaunchData state document populated by the LMS

            Fetch data has to have already been loaded, in order to have LRS credential.

            @method loadLMSLaunchData
            @param {Function} callback Function to call on error or success
        */
        loadLMSLaunchData: function (callback) {
            this.log("loadLMSLaunchData");
            var self = this;

            if (this._fetchContent === null) {
                callback(new Error("Can't retrieve LMS Launch Data without successful postFetch"));
                return;
            }

            this._lrs.retrieveState(
                STATE_LMS_LAUNCHDATA,
                {
                    activity: this._activity,
                    agent: this._actor,
                    registration: this._registration,
                    callback: function (err, result) {
                        if (err !== null) {
                            callback(new Error("Failed to retrieve " + STATE_LMS_LAUNCHDATA + " State: " + err), result);
                            return;
                        }

                        //
                        // a missing state isn't an error as far as TinCanJS is concerned, but
                        // getting a 404 on the LMS LaunchData is a problem in cmi5 so fail here
                        // in that case (which is when result is null)
                        //
                        if (result === null) {
                            callback(new Error(STATE_LMS_LAUNCHDATA + " State not found"), result);
                            return;
                        }

                        self._lmsLaunchData = result.contents;

                        //
                        // store a stringified version of the context template for cheap
                        // cloning when we go to prepare it later for use in statements
                        //
                        self._contextTemplate = JSON.stringify(self._lmsLaunchData.contextTemplate);

                        callback(null, result);
                    }
                }
            );
        },

        /**
            Method to load learner prefs agent profile document possibly populated by the LMS

            @method loadLearnerPrefs
            @param {Function} callback Function to call on error or success
        */
        loadLearnerPrefs: function (callback) {
            this.log("loadLearnerPrefs");
            var self = this;

            if (this._lmsLaunchData === null) {
                callback(new Error("Can't retrieve Learner Preferences without successful loadLMSLaunchData"));
                return;
            }

            this._lrs.retrieveAgentProfile(
                AGENT_PROFILE_LEARNER_PREFS,
                {
                    agent: this._actor,
                    callback: function (err, result) {
                        if (err !== null) {
                            callback(new Error("Failed to retrieve " + AGENT_PROFILE_LEARNER_PREFS + " Agent Profile" + err), result);
                            return;
                        }

                        //
                        // result is null when the profile 404s which is not an error,
                        // just means it hasn't been set to anything
                        //
                        if (result !== null) {
                            self._learnerPrefs = result;
                        }
                        else {
                            //
                            // store an empty object locally to be able to distinguish a non-set
                            // preference document vs a non-fetched preference document
                            //
                            self._learnerPrefs = new TinCan.AgentProfile(
                                {
                                    id: AGENT_PROFILE_LEARNER_PREFS,
                                    contentType: "application/json",
                                    contents: {}
                                }
                            );
                        }

                        callback(null, result);
                    }
                }
            );
        },

        /**
            Method to save learner prefs to agent profile document in LRS

            @method saveLearnerPrefs
            @param {Function} [callback] Function to call on error or success
        */
        saveLearnerPrefs: function (callback) {
            this.log("saveLearnerPrefs");
            var self = this,
                result,
                cbWrapper;

            if (this._learnerPrefs === null) {
                result = new Error("Can't save Learner Preferences without first loading them");
                if (callback) {
                    callback(result);
                    return;
                }
                return result;
            }

            if (callback) {
                cbWrapper = function (err, result) {
                    self.log("saveLearnerPrefs - saveAgentProfile callback", err, result);
                    if (err !== null) {
                        callback(new Error("Failed to save " + AGENT_PROFILE_LEARNER_PREFS + " Agent Profile: " + err), result);
                        return;
                    }

                    self._learnerPrefs.etag = TinCan.Utils.getSHA1String(
                        (typeof self._learnerPrefs.contents === "object" && TinCan.Utils.isApplicationJSON(self._learnerPrefs.contentType))
                            ? JSON.stringify(self._learnerPrefs.contents)
                            : self._learnerPrefs.contents
                    );

                    callback(null, result);
                };
            }

            result = this._lrs.saveAgentProfile(
                AGENT_PROFILE_LEARNER_PREFS,
                this._learnerPrefs.contents,
                {
                    agent: this._actor,
                    lastSHA1: this._learnerPrefs.etag,
                    contentType: this._learnerPrefs.contentType,
                    callback: cbWrapper
                }
            );
            if (cbWrapper) {
                return;
            }

            if (result.err !== null) {
                return new Error("Failed to save " + AGENT_PROFILE_LEARNER_PREFS + " Agent Profile: " + result.err);
            }

            self._learnerPrefs.etag = TinCan.Utils.getSHA1String(
                (typeof self._learnerPrefs.contents === "object" && TinCan.Utils.isApplicationJSON(self._learnerPrefs.contentType))
                    ? JSON.stringify(self._learnerPrefs.contents)
                    : self._learnerPrefs.contents
            );

            return;
        },

        /**
            Finalize initialization process by sending initialized statement, starting duration tracker, and marking AU active

            @method initialize
            @param {Function} [callback] Function to call on error or success
            @throws {Error} <ul><li>Learner prefs not loaded</li><li>AU already initialized</li></ul>
        */
        initialize: function (callback) {
            this.log("initialize");
            var st,
                err,
                callbackWrapper,
                result;

            if (this._learnerPrefs === null) {
                err = new Error("Can't send initialized statement without successful loadLearnerPrefs");
                if (callback) {
                    callback(err);
                    return;
                }

                throw err;
            }

            if (this._initialized) {
                this.log("initialize - already initialized");

                err = new Error("AU already initialized");
                if (callback) {
                    callback(err);
                    return;
                }

                throw err;
            }

            st = this.initializedStatement();

            if (callback) {
                callbackWrapper = function (err) {
                    this.log("initialize - callbackWrapper: " + err);
                    if (err === null) {
                        this._initialized = true;
                        this._isActive = true;
                        this._durationStart = new Date().getTime();
                    }

                    callback.apply(this, arguments);
                }.bind(this);
            }

            result = this.sendStatement(st, callbackWrapper);
            this.log("initialize - result: ", result);

            if (! callback && result.response.err === null) {
                this._initialized = true;
                this._isActive = true;
                this._durationStart = new Date().getTime();
            }

            return result;
        },

        /**
            Method to indicate session termination should occur, sends terminated statement, marks AU inactive

            @method terminate
            @param {Function} [callback] Function to call on error or success
            @throws {Error} <ul><li>AU not initialized</li><li>AU already terminated</li></ul>
        */
        terminate: function (callback) {
            this.log("terminate");
            var st,
                err,
                callbackWrapper,
                result;

            if (! this._initialized) {
                this.log("terminate - not initialized");

                err = new Error("AU not initialized");
                if (callback) {
                    callback(err);
                    return;
                }

                throw err;
            }

            if (this._terminated) {
                this.log("terminate - already terminated");

                err = new Error("AU already terminated");
                if (callback) {
                    callback(err);
                    return;
                }

                throw err;
            }

            st = this.terminatedStatement();

            if (callback) {
                callbackWrapper = function (err) {
                    this.log("terminate - callbackWrapper: " + err);
                    if (err === null) {
                        this._terminated = true;
                        this._isActive = false;
                    }

                    callback.apply(this, arguments);
                }.bind(this);
            }

            result = this.sendStatement(st, callbackWrapper);
            this.log("terminate - result: ", result);

            if (! callback && result.response.err === null) {
                this._terminated = true;
                this._isActive = false;
            }

            return result;
        },

        /**
            Method to indicate learner has completed the AU, sends completed statement

            @method completed
            @param {Function} [callback] Function to call on error or success
            @throws {Error} <ul><li>AU not active</li><li>AU not in normal launch mode</li><li>AU already completed</li></ul>
        */
        completed: function (callback) {
            this.log("completed");
            var st,
                err,
                callbackWrapper,
                result;

            if (! this.isActive()) {
                this.log("completed - not active");
                err = new Error("AU not active");

                if (callback) {
                    callback(err);
                    return;
                }

                throw err;
            }

            if (this.getLaunchMode() !== LAUNCH_MODE_NORMAL) {
                this.log("completed - non-Normal launch mode: ", this.getLaunchMode());
                err = new Error("AU not in Normal launch mode");

                if (callback) {
                    callback(err);
                    return;
                }

                throw err;
            }

            if (this._completed) {
                this.log("completed - already completed");
                err = new Error("AU already completed");

                if (callback) {
                    callback(err);
                    return;
                }

                throw err;
            }

            st = this.completedStatement();

            if (callback) {
                callbackWrapper = function (err) {
                    this.log("completed - callbackWrapper: " + err);
                    if (err === null) {
                        this.setProgress(null);
                        this._completed = true;
                    }

                    callback.apply(this, arguments);
                }.bind(this);
            }

            result = this.sendStatement(st, callbackWrapper);
            this.log("completed - result: ", result);

            if (! callback && result.response.err === null) {
                this.setProgress(null);
                this._completed = true;
            }

            return result;
        },

        /**
            Method to indicate learner has passed the AU, sends passed statement with optional score

            @method passed
            @param {Object} [score] Score to be included in statement (see `passedStatement`)
            @param {Function} [callback] Function to call on error or success
            @throws {Error} <ul><li>AU not active,</li><li>AU not in normal launch mode,</li><li>AU already passed,</li><li>Failed to create passed statement (usually because of malformed score)</li></ul>
        */
        passed: function (score, callback) {
            this.log("passed");
            var st,
                err,
                callbackWrapper,
                result;

            if (! this.isActive()) {
                this.log("passed - not active");
                err = new Error("AU not active");

                if (callback) {
                    callback(err);
                    return;
                }

                throw err;
            }

            if (this.getLaunchMode() !== LAUNCH_MODE_NORMAL) {
                this.log("passed - non-Normal launch mode: ", this.getLaunchMode());
                err = new Error("AU not in Normal launch mode");

                if (callback) {
                    callback(err);
                    return;
                }

                throw err;
            }

            if (this._passed !== null) {
                this.log("passed - already passed");
                err = new Error("AU already passed");

                if (callback) {
                    callback(err);
                    return;
                }

                throw err;
            }

            try {
                st = this.passedStatement(score);
            }
            catch (ex) {
                this.log("passed - failed to create passed statement: " + ex);
                if (callback) {
                    callback("Failed to create passed statement - " + ex);
                    return;
                }

                throw ex;
            }

            if (callback) {
                callbackWrapper = function (err) {
                    this.log("passed - callbackWrapper: " + err);
                    if (err === null) {
                        this._passed = true;
                    }

                    callback.apply(this, arguments);
                }.bind(this);
            }

            result = this.sendStatement(st, callbackWrapper);
            this.log("passed - result: ", result);

            if (! callback && result.response.err === null) {
                this._passed = true;
            }

            return result;
        },

        /**
            Method to indicate learner has failed the AU, sends failed statement with optional score

            @method failed
            @param {Object} [score] Score to be included in statement (see `failedStatement`)
            @param {Function} [callback] Function to call on error or success
            @throws {Error} <ul><li>AU not active</li><li>AU not in normal launch mode</li><li>AU already passed/failed</li><li>Failed to create failed statement (usually because of malformed score)</li></ul>
        */
        failed: function (score, callback) {
            this.log("failed");
            var st,
                err,
                callbackWrapper,
                result;

            if (! this.isActive()) {
                this.log("failed - not active");
                err = new Error("AU not active");

                if (callback) {
                    callback(err);
                    return;
                }

                throw err;
            }

            if (this.getLaunchMode() !== LAUNCH_MODE_NORMAL) {
                this.log("failed - non-Normal launch mode: ", this.getLaunchMode());
                err = new Error("AU not in Normal launch mode");

                if (callback) {
                    callback(err);
                    return;
                }

                throw err;
            }

            if (this._failed !== null || this._passed !== null) {
                this.log("failed - already passed/failed");
                err = new Error("AU already passed/failed");

                if (callback) {
                    callback(err);
                    return;
                }

                throw err;
            }

            try {
                st = this.failedStatement(score);
            }
            catch (ex) {
                this.log("failed - failed to create failed statement: " + ex);
                if (callback) {
                    callback("Failed to create failed statement - " + ex);
                    return;
                }

                throw ex;
            }

            if (callback) {
                callbackWrapper = function (err) {
                    this.log("failed - callbackWrapper: " + err);
                    if (err === null) {
                        this._failed = true;
                    }

                    callback.apply(this, arguments);
                }.bind(this);
            }

            result = this.sendStatement(st, callbackWrapper);
            this.log("failed - result: ", result);

            if (! callback && result.response.err === null) {
                this._failed = true;
            }

            return result;
        },

        /**
            Method indicating whether the AU is currently active, has been initialized and not terminated

            @method isActive
            @return {Boolean} Active flag
        */
        isActive: function () {
            this.log("isActive");
            return this._isActive;
        },

        /**
            Safe version of logging, only displays when .DEBUG is true, and console.log
            is available

            See `console.log` for parameters.

            @method log
        */
        log: function () {
            /* eslint-disable no-console */
            if (Cmi5.DEBUG && typeof console !== "undefined" && console.log) {
                arguments[0] = "cmi5.js:" + arguments[0];
                console.log.apply(console, arguments);
            }
            /* eslint-enable no-console */
        },

        /**
            Switch on/off whether a source activity is included in statements by default

            Default: on

            @method includeSourceActivity
            @param {Boolean} val true is include, false is exclude
        */
        includeSourceActivity: function (val) {
            this._includeSourceActivity = val ? true : false;
        },

        /**
            Retrieve the launch method as provided in the LMS launch data

            @method getLaunchMethod
            @throws {Error} LMS launch data has not been loaded
            @return {String} launch method
        */
        getLaunchMethod: function () {
            this.log("getLaunchMethod");
            if (this._lmsLaunchData === null) {
                throw new Error("Can't determine launchMethod until LMS LaunchData has been loaded");
            }

            return this._lmsLaunchData.launchMethod;
        },

        /**
            Retrieve the launch mode as provided in the LMS launch data

            @method getLaunchMode
            @throws {Error} LMS launch data has not been loaded
            @return {String} launch mode
        */
        getLaunchMode: function () {
            this.log("getLaunchMode");
            if (this._lmsLaunchData === null) {
                throw new Error("Can't determine launchMode until LMS LaunchData has been loaded");
            }

            return this._lmsLaunchData.launchMode;
        },

        /**
            Retrieve the launch parameters when provided by the AU and in the launch data

            @method getLaunchParameters
            @throws {Error} LMS launch data has not been loaded
            @return {String|null} launch parameters when exist or null
        */
        getLaunchParameters: function () {
            this.log("getLaunchParameters");
            var result = null;

            if (this._lmsLaunchData === null) {
                throw new Error("Can't determine LaunchParameters until LMS LaunchData has been loaded");
            }

            if (typeof this._lmsLaunchData.launchParameters !== "undefined") {
                result = this._lmsLaunchData.launchParameters;
            }

            return result;
        },

        /**
            Retrieve the session id

            @method getSessionId
            @throws {Error} LMS launch data has not been loaded
            @return {String} session id
        */
        getSessionId: function () {
            this.log("getSessionId");
            if (this._lmsLaunchData === null) {
                throw new Error("Can't determine session id until LMS LaunchData has been loaded");
            }

            return this._lmsLaunchData.contextTemplate.extensions[EXTENSION_SESSION_ID];
        },

        /**
            Retrieve the moveOn value

            @method getMoveOn
            @throws {Error} LMS launch data has not been loaded
            @return {String} moveOn value
        */
        getMoveOn: function () {
            this.log("getMoveOn");
            if (this._lmsLaunchData === null) {
                throw new Error("Can't determine moveOn until LMS LaunchData has been loaded");
            }

            return this._lmsLaunchData.moveOn;
        },

        /**
            Retrieve the mastery score as provided in LMS launch data

            @method getMasteryScore
            @throws {Error} LMS launch data has not been loaded
            @return {String|null} mastery score or null
        */
        getMasteryScore: function () {
            this.log("getMasteryScore");
            var result = null;

            if (this._lmsLaunchData === null) {
                throw new Error("Can't determine masteryScore until LMS LaunchData has been loaded");
            }

            if (typeof this._lmsLaunchData.masteryScore !== "undefined") {
                result = this._lmsLaunchData.masteryScore;
            }

            return result;
        },

        /**
            Retrieve the return URL as provided in LMS launch data

            @method getReturnURL
            @throws {Error} LMS launch data has not been loaded
            @return {String|null} mastery score or null
        */
        getReturnURL: function () {
            this.log("getReturnURL");
            var result = null;

            if (this._lmsLaunchData === null) {
                throw new Error("Can't determine returnURL until LMS LaunchData has been loaded");
            }

            if (typeof this._lmsLaunchData.returnURL !== "undefined") {
                result = this._lmsLaunchData.returnURL;
            }

            return result;
        },

        /**
            Retrieve the entitlement key as provided in LMS launch data

            @method getEntitlementKey
            @throws {Error} LMS launch data has not been loaded
            @return {String|null} entitlement key
        */
        getEntitlementKey: function () {
            this.log("getEntitlementKey");
            var result = null;

            if (this._lmsLaunchData === null) {
                throw new Error("Can't determine entitlementKey until LMS LaunchData has been loaded");
            }

            if (typeof this._lmsLaunchData.entitlementKey !== "undefined") {
                if (typeof this._lmsLaunchData.entitlementKey.alternate !== "undefined") {
                    result = this._lmsLaunchData.entitlementKey.alternate;
                }
                else if (typeof this._lmsLaunchData.entitlementKey.courseStructure !== "undefined") {
                    result = this._lmsLaunchData.entitlementKey.courseStructure;
                }
            }

            return result;
        },

        /**
            Retrieve the language preference as provided in learner preferences

            @method getLanguagePreference
            @throws {Error} Learner preference data has not been loaded
            @return {String|null} language preference
        */
        getLanguagePreference: function () {
            this.log("getLanguagePreference");
            var result = null;

            if (this._learnerPrefs === null) {
                throw new Error("Can't determine language preference until learner preferences have been loaded");
            }

            if (typeof this._learnerPrefs.contents.languagePreference !== "undefined") {
                result = this._learnerPrefs.contents.languagePreference;
            }

            return result;
        },

        /**
            Locally set the learner's language preference

            @method setLanguagePreference
            @param {String} pref language preference code (use `""` to unset)
            @throws {Error} Learner preference data has not been loaded
        */
        setLanguagePreference: function (pref) {
            this.log("setLanguagePreference");

            if (this._learnerPrefs === null) {
                throw new Error("Can't set language preference until learner preferences have been loaded");
            }

            if (pref === "") {
                pref = null;
            }

            this._learnerPrefs.contents.languagePreference = pref;

            return;
        },

        /**
            Retrieve the audio preference as provided in learner preferences

            @method getAudioPreference
            @throws {Error} Learner preference data has not been loaded
            @return {String|null} audio preference
        */
        getAudioPreference: function () {
            this.log("getAudioPreference");
            var result = null;

            if (this._learnerPrefs === null) {
                throw new Error("Can't determine audio preference until learner preferences have been loaded");
            }

            if (typeof this._learnerPrefs.contents.audioPreference !== "undefined") {
                result = this._learnerPrefs.contents.audioPreference;
            }

            return result;
        },

        /**
            Locally set the learner's audio preference

            @method setAudioPreference
            @param {String} pref "on", "off", or `null`
            @throws {Error} Learner preference data has not been loaded
        */
        setAudioPreference: function (pref) {
            this.log("setAudioPreference");

            if (this._learnerPrefs === null) {
                throw new Error("Can't set audio preference until learner preferences have been loaded");
            }

            if (pref !== "on" && pref !== "off" && pref !== null) {
                throw new Error("Unrecognized value for audio preference: " + pref);
            }

            this._learnerPrefs.contents.audioPreference = pref;

            return;
        },

        /**
            Get the duration of this session so far

            @method getDuration
            @return {Number} Number of milliseconds
        */
        getDuration: function () {
            this.log("getDuration");

            return (new Date().getTime() - this._durationStart);
        },

        /**
            Locally set the progress towards completion

            @method setProgress
            @param {Integer} progress progress as a percentage between 0 and 100
            @throws {Error} <ul><li>Not an integer</li><li>Less than zero or greater than 100</li></ul>
        */
        setProgress: function (progress) {
            this.log("setProgress: ", progress);

            if (progress !== null) {
                if (! isInteger(progress)) {
                    throw new Error("Invalid progress measure (not an integer): " + progress);
                }
                if (progress < 0 || progress > 100) {
                    throw new Error("Invalid progress measure must be greater than or equal to 0 and less than or equal to 100: " + progress);
                }
            }
            this._progress = progress;
        },

        /**
            Get progress

            @method getProgress
            @return {Integer|null} Integer value of locally set progress measure or null when not set
        */
        getProgress: function () {
            this.log("getProgress");
            return this._progress;
        },

        /**
            Set the fetch URL, called by the `Cmi5` constructor when provided a launch URL

            @method setFetch
            @param {String} fetchURL fetchURL as provided by the LMS in the launch string
        */
        setFetch: function (fetchURL) {
            this.log("setFetch: ", fetchURL);
            var urlParts,
                schemeMatches,
                locationPort,
                isXD;

            this._fetch = fetchURL;

            //
            // default to native request mode
            //
            this._fetchRequest = nativeRequest;

            // TODO: swap this for uri.js

            urlParts = fetchURL.toLowerCase().match(/([A-Za-z]+:)\/\/([^:\/]+):?(\d+)?(\/.*)?$/);
            if (urlParts === null) {
                throw new Error("URL invalid: failed to divide URL parts");
            }

            //
            // determine whether this is a cross domain request,
            // whether our browser has CORS support at all, and then
            // if it does then if we are in IE with XDR only check that
            // the schemes match to see if we should be able to talk to
            // the other side
            //
            locationPort = location.port;
            schemeMatches = location.protocol.toLowerCase() === urlParts[1];

            //
            // normalize the location.port cause it appears to be "" when 80/443
            // but our endpoint may have provided it
            //
            if (locationPort === "") {
                locationPort = (location.protocol.toLowerCase() === "http:" ? "80" : (location.protocol.toLowerCase() === "https:" ? "443" : ""));
            }

            isXD = (

                // is same scheme?
                ! schemeMatches

                // is same host?
                || location.hostname.toLowerCase() !== urlParts[2]

                // is same port?
                || locationPort !== (
                    (urlParts[3] !== null && typeof urlParts[3] !== "undefined" && urlParts[3] !== "")
                        ? urlParts[3]
                        : (urlParts[1] === "http:" ? "80" : (urlParts[1] === "https:" ? "443" : "")
                    )
                )
            );
            if (isXD) {
                if (env.hasCORS) {
                    if (env.useXDR && schemeMatches) {
                        this._fetchRequest = xdrRequest;
                    }
                    else if (env.useXDR && ! schemeMatches) {
                        this.log("[error] URL invalid: cross domain request for differing scheme in IE with XDR");
                        throw new Error("URL invalid: cross domain request for differing scheme in IE with XDR");
                    }
                }
                else {
                    this.log("[error] URL invalid: cross domain requests not supported in this browser");
                    throw new Error("URL invalid: cross domain requests not supported in this browser");
                }
            }
        },

        /**
            Retrieve the fetch URL

            @method getFetch
            @return {String} the previous set fetch URL
        */
        getFetch: function () {
            return this._fetch;
        },

        /**
            Initialize the LRS to a `TinCan.LRS` object or update the existing object which will be used for all xAPI communications

            Called by the `Cmi5` constructor when provided a launch URL.

            @method setLRS
            @param {String} endpoint LRS location
            @param {String} auth Authentication token value
        */
        setLRS: function (endpoint, auth) {
            this.log("setLRS: ", endpoint, auth);
            if (this._lrs !== null) {
                if ((typeof auth === "undefined" && endpoint === null) || endpoint !== null) {
                    this._endpoint = this._lrs.endpoint = endpoint;
                }
                if (typeof auth !== "undefined" && auth !== null) {
                    this._lrs.auth = auth;
                }
            }
            else {
                this._lrs = new TinCan.LRS(
                    {
                        endpoint: endpoint,
                        auth: auth,
                        allowFail: false
                    }
                );
            }
        },

        /**
            Retrieve the `TinCan.LRS` object

            @method getLRS
            @return {TinCan.LRS} LRS object
        */
        getLRS: function () {
            return this._lrs;
        },

        /**
            Initialize the actor using a `TinCan.Agent` that will represent the learner

            Called by the `Cmi5` constructor when provided a launch URL.

            @method setActor
            @param {String|TinCan.Agent} agent Pre-constructed Agent or string of JSON used to construct Agent
            @throws {Error} <ul><li>Invalid actor, missing account IFI</li><li>Invalid account IFI</li></ul>
        */
        setActor: function (agent) {
            if (! (agent instanceof TinCan.Agent)) {
                agent = TinCan.Agent.fromJSON(agent);
            }

            //
            // don't generally want to do too much validation as the LMS
            // should be giving us valid information, *but* in this case
            // users need to be able to count on the type of object being
            // returned
            //
            if ((agent.account === null) || (! (agent.account instanceof TinCan.AgentAccount))) {
                throw new Error("Invalid actor: missing or invalid account");
            }
            else if (agent.account.name === null) {
                throw new Error("Invalid actor: name is null");
            }
            else if (agent.account.name === "") {
                throw new Error("Invalid actor: name is empty");
            }
            else if (agent.account.homePage === null) {
                throw new Error("Invalid actor: homePage is null");
            }
            else if (agent.account.homePage === "") {
                throw new Error("Invalid actor: homePage is empty");
            }

            this._actor = agent;
        },

        /**
            Retrieve the `TinCan.Agent` object representing the learner

            @method getActor
            @return {TinCan.Agent} Learner's Agent
        */
        getActor: function () {
            return this._actor;
        },

        /**
            Initialize the root object representing the AU

            Called by the `Cmi5` constructor when provided a launch URL.

            @method setActivity
            @param {String|TinCan.Activity} activity Pre-constructed Activity or string id used to construct Activity
            @throws {Error} <ul><li>Invalid activity, null id</li><li>Invalid activity, empty string id</li></ul>
        */
        setActivity: function (activity) {
            if (! (activity instanceof TinCan.Activity)) {
                activity = new TinCan.Activity(
                    {
                        id: activity
                    }
                );
            }

            if (activity.id === null) {
                throw new Error("Invalid activity: id is null");
            }
            else if (activity.id === "") {
                throw new Error("Invalid activity: id is empty");
            }

            this._activity = activity;
        },

        /**
            Retrieve the `TinCan.Activity` that is the root object representing the AU

            @method getActivity
            @return {TinCan.Activity} Root Activity
        */
        getActivity: function () {
            return this._activity;
        },

        /**
            Set the registration value

            Called by the `Cmi5` constructor when provided a launch URL.

            @method setRegistration
            @param {String} registration UUID representing the registration
            @throws {Error} <ul><li>Invalid registration, null</li><li>Invalid registration, empty string</li></ul>
        */
        setRegistration: function (registration) {
            if (registration === null) {
                throw new Error("Invalid registration: null");
            }
            else if (registration === "") {
                throw new Error("Invalid registration: empty");
            }

            this._registration = registration;
        },

        /**
            Retrieve the registration associated with the session

            @method getRegistration
            @return {String} Registration
        */
        getRegistration: function () {
            return this._registration;
        },

        /**
            Validate a Score object's properties

            @method validateScore
            @param {TinCan.Score|Object} score Score object to validate
            @throws {Error} <ul><li>Null or missing score argument</li><li>Non-integer min or max value (when provided)</li><li>Non-number, negative, or greater than 1 scaled value (when provided)</li><li>Non-integer, missing or invalid min/max, raw value (when provided)</li></ul>
            @return {Boolean} true for passing, otherwise exception is thrown
        */
        validateScore: function (score) {
            if (typeof score === "undefined" || score === null) {
                throw new Error("cannot validate score (score not provided): " + score);
            }

            if (typeof score.min !== "undefined") {
                if (! isInteger(score.min)) {
                    throw new Error("score.min is not an integer");
                }
            }
            if (typeof score.max !== "undefined") {
                if (! isInteger(score.max)) {
                    throw new Error("score.max is not an integer");
                }
            }

            if (typeof score.scaled !== "undefined") {
                if (! /^(\-|\+)?[01]+(\.[0-9]+)?$/.test(score.scaled)) {
                    throw new Error("scaled score not a recognized number: " + score.scaled);
                }

                if (score.scaled < 0) {
                    throw new Error("scaled score must be greater than or equal to 0");
                }
                if (score.scaled > 1) {
                    throw new Error("scaled score must be less than or equal to 1");
                }
            }

            if (typeof score.raw !== "undefined") {
                if (! isInteger(score.raw)) {
                    throw new Error("score.raw is not an integer");
                }
                if (typeof score.min === "undefined") {
                    throw new Error("minimum score must be provided when including a raw score");
                }
                if (typeof score.max === "undefined") {
                    throw new Error("maximum score must be provided when including a raw score");
                }
                if (score.raw < score.min) {
                    throw new Error("raw score must be greater than or equal to minimum score");
                }
                if (score.raw > score.max) {
                    throw new Error("raw score must be less than or equal to maximum score");
                }
            }

            return true;
        },

        /**
            Prepare a cmi5 "allowed" statement including the actor, verb, object, and context

            Used to construct a cmi5 "allowed" statement with all relevant information that can then be optionally added to prior to sending.

            @method prepareStatement
            @param {String} verbId Verb identifier combined with other cmi5 pre-determined information (must be IRI)
            @return {TinCan.Statement} Statement
        */
        prepareStatement: function (verbId) {
            //
            // the specification allows for statements that are 'cmi5 allowed'
            // as oppposed to 'cmi5 defined' to be sent by the AU, so give the
            // AU the ability to get a prepared statement with a populated context
            // based on the template but without the category having been added
            //
            var stCfg = {
                    actor: this._actor,
                    verb: {
                        id: verbId
                    },
                    target: this._activity,
                    context: this._prepareContext()
                },
                progress = this.getProgress();

            if (typeof verbDisplay[verbId] !== "undefined") {
                stCfg.verb.display = verbDisplay[verbId];
            }

            if (verbId !== VERB_COMPLETED_ID && progress !== null) {
                stCfg.result = {
                    extensions: {
                        "https://w3id.org/xapi/cmi5/result/extensions/progress": progress
                    }
                };
            }

            return new TinCan.Statement(stCfg);
        },

        /**
            Store provided statement in the configured LRS

            @method sendStatement
            @param {TinCan.Statement} st Statement to be stored
            @param {Function} [callback] Function to run on success/failure of statement save
        */
        sendStatement: function (st, callback) {
            var cbWrapper,
                result;

            if (callback) {
                cbWrapper = function (err, result) {
                    if (err !== null) {
                        callback(new Error(err), result);
                        return;
                    }

                    callback(err, result, st);
                };
            }

            result = this._lrs.saveStatement(
                st,
                {
                    callback: cbWrapper
                }
            );
            if (! callback) {
                return {
                    response: result,
                    statement: st
                };
            }
        },

        /*
         * The ...Statement methods are provided for users that want to implement
         * a queueing like mechansim or something similar where they are expected
         * to abide by the AU restrictions on what statements can be sent, etc. on
         * their own.
         *
         * (Such as in SCORM Driver which was the impetus for adding them.)
        */

        /**
            Advanced Usage: retrieve prepared "initialized" statement

            Statement methods are provided for users that want to implement
            a queueing like mechansim or something similar where they are expected
            to abide by the AU restrictions on what statements can be sent, etc. on
            their own.

            @method initializedStatement
            @return {TinCan.Statement} Initialized statement
        */
        initializedStatement: function () {
            this.log("initializedStatement");
            return this._prepareStatement(VERB_INITIALIZED_ID);
        },

        /**
            Advanced Usage: retrieve prepared "terminated" statement

            Statement methods are provided for users that want to implement
            a queueing like mechansim or something similar where they are expected
            to abide by the AU restrictions on what statements can be sent, etc. on
            their own.

            @method terminatedStatement
            @return {TinCan.Statement} Terminated statement
        */
        terminatedStatement: function () {
            this.log("terminatedStatement");
            var st = this._prepareStatement(VERB_TERMINATED_ID);

            st.result = st.result || new TinCan.Result();
            st.result.duration = TinCan.Utils.convertMillisecondsToISO8601Duration(this.getDuration());

            return st;
        },

        /**
            Advanced Usage: retrieve prepared "passed" statement

            Statement methods are provided for users that want to implement
            a queueing like mechansim or something similar where they are expected
            to abide by the AU restrictions on what statements can be sent, etc. on
            their own.

            @method passedStatement
            @param {Object} [score] Object to be used as the score, must meet masteryScore requirements, etc.
            @return {TinCan.Statement} Passed statement
        */
        passedStatement: function (score) {
            this.log("passedStatement");
            var st = this._prepareStatement(VERB_PASSED_ID),
                masteryScore;

            st.result = st.result || new TinCan.Result();
            st.result.success = true;
            st.result.duration = TinCan.Utils.convertMillisecondsToISO8601Duration(this.getDuration());

            if (score) {
                try {
                    this.validateScore(score);
                }
                catch (ex) {
                    throw new Error("Invalid score - " + ex);
                }

                masteryScore = this.getMasteryScore();
                if (masteryScore !== null && typeof score.scaled !== "undefined") {
                    if (score.scaled < masteryScore) {
                        throw new Error("Invalid score - scaled score does not meet or exceed mastery score (" + score.scaled + " < " + masteryScore + ")");
                    }

                    st.context.extensions = st.context.extensions || {};
                    st.context.extensions[EXTENSION_MASTERY_SCORE] = masteryScore;
                }

                st.result.score = new TinCan.Score(score);
            }

            st.context.contextActivities.category.push(CATEGORY_ACTIVITY_MOVEON);

            return st;
        },

        /**
            Advanced Usage: retrieve prepared "failed" statement

            Statement methods are provided for users that want to implement
            a queueing like mechansim or something similar where they are expected
            to abide by the AU restrictions on what statements can be sent, etc. on
            their own.

            @method failedStatement
            @param {Object} [score] Object to be used as the score, must meet masteryScore requirements, etc.
            @return {TinCan.Statement} Failed statement
        */
        failedStatement: function (score) {
            this.log("failedStatement");
            var st = this._prepareStatement(VERB_FAILED_ID),
                masteryScore;

            st.result = st.result || new TinCan.Result();
            st.result.success = false;
            st.result.duration = TinCan.Utils.convertMillisecondsToISO8601Duration(this.getDuration());

            if (score) {
                try {
                    this.validateScore(score);
                }
                catch (ex) {
                    throw new Error("Invalid score - " + ex);
                }

                masteryScore = this.getMasteryScore();
                if (masteryScore !== null && typeof score.scaled !== "undefined") {
                    if (score.scaled >= masteryScore) {
                        throw new Error("Invalid score - scaled score exceeds mastery score (" + score.scaled + " >= " + masteryScore + ")");
                    }

                    st.context.extensions = st.context.extensions || {};
                    st.context.extensions[EXTENSION_MASTERY_SCORE] = masteryScore;
                }

                st.result.score = new TinCan.Score(score);
            }

            st.context.contextActivities.category.push(CATEGORY_ACTIVITY_MOVEON);

            return st;
        },

        /**
            Advanced Usage: retrieve prepared "completed" statement

            Statement methods are provided for users that want to implement
            a queueing like mechansim or something similar where they are expected
            to abide by the AU restrictions on what statements can be sent, etc. on
            their own.

            @method completedStatement
            @return {TinCan.Statement} Completed statement
        */
        completedStatement: function () {
            this.log("completedStatement");
            var st = this._prepareStatement(VERB_COMPLETED_ID);

            st.result = st.result || new TinCan.Result();
            st.result.completion = true;
            st.result.duration = TinCan.Utils.convertMillisecondsToISO8601Duration(this.getDuration());

            st.context.contextActivities.category.push(CATEGORY_ACTIVITY_MOVEON);

            return st;
        },

        /**
            @method _prepareContext
            @private
        */
        _prepareContext: function () {
            //
            // deserializing a string version of the template is slower
            // but gives us cheap cloning capability so that we don't
            // alter the template itself
            //
            var context = JSON.parse(this._contextTemplate);

            context.registration = this._registration;

            if (this._includeSourceActivity) {
                context.contextActivities = context.contextActivities || new TinCan.ContextActivities();
                context.contextActivities.other = context.contextActivities.other || [];
                context.contextActivities.other.push(OTHER_ACTIVITY_CMI5JS);
            }

            return context;
        },

        /**
            @method _prepareStatement
            @private
        */
        _prepareStatement: function (verbId) {
            //
            // statements sent by this lib are "cmi5 defined" statements meaning
            // they have the context category value added
            //
            var st = this.prepareStatement(verbId);

            st.context.contextActivities = st.context.contextActivities || new TinCan.ContextActivities();
            st.context.contextActivities.category = st.context.contextActivities.category || [];
            st.context.contextActivities.category.push(CATEGORY_ACTIVITY_CMI5);

            return st;
        }
    };

    /**
        Turn on debug logging

        @method enableDebug
        @static
        @param {Boolean} [includeTinCan] Whether to enable debug logging from TinCanJS
    */
    Cmi5.enableDebug = function (includeTinCan) {
        Cmi5.DEBUG = true;

        if (includeTinCan) {
            TinCan.enableDebug();
        }
    };

    /**
        Turn off debug logging

        @method disableDebug
        @static
        @param {Boolean} [includeTinCan] Whether to disable debug logging from TinCanJS
    */
    Cmi5.disableDebug = function (includeTinCan) {
        Cmi5.DEBUG = false;

        if (includeTinCan) {
            TinCan.disableDebug();
        }
    };

    //
    // Setup request callback
    //
    requestComplete = function (xhr, cfg, control, callback) {
        this.log("requestComplete: " + control.finished + ", xhr.status: " + xhr.status);
        var requestCompleteResult,
            notFoundOk,
            httpStatus;

        //
        // XDomainRequest doesn't give us a way to get the status,
        // so allow passing in a forged one
        //
        if (typeof xhr.status === "undefined") {
            httpStatus = control.fakeStatus;
        }
        else {
            //
            // older versions of IE don't properly handle 204 status codes
            // so correct when receiving a 1223 to be 204 locally
            // http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request
            //
            httpStatus = (xhr.status === 1223) ? 204 : xhr.status;
        }

        if (! control.finished) {
            // may be in sync or async mode, using XMLHttpRequest or IE XDomainRequest, onreadystatechange or
            // onload or both might fire depending upon browser, just covering all bases with event hooks and
            // using 'finished' flag to avoid triggering events multiple times
            control.finished = true;

            notFoundOk = (cfg.ignore404 && httpStatus === 404);
            if ((httpStatus >= 200 && httpStatus < 400) || notFoundOk) {
                if (callback) {
                    callback(null, xhr);
                }
                else {
                    requestCompleteResult = {
                        err: null,
                        xhr: xhr
                    };
                    return requestCompleteResult;
                }
            }
            else {
                requestCompleteResult = {
                    err: httpStatus,
                    xhr: xhr
                };
                if (httpStatus === 0) {
                    this.log("[warning] There was a problem communicating with the server. Aborted, offline, or invalid CORS endpoint (" + httpStatus + ")");
                }
                else {
                    this.log("[warning] There was a problem communicating with the server. (" + httpStatus + " | " + xhr.responseText + ")");
                }
                if (callback) {
                    callback(httpStatus, xhr);
                }
                return requestCompleteResult;
            }
        }
        else {
            return requestCompleteResult;
        }
    };

    //
    // one of the two of these is stuffed into the Cmi5 instance where a
    // request is needed which is fetch at the moment
    //
    nativeRequest = function (fullUrl, cfg, callback) {
        this.log("sendRequest using XMLHttpRequest");
        var self = this,
            xhr,
            prop,
            pairs = [],
            data,
            control = {
                finished: false,
                fakeStatus: null
            },
            async,
            fullRequest = fullUrl;

        this.log("sendRequest using XMLHttpRequest - async: " + async);

        cfg = cfg || {};
        cfg.params = cfg.params || {};
        cfg.headers = cfg.headers || {};

        async = typeof callback !== "undefined";

        for (prop in cfg.params) {
            if (cfg.params.hasOwnProperty(prop)) {
                pairs.push(prop + "=" + encodeURIComponent(cfg.params[prop]));
            }
        }
        if (pairs.length > 0) {
            fullRequest += "?" + pairs.join("&");
        }

        xhr = new XMLHttpRequest();

        xhr.open(cfg.method, fullRequest, async);
        for (prop in cfg.headers) {
            if (cfg.headers.hasOwnProperty(prop)) {
                xhr.setRequestHeader(prop, cfg.headers[prop]);
            }
        }

        if (typeof cfg.data !== "undefined") {
            cfg.data += "";
        }
        data = cfg.data;

        if (async) {
            xhr.onreadystatechange = function () {
                self.log("xhr.onreadystatechange - xhr.readyState: " + xhr.readyState);
                if (xhr.readyState === 4) {
                    requestComplete.call(self, xhr, cfg, control, callback);
                }
            };
        }

        //
        // research indicates that IE is known to just throw exceptions
        // on .send and it seems everyone pretty much just ignores them
        // including jQuery (https://github.com/jquery/jquery/blob/1.10.2/src/ajax.js#L549
        // https://github.com/jquery/jquery/blob/1.10.2/src/ajax/xhr.js#L97)
        //
        try {
            xhr.send(data);
        }
        catch (ex) {
            this.log("sendRequest caught send exception: " + ex);
        }

        if (async) {
            return;
        }

        return requestComplete.call(this, xhr, cfg, control);
    };
    xdrRequest = function (fullUrl, cfg, callback) {
        this.log("sendRequest using XDomainRequest");
        var self = this,
            xhr,
            pairs = [],
            data,
            prop,
            until,
            control = {
                finished: false,
                fakeStatus: null
            },
            err;

        cfg = cfg || {};
        cfg.params = cfg.params || {};
        cfg.headers = cfg.headers || {};

        if (typeof cfg.headers["Content-Type"] !== "undefined" && cfg.headers["Content-Type"] !== "application/json") {
            err = new Error("Unsupported content type for IE Mode request");
            if (callback) {
                callback(err, null);
                return null;
            }
            return {
                err: err,
                xhr: null
            };
        }

        for (prop in cfg.params) {
            if (cfg.params.hasOwnProperty(prop)) {
                pairs.push(prop + "=" + encodeURIComponent(cfg.params[prop]));
            }
        }

        if (pairs.length > 0) {
            fullUrl += "?" + pairs.join("&");
        }

        xhr = new XDomainRequest();
        xhr.open("POST", fullUrl);

        if (! callback) {
            xhr.onload = function () {
                control.fakeStatus = 200;
            };
            xhr.onerror = function () {
                control.fakeStatus = 400;
            };
            xhr.ontimeout = function () {
                control.fakeStatus = 0;
            };
        }
        else {
            xhr.onload = function () {
                control.fakeStatus = 200;
                requestComplete.call(self, xhr, cfg, control, callback);
            };
            xhr.onerror = function () {
                control.fakeStatus = 400;
                requestComplete.call(self, xhr, cfg, control, callback);
            };
            xhr.ontimeout = function () {
                control.fakeStatus = 0;
                requestComplete.call(self, xhr, cfg, control, callback);
            };
        }

        //
        // IE likes to randomly abort requests when some handlers
        // aren't defined, so define them with no-ops, see:
        //
        // http://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/
        // http://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified
        //
        xhr.onprogress = function () {};
        xhr.timeout = 0;

        //
        // research indicates that IE is known to just throw exceptions
        // on .send and it seems everyone pretty much just ignores them
        // including jQuery (https://github.com/jquery/jquery/blob/1.10.2/src/ajax.js#L549
        // https://github.com/jquery/jquery/blob/1.10.2/src/ajax/xhr.js#L97)
        //
        try {
            xhr.send(data);
        }
        catch (ex) {
            this.log("sendRequest caught send exception: " + ex);
        }

        if (! callback) {
            // synchronous call in IE, with no synchronous mode available
            until = 10000 + Date.now();
            this.log("sendRequest - until: " + until + ", finished: " + control.finished);

            while (Date.now() < until && control.fakeStatus === null) {
                __delay();
            }
            return requestComplete.call(self, xhr, cfg, control);
        }

        return;
    };

    /**
        Non-environment safe method used to create a delay to give impression
        of synchronous response (for IE, shocker)

        @method __delay
        @private
    */
    __delay = function () {
        //
        // use a synchronous request to the current location to allow the browser
        // to yield to the asynchronous request's events but still block in the
        // outer loop to make it seem synchronous to the end user
        //
        // removing this made the while loop too tight to allow the asynchronous
        // events through to get handled so that the response was correctly handled
        //
        var xhr = new XMLHttpRequest(),
            url = window.location + "?forcenocache=" + TinCan.Utils.getUUID();

        xhr.open("GET", url, false);
        xhr.send(null);
    };
}());
/* declare globals that are set via Desktop token replacement for configuration */
var TC_COURSE_ID = "__TC_COURSE_ID__";
var TC_COURSE_NAME = "__TC_COURSE_NAME__";
var TC_COURSE_DESC = "__TC_COURSE_DESC__";
var TC_RECORD_STORES = "__TC_RECORD_STORES__";

/* declare other globals we'll use */
var TCAPI_STATUS = "",
    TCAPI_STATUS_CHANGED = false,
    TCAPI_SCORE = {},
    TCAPI_COMPLETION_STATUS = "",
    TCAPI_SATISFACTION_STATUS = null,
    TCAPI_UPDATES_PENDING = false,
    TCAPI_IN_PROGRESS = false,

    TCAPI_NO_ERROR = "",
    TCAPI_VERB_COMPLETED = "completed",
    TCAPI_VERB_EXPERIENCED = "experienced",
    TCAPI_VERB_ATTEMPTED = "attempted",
    TCAPI_VERB_ANSWERED = "answered",
    TCAPI_VERB_PASSED = "passed",
    TCAPI_VERB_FAILED = "failed",
    TCAPI_INIT_VERB = TCAPI_VERB_ATTEMPTED,

    TCAPI_INTERACTION = "http://adlnet.gov/expapi/activities/cmi.interaction",
    TCAPI_INTERACTION_TYPE_TRUE_FALSE = "true-false",
    TCAPI_INTERACTION_TYPE_CHOICE = "choice",
    TCAPI_INTERACTION_TYPE_FILL_IN = "fill-in",
    TCAPI_INTERACTION_TYPE_MATCHING = "matching",
    TCAPI_INTERACTION_TYPE_PERFORMANCE = "performance",
    TCAPI_INTERACTION_TYPE_SEQUENCING = "sequencing",
    TCAPI_INTERACTION_TYPE_LIKERT = "likert",
    TCAPI_INTERACTION_TYPE_NUMERIC = "numeric",

    TCAPI_STATE_BOOKMARK = "bookmark",
    TCAPI_STATE_TOTAL_TIME = "cumulative_time",
    TCAPI_STATE_SUSPEND_DATA = "suspend_data",

    // this is a non-HTTP status code error from TinCanJS
    TCAPI_DEP_ERROR = 999,

    TCAPI_ERROR_INVALID_PREFERENCE = 0,
    TCAPI_ERROR_INVALID_TIMESPAN   = 1,

    TCAPI_FUNC_NOOP = function () { /* no-op */ },

    intTCAPIError,
    strTCAPIErrorString,
    strTCAPIErrorDiagnostic;

/* Declare global used for interacting with the LRS */
var tincan;

/* Declare global used for our own local cache of items */
var tcapi_cache;


// this is just here to make it possible to mock the launch URL
// for testing purposes
//
function TCAPI_GetLaunchUrl () {
    WriteToDebug("In TCAPI_GetLaunchUrl");
    return location.href;
}

function TCAPI_Initialize () {
    WriteToDebug("In TCAPI_Initialize");

    tcapi_cache = {
        totalPrevDuration: null,
        statementQueue: []
    };

    TinCan.prototype.log =
    TinCan.LRS.prototype.log = function (msg, src) {
        src = src || this.LOG_SRC || "TinCan";
        WriteToDebug("TinCan." + src + ": " + msg);
    };

    try {
        tincan = new TinCan (
            {
                url: TCAPI_GetLaunchUrl(),
                recordStores: TC_RECORD_STORES,
                activity: {
                    id: TC_COURSE_ID,
                    definition: {
                        name: TC_COURSE_NAME,
                        description: TC_COURSE_DESC
                    }
                }
            }
        );
    } catch (ex) {
        WriteToDebug("TCAPI_Initialize - TinCan construction failed: " + JSON.stringify(ex));
        return;
    }

    if (tincan.recordStores.length === 0) {
        WriteToDebug("TCAPI_Initialize - resulted in no LRS: DATA CANNOT BE STORED");
        return;
    }

    //See if we've saved the cumulative time previously
    WriteToDebug("TCAPI_Initialize - fetching cumulative time from state: " + TCAPI_STATE_TOTAL_TIME);
    tincan.getState(
        TCAPI_STATE_TOTAL_TIME,
        {
            callback: function (err, state) {
                WriteToDebug("TCAPI_Initialize - getState callback");
                var contents;
                if (err !== null) {
                    WriteToDebug("TCAPI_Initialize - getState callback: " + err.responseText + " (" + err.status + ")");
                    return;
                }

                WriteToDebug("TCAPI_Initialize - getState callback - state: " + state);
                if (state !== null && state.contents !== null && state.contents.match(/^\d+$/)) {
                    tcapi_cache.totalPrevDuration = Number(state.contents);
                }
                else {
                    tcapi_cache.totalPrevDuration = 0;
                }
            }
        }
    );

    TCAPI_STATUS = TCAPI_INIT_VERB;
    TCAPI_IN_PROGRESS = true;

    // Record an initial launch statement
    WriteToDebug("TCAPI_Initialize - record initial launch statement");
    tincan.sendStatement(
        {
            verb: TCAPI_INIT_VERB,
            inProgress: TCAPI_IN_PROGRESS
        },
        function (results, statement) {
            if (results[0].err !== null) {
                WriteToDebug("TCAPI_Initialize - record initial launch statement - err: " + results[0].err.responseText + " (" + results[0].err.status + ")");
                return;
            }

            WriteToDebug("TCAPI_Initialize - record initial launch statement success: " + statement.id);
        }
    );

    InitializeExecuted(true, "");

    return true;
}

//
// setting state in IE7 was throwing an "Operation Aborted" message for no apparent
// reason after the state was already set, so just wrap it with a try/catch
//
function _TCAPI_SetStateSafe (key, value) {
    var result;
    try {
        result = tincan.setState(key, value);
    }
    catch (ex) {
        WriteToDebug("In _TCAPI_SetStateSafe - caught exception from setState: " + ex.message);
    }
    return result;
}

function TCAPI_GetStudentID () {
    WriteToDebug("In TCAPI_GetStudentID");

    if (tincan.actor.mbox !== null) {
        return tincan.actor.mbox;
    }
    if (tincan.actor.mbox_sha1sum !== null) {
        return tincan.actor.mbox_sha1sum;
    }
    if (tincan.actor.openid !== null) {
        return tincan.actor.openid;
    }
    if (tincan.actor.account !== null) {
        //
        // it was discussed about whether returning just the account name
        // was a proper approach and decided because SD is really only
        // supported by LMS launch that the account would normally only
        // be used with the launching system's account and that in that
        // case the homePage isn't strictly necessary for ID determination
        //
        // more advanced use cases should avoid using this method if they
        // require the homePage as well
        //
        return tincan.actor.account.name;
    }
    return null;
}

function TCAPI_GetStudentName () {
    WriteToDebug("In TCAPI_GetStudentName");

    return tincan.actor !== null ? tincan.actor.toString() : "";
}

function TCAPI_GetBookmark () {
    WriteToDebug("In TCAPI_GetBookmark");
    var bookmark = "",
        getStateResult = tincan.getState(TCAPI_STATE_BOOKMARK);

    if (getStateResult.state !== null) {
        bookmark = getStateResult.state.contents;
    }

    return bookmark;
}

function TCAPI_SetBookmark (value, name) {
    WriteToDebug("In TCAPI_SetBookmark - value: " + value + ", name: " + name);

    _TCAPI_SetStateSafe(TCAPI_STATE_BOOKMARK, value);

    WriteToDebug("In TCAPI_SetBookmark - sending statement: " + value);
    tincan.sendStatement(
        {
            verb: TCAPI_VERB_EXPERIENCED,
            object: {
                id: tincan.activity.id + "/" + value,
                definition: {
                    name: {
                        "en-US": ((name !== undefined && name !== "") ? name : value)
                    }
                }
            },
            context: {
                contextActivities: {
                    parent: tincan.activity
                }
            }
        },
        function (results, statement) {
            if (results[0].err !== null) {
                WriteToDebug("TCAPI_SetBookmark - sending statement: " + value + " - err: " + results[0].err.responseText + " (" + results[0].err.status + ")");
                return;
            }

            WriteToDebug("TCAPI_SetBookmark - sending statement success: " + value + " - id: " + statement.id);
        }
    );

    return true;
}

function TCAPI_GetDataChunk () {
    WriteToDebug("In TCAPI_GetDataChunk");
    var data = "",
        getStateResult = tincan.getState(TCAPI_STATE_SUSPEND_DATA);

    if (getStateResult.state !== null) {
        data = getStateResult.state.contents;
    }

    return data;
}

function TCAPI_SetDataChunk (value) {
    WriteToDebug("In TCAPI_SetDataChunk");

    _TCAPI_SetStateSafe(TCAPI_STATE_SUSPEND_DATA, value);

    return true;
}

function TCAPI_CommitData () {
    WriteToDebug("In TCAPI_CommitData - TCAPI_STATUS:" + TCAPI_STATUS);
    WriteToDebug("In TCAPI_CommitData - TCAPI_UPDATES_PENDING: " + TCAPI_UPDATES_PENDING);

    var stmt,
        requestResults,
        result,
        statementAdded = false;

    TCAPI_ClearErrorInfo();

    if (TCAPI_UPDATES_PENDING) {
        stmt = {
            verb: TCAPI_STATUS,
            inProgress: TCAPI_IN_PROGRESS,
            result: {}
        };

        if (TCAPI_COMPLETION_STATUS !== '' || ! TCAPI_IN_PROGRESS) {
            // report the duration in the ISO8601 format that SCORM provided
            stmt.result.duration = ConvertMilliSecondsIntoSCORM2004Time(
                GetSessionAccumulatedTime() + TCAPI_GetPreviouslyAccumulatedTime()
            );
        }

        if (TCAPI_COMPLETION_STATUS !== '') {
            stmt.result.completion = true;
        }

        if (TCAPI_SATISFACTION_STATUS !== null) {
            stmt.result.success = TCAPI_SATISFACTION_STATUS;
        }

        if (typeof TCAPI_SCORE.raw !== "undefined") {
            stmt.result.score = TCAPI_SCORE;
        }

        tcapi_cache.statementQueue.push(stmt);

        statementAdded = true;

        // reset the pending updates flag
        TCAPI_UPDATES_PENDING = false;
    }

    if (tcapi_cache.statementQueue.length > 0) {
        requestResults = tincan.sendStatements(
            tcapi_cache.statementQueue
        );

        if (typeof requestResults.results !== "undefined" && requestResults.results.length > 0) {
            //
            // we can only count on caring about the first result because
            // this supports multiple LRS send, but the first is the one
            // the package was configured with at launch
            //
            result = requestResults.results[0];

            if (result.err !== null) {
                errMesg = "Failed to commit data: statements";

                //
                // in the case of errors result.err either contains an Error object (theoretically)
                // or the HTTP status code if the request actually made it to the LRS, to distinguish
                // we can check it for all digits (which is safer than checking for an Error object,
                // at least in this version of TinCanJS), which then allows us to look at the response
                // text which is generally a more helpful message than the status code
                //
                if (/^\d+$/.test(result.err)) {
                    errCode = result.err;

                    if (result.err === 0) {
                        errDiag = "Aborted, offline, or invalid CORS endpoint";
                    }
                    else {
                        errDiag = result.xhr.responseText;
                    }
                }
                else {
                    errCode = TCAPI_DEP_ERROR;
                    errDiag = result.err;
                }

                //
                // if there were updates above, then we added a statement to the queue, but since it
                // couldn't be sent we need to remove it from the queue and restore the fact that there
                // are updates pending, which also leaves the other statements that might have been queued
                // (via interactions for instance) in place for the next commit
                //
                if (statementAdded) {
                    tcapi_cache.statementQueue.pop();

                    TCAPI_UPDATES_PENDING = true;
                }

                TCAPI_SetErrorInfoManually(errCode, errMesg, errDiag);
                return false;
            }
        }

        tcapi_cache.statementQueue = [];
    }

    return true;
}

function TCAPI_Finish (exitType, statusWasSet) {
    WriteToDebug("In TCAPI_Finish - exitType: " + exitType);

    if (exitType === EXIT_TYPE_SUSPEND) {
        _TCAPI_SetStateSafe(
            TCAPI_STATE_TOTAL_TIME,
            TCAPI_GetPreviouslyAccumulatedTime() + GetSessionAccumulatedTime()
        );
        TCAPI_SetSuspended();
    }

    TCAPI_CommitData();

    return true;
}

/*****************************
* Preferences
*****************************/

function TCAPI_GetAudioPlayPreference () {
    WriteToDebug("In TCAPI_GetAudioPlayPreference");
    var intTempPreference = 0,
        getStateResult;

    TCAPI_ClearErrorInfo();

    getStateResult = tincan.getState("cmi.student_preference.audio");
    if (getStateResult.state !== null) {
        intTempPreference = getStateResult.state.contents;
    }

    intTempPreference = parseInt(intTempPreference, 10);
    WriteToDebug("intTempPreference=" + intTempPreference);

    if (intTempPreference > 0) {
        WriteToDebug("Returning On");
        return PREFERENCE_ON;
    }
    else if (intTempPreference == 0) {
        WriteToDebug("Returning Default");
        return PREFERENCE_DEFAULT;
    }
    else if (intTempPreference < 0) {
        WriteToDebug("returning Off");
        return PREFERENCE_OFF;
    }

    WriteToDebug("Error: Invalid preference");
    TCAPI_SetErrorInfoManually(
        TCAPI_ERROR_INVALID_PREFERENCE,
        "Invalid audio preference received from LMS",
        "intTempPreference=" + intTempPreference
    );
    return null;
}

function TCAPI_GetAudioVolumePreference () {
    WriteToDebug("In TCAPI_GetAudioVollumePreference");
    var intTempPreference = 100,
        getStateResult;

    TCAPI_ClearErrorInfo();

    getStateResult = tincan.getState("cmi.student_preference.audio");
    if (getStateResult.state !== null) {
        intTempPreference = getStateResult.state.contents;
    }

    WriteToDebug("intTempPreference=" + intTempPreference);
    intTempPreference = parseInt(intTempPreference, 10);

    if (intTempPreference <= 0) {
        WriteToDebug("Setting to 100");
        intTempPreference = 100;
    }

    if (intTempPreference > 100) {
        WriteToDebug("ERROR: invalid preference");
        TCAPI_SetErrorInfoManually(
            TCAPI_ERROR_INVALID_PREFERENCE,
            "Invalid audio preference received from LMS",
            "intTempPreference=" + intTempPreference
        );
        return null;
    }

    WriteToDebug("Returning " + intTempPreference);
    return intTempPreference;
}

function TCAPI_SetAudioPreference (PlayPreference, intPercentOfMaxVolume) {
    WriteToDebug("In TCAPI_SetAudioPreference PlayPreference=" + PlayPreference + ", intPercentOfMaxVolume=" + intPercentOfMaxVolume);

    TCAPI_ClearErrorInfo();

    if (PlayPreference == PREFERENCE_OFF) {
        WriteToDebug("Setting percent to -1 - OFF");
        intPercentOfMaxVolume = -1;
    }

    _TCAPI_SetStateSafe("cmi.student_preference.audio", intPercentOfMaxVolume);
}

function TCAPI_SetLanguagePreference (strLanguage) {
    WriteToDebug("In TCAPI_SetLanguagePreference strLanguage=" + strLanguage);
    TCAPI_ClearErrorInfo();
    _TCAPI_SetStateSafe("cmi.student_preference.language", strLanguage);
}

function TCAPI_GetLanguagePreference () {
    WriteToDebug("In TCAPI_GetLanguagePreference");
    var pref,
        getStateResult;

    TCAPI_ClearErrorInfo();
    getStateResult = tincan.getState("cmi.student_preference.language");
    if (getStateResult.state !== null) {
        pref = getStateResult.state.contents;
    }

    return pref;
}

function TCAPI_SetSpeedPreference (intPercentOfMax) {
    WriteToDebug("In TCAPI_SetSpeedPreference intPercentOfMax=" + intPercentOfMax);
    var intTCAPISpeed;  //TCAPI's scale is -100 to +100, our range is 1 to 100

    TCAPI_ClearErrorInfo();

    intTCAPISpeed = (intPercentOfMax * 2) - 100;

    WriteToDebug("intTCAPISpeed=" + intTCAPISpeed);
    _TCAPI_SetStateSafe("cmi.student_preference.speed", intTCAPISpeed);
}

function TCAPI_GetSpeedPreference () {
    WriteToDebug("In TCAPI_GetSpeedPreference");
    var intTCAPISpeed = 100,
        intPercentOfMax,
        getStateResult
    ;

    TCAPI_ClearErrorInfo();

    getStateResult = tincan.getState("cmi.student_preference.speed");
    if (getStateResult.state !== null) {
        intTCAPISpeed = getStateResult.state.contents;
    }
    WriteToDebug("intTCAPISpeed=" + intTCAPISpeed);

    if ( ! ValidInteger(intTCAPISpeed) ) {
        WriteToDebug("ERROR - invalid integer");
        TCAPI_SetErrorInfoManually(
            TCAPI_ERROR_INVALID_SPEED,
            "Invalid speed preference received from LMS - not an integer",
            "intTCAPISpeed=" + intTCAPISpeed
        );
        return null;
    }

    intTCAPISpeed = parseInt(intTCAPISpeed, 10);

    if (intTCAPISpeed < -100 || intTCAPISpeed > 100) {
        WriteToDebug("ERROR - out of range");
        TCAPI_SetErrorInfoManually(
            TCAPI_ERROR_INVALID_SPEED,
            "Invalid speed preference received from LMS - out of range",
            "intTCAPISpeed=" + intTCAPISpeed
        );
        return null;
    }

    intPercentOfMax = (intTCAPISpeed + 100) / 2;
    intPercentOfMax = parseInt(intPercentOfMax, 10);

    WriteToDebug("Returning " + intPercentOfMax);
    return intPercentOfMax;
}

function TCAPI_SetTextPreference (intPreference) {
    WriteToDebug("In TCAPI_SetTextPreference intPreference=" + intPreference);
    TCAPI_ClearErrorInfo();
    _TCAPI_SetStateSafe("cmi.student_preference.text", intPreference);
}

function TCAPI_GetTextPreference () {
    WriteToDebug("In TCAPI_GetTextPreference");
    var intTempPreference = 0,
        getStateResult;

    TCAPI_ClearErrorInfo();

    getStateResult = tincan.getState("cmi.student_preference.text");
    if (getStateResult.state !== null) {
        intTempPreference = getStateResult.state.contents;
    }

    intTempPreference = parseInt(intTempPreference, 10);
    WriteToDebug("intTempPreference=" + intTempPreference);

    if (intTempPreference > 0) {
        WriteToDebug("Returning On");
        return PREFERENCE_ON;
    }
    else if (intTempPreference == 0 || intTempPreference == "") {
        WriteToDebug("Returning Default");
        return PREFERENCE_DEFAULT;
    }
    else if (intTempPreference < 0) {
        WriteToDebug("returning Off");
        return PREFERENCE_OFF;
    }

    WriteToDebug("Error: Invalid preference");
    TCAPI_SetErrorInfoManually(
        TCAPI_ERROR_INVALID_PREFERENCE,
        "Invalid text preference received from LMS",
        "intTempPreference=" + intTempPreference
    );
    return null;
}

/*****************************
*  Time management
*****************************/

//
// returns number of milliseconds duration of all launches previous
// to this session
//
function TCAPI_GetPreviouslyAccumulatedTime () {
    WriteToDebug("In TCAPI_GetPreviouslyAccumulatedTime");
    var data = 0,
        getStateResult;

    WriteToDebug("In TCAPI_GetPreviouslyAccumulatedTime - cached: " + tcapi_cache.totalPrevDuration);
    if (tcapi_cache.totalPrevDuration === null) {
        getStateResult = tincan.getState(TCAPI_STATE_TOTAL_TIME);
        if (getStateResult.state !== null) {
            data = Number(getStateResult.state.contents);
        }
        tcapi_cache.totalPrevDuration = (data === NaN ) ? 0 : data;
    }

    return tcapi_cache.totalPrevDuration;
}

//
// SaveTime is used to track session time which we don't need
// in TC because we are creating our own total durations
//
function TCAPI_SaveTime (intMilliSeconds) {
    WriteToDebug("In TCAPI_SaveTime");
    return true;
}

function TCAPI_GetMaxTimeAllowed () {
    WriteToDebug("In TCAPI_GetMaxTimeAllowed");
    return null;
}

/*****************************
*  Scoring
*****************************/

function TCAPI_SetScore (intScore, intMaxScore, intMinScore) {
    WriteToDebug("In TCAPI_SetScore intScore=" + intScore + ", intMaxScore=" + intMaxScore + ", intMinScore=" + intMinScore);

    TCAPI_ClearErrorInfo();

    TCAPI_SCORE["raw"] = intScore;
    TCAPI_SCORE["max"] = intMaxScore;
    TCAPI_SCORE["min"] = intMinScore;

    WriteToDebug("Returning " + TCAPI_SCORE);

    TCAPI_UPDATES_PENDING = true;

    return true;
}

function TCAPI_GetScore () {
    WriteToDebug("In TCAPI_GetScore");

    TCAPI_ClearErrorInfo();

    WriteToDebug("Returning " + TCAPI_SCORE['raw']);
    return TCAPI_SCORE['raw'];
}

function TCAPI_SetPointBasedScore (intScore, intMaxScore, intMinScore) {
    WriteToDebug("TCAPI_SetPointBasedScore - TCAPI does not support SetPointBasedScore, falling back to SetScore");
    return TCAPI_SetScore(intScore, intMaxScore, intMinScore);
}

function TCAPI_GetScaledScore (intScore, intMaxScore, intMinScore) {
    WriteToDebug("TCAPI_GetScaledScore - TCAPI does not support GetScaledScore, returning false");
    return false;
}

/***********************
*    Interactions
************************/

function TCAPI_RecordInteraction (strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime, TCAPIInteractionType, strAlternateResponse, strAlternateCorrectResponse) {
    var blnTempResult,
        intInteractionIndex,
        strResult,
        actObj = {},
        stmt,
        activityName,

        //
        // All of the processing for this ID is handled in TCAPI_CreateValidIdentifier.
        // In that function, the tincan.activity.id is added to strID and then URI encoded.
        // To fix backwards compatibility, the TCAPI_DONT_USE_BROKEN_URN_IDS flag should be set to false,
        // which will cause "urn:scormdriver:" to be added between the tincan.activity.id
        // and the ID that is passed.
        //
        // We also set the activity definition name to the original ID that is passed (the interactionActivityId
        // without the tincan.activity.id on the beginning).
        //
        // All of these fixes make it so that future interactions use valid IDs without breaking any
        // backwards compatibility.
        //
        interactionActivityId = strID,
        interactionActivityType = TCAPI_INTERACTION
    ;

    TCAPI_ClearErrorInfo();

    if (strID.indexOf(tincan.activity.id + "-") == 0) {
        activityName = strID.substring(tincan.activity.id.length + 1);
    }

    if (! TCAPI_DONT_USE_BROKEN_URN_IDS) {
        interactionActivityId = tincan.activity.id + "-urn:scormdriver:" + activityName;
    }

    switch (TCAPIInteractionType) {
        case "true-false":
            actObj = {
                id: interactionActivityId,
                definition: {
                    description: {'en-US': strDescription},
                    type: interactionActivityType,
                    interactionType: "true-false",
                    name: {"und": activityName}
                }
            };
            break;
        case "choice":
            actObj = {
                id: interactionActivityId,
                definition: {
                    description: {'en-US': strDescription},
                    type: interactionActivityType,
                    interactionType: "choice",
                    name: {"und": activityName}
                }
            };
            break;
        case "fill-in":
            actObj = {
                id: interactionActivityId,
                definition: {
                    description: {'en-US': strDescription},
                    type: interactionActivityType,
                    interactionType: "fill-in",
                    name: {"und": activityName}
                }
            };
            break;
        case "matching":
            actObj = {
                id: interactionActivityId,
                definition: {
                    description: {'en-US': strDescription},
                    type: interactionActivityType,
                    interactionType: "matching",
                    name: {"und": activityName}
                    //source: [],
                    //target: []
                }
            };
            break;
        case "performance":
            actObj = {
                id: interactionActivityId,
                definition: {
                    description: {'en-US': strDescription},
                    type: interactionActivityType,
                    interactionType: "performance",
                    name: {"und": activityName}
                    //steps: []
                }
            };
            break;
        case "sequencing":
            actObj = {
                id: interactionActivityId,
                definition: {
                    description: {'en-US': strDescription},
                    type: interactionActivityType,
                    interactionType: "sequencing",
                    name: {"und": activityName}
                    //choices: []
                }
            };
            break;
        case "likert":
            actObj = {
                id: interactionActivityId,
                definition: {
                    description: {'en-US': strDescription},
                    type: interactionActivityType,
                    interactionType: "likert",
                    name: {"und": activityName}
                    //scale: []
                }
            };
            break;
        case "numeric":
            actObj = {
                id: interactionActivityId,
                definition: {
                    description: {'en-US': strDescription},
                    type: interactionActivityType,
                    interactionType: "numeric",
                    name: {"und": activityName}
                }
            };
            break;
        case "other":
            actObj = {
                id: interactionActivityId,
                definition: {
                    description: {'en-US': strDescription},
                    type: interactionActivityType,
                    interactionType: "other",
                    name: {"und": activityName}
                }
            };
            break;
        default:
            WriteToDebug("TCAPI_RecordInteraction received an invalid TCPAIInteractionType of " + TCAPIInteractionType);
            return false;
    }

    if (actObj.id !== null) {
        //
        // this makes it so that "" can never be a correct response, but that's the way
        // the other standards in Driver work, and is potentially a reasonable expectation
        //
        if (strCorrectResponse !== null && strCorrectResponse !== "") {
            actObj.definition.correctResponsesPattern = [strCorrectResponse];
        }

        stmt = {
            verb: TCAPI_VERB_ANSWERED,
            object: actObj,
            context: {
                contextActivities: {
                    parent: tincan.activity,
                    grouping: {
                        id: tincan.activity.id + '-' + strLearningObjectiveID
                    }
                }
            }
        };

        if ((strResponse !== null) || (intLatency !== null && intLatency !== "") || (intWeighting !== null && intWeighting !== "")) {
            stmt.result = {};

            if (strResponse !== null) {
                stmt.result.response = strResponse;
                if (blnCorrect === true || blnCorrect === INTERACTION_RESULT_CORRECT) {
                    stmt.result.success = true;
                }
                else if (blnCorrect === false || blnCorrect === "" || blnCorrect === "false" || blnCorrect === INTERACTION_RESULT_WRONG) {
                    stmt.result.success = false;
                }
            }
            if (intLatency !== null && intLatency !== "") {
                stmt.result.duration = TinCan.Utils.convertMillisecondsToISO8601Duration(intLatency);
            }
            if (intWeighting !== null && intWeighting !== "") {
                stmt.result.extensions = stmt.result.extensions || {};
                stmt.result.extensions["http://id.tincanapi.com/extension/cmi-interaction-weighting"] = intWeighting;
            }
        }

        tcapi_cache.statementQueue.push(stmt);
    }

    return true;
}

function TCAPI_RecordTrueFalseInteraction (strID, blnResponse, blnCorrect, blnCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime) {
    WriteToDebug(
        "In TCAPI_RecordTrueFalseInteraction strID=" + strID + ", blnResponse=" + blnResponse +
        ", blnCorrect=" + blnCorrect + ", blnCorrectResponse=" + blnCorrectResponse +
        ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
        ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime
    );

    var strResponse = null,
        strCorrectResponse = null
    ;

    if (blnResponse === true) {
        strResponse = "true";
    }
    // test for false b/c it could be null in which case we want to leave it as `null`
    else if (blnResponse === false) {
        strResponse = "false";
    }

    if (blnCorrectResponse === true) {
        strCorrectResponse = "true";
    }
    // test for false b/c it could be null in which case we want to leave it as `null`
    else if (blnCorrectResponse === false) {
        strCorrectResponse = "false";
    }

    return TCAPI_RecordInteraction(
        strID,
        strResponse,
        blnCorrect,
        strCorrectResponse,
        strDescription,
        intWeighting,
        intLatency,
        strLearningObjectiveID,
        dtmTime,
        TCAPI_INTERACTION_TYPE_TRUE_FALSE,
        strResponse,
        strCorrectResponse
    );
}

function TCAPI_RecordMultipleChoiceInteraction (strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime) {
    WriteToDebug(
        "In TCAPI_RecordMultipleChoiceInteraction strID=" + strID + ", aryResponse=" + aryResponse +
        ", blnCorrect=" + blnCorrect + ", aryCorrectResponse=" + aryCorrectResponse +
        ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
        ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime
    );

    var strResponse = null,
        strResponseLong = null,
        strCorrectResponse = "",
        strCorrectResponseLong = ""
    ;

    if (aryResponse !== null) {
        strResponse = "";
        strResponseLong = "";

        for (var i = 0; i < aryResponse.length; i++) {
            if (strResponse.length > 0) { strResponse += "[,]"; }
            if (strResponseLong.length > 0) { strResponseLong += "[,]"; }

            strResponse += aryResponse[i].Short;
            strResponseLong += aryResponse[i].Long;
        }
    }

    for (var i = 0; i < aryCorrectResponse.length; i++) {
        if (strCorrectResponse.length > 0) { strCorrectResponse += "[,]"; }
        if (strCorrectResponseLong.length > 0) { strCorrectResponseLong += "[,]"; }

        strCorrectResponse += aryCorrectResponse[i].Short;
        strCorrectResponseLong += aryCorrectResponse[i].Long;
    }

    return TCAPI_RecordInteraction(
        strID,
        strResponseLong,
        blnCorrect,
        strCorrectResponseLong,
        strDescription,
        intWeighting,
        intLatency,
        strLearningObjectiveID,
        dtmTime,
        TCAPI_INTERACTION_TYPE_CHOICE,
        strResponse,
        strCorrectResponse
    );
}

function TCAPI_RecordFillInInteraction (strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime) {
    WriteToDebug(
        "In TCAPI_RecordFillInInteraction strID=" + strID + ", strResponse=" + strResponse +
        ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
        ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
        ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime
    );

    if (strResponse !== null) {
        strResponse = new String(strResponse);

        if (strResponse.length > 255) {
            strResponse = strResponse.substr(0, 255);
        }
    }

    if (strCorrectResponse !== null) {
        strCorrectResponse = new String(strCorrectResponse);

        if (strCorrectResponse.length > 255) {
            strCorrectResponse = strCorrectResponse.substr(0, 255);
        }
    }

    return TCAPI_RecordInteraction(
        strID,
        strResponse,
        blnCorrect,
        strCorrectResponse,
        strDescription,
        intWeighting,
        intLatency,
        strLearningObjectiveID,
        dtmTime,
        TCAPI_INTERACTION_TYPE_FILL_IN,
        strResponse,
        strCorrectResponse
    );
}

function TCAPI_RecordMatchingInteraction (strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime) {
    WriteToDebug(
        "In TCAPI_RecordMatchingInteraction strID=" + strID + ", aryResponse=" + aryResponse +
        ", blnCorrect=" + blnCorrect + ", aryCorrectResponse=" + aryCorrectResponse +
        ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
        ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime
    );

    var strResponse = null,
        strResponseLong = null,
        strCorrectResponse = "",
        strCorrectResponseLong = ""
    ;

    if (aryResponse !== null) {
        strResponse = "";
        strResponseLong = "";

        for (var i = 0; i < aryResponse.length; i++) {
            if (strResponse.length > 0) { strResponse += "[,]"; }
            if (strResponseLong.length > 0) { strResponseLong += "[,]"; }

            strResponse += aryResponse[i].Source.Short + "[.]" + aryResponse[i].Target.Short;
            strResponseLong += aryResponse[i].Source.Long + "[.]" + aryResponse[i].Target.Long;
        }
    }

    for (var i = 0; i < aryCorrectResponse.length; i++) {
        if (strCorrectResponse.length > 0) { strCorrectResponse += "[,]"; }
        if (strCorrectResponseLong.length > 0) { strCorrectResponseLong += "[,]"; }

        strCorrectResponse += aryCorrectResponse[i].Source.Short + "[.]" + aryCorrectResponse[i].Target.Short;
        strCorrectResponseLong += aryCorrectResponse[i].Source.Long + "[.]" + aryCorrectResponse[i].Target.Long;
    }

    return TCAPI_RecordInteraction(
        strID,
        strResponseLong,
        blnCorrect,
        strCorrectResponseLong,
        strDescription,
        intWeighting,
        intLatency,
        strLearningObjectiveID,
        dtmTime,
        TCAPI_INTERACTION_TYPE_MATCHING,
        strResponse,
        strCorrectResponse
    );
}

function TCAPI_RecordPerformanceInteraction (strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime) {
    WriteToDebug(
        "In TCAPI_RecordPerformanceInteraction strID=" + strID + ", strResponse=" + strResponse +
        ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
        ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
        ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime
    );

    if (strResponse !== null) {
        strResponse = new String(strResponse);
        if (strResponse.length > 255) {
            strResponse = strResponse.substr(0, 255);
        }
    }

    if (strCorrectResponse !== null) {
        strCorrectResponse = new String(strCorrectResponse);
        if (strCorrectResponse.length > 255) {
            strCorrectResponse = strCorrectResponse.substr(0, 255);
        }
    }

    return TCAPI_RecordInteraction(
        strID,
        strResponse,
        blnCorrect,
        strCorrectResponse,
        strDescription,
        intWeighting,
        intLatency,
        strLearningObjectiveID,
        dtmTime,
        TCAPI_INTERACTION_TYPE_PERFORMANCE,
        strResponse,
        strCorrectResponse
    );
}

function TCAPI_RecordSequencingInteraction (strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime) {
    WriteToDebug(
        "In TCAPI_RecordSequencingInteraction strID=" + strID + ", aryResponse=" + aryResponse +
        ", blnCorrect=" + blnCorrect + ", aryCorrectResponse=" + aryCorrectResponse +
        ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
        ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime
    );

    var strResponse = null,
        strResponseLong = null,
        strCorrectResponse = "",
        strCorrectResponseLong = ""
    ;

    if (aryResponse !== null) {
        strResponse = "";
        strResponseLong = "";

        for (var i = 0; i < aryResponse.length; i++) {
            if (strResponse.length > 0) { strResponse += "[,]"; }
            if (strResponseLong.length > 0) { strResponseLong += "[,]"; }

            strResponse += aryResponse[i].Short;
            strResponseLong += aryResponse[i].Long;
        }
    }

    for (var i = 0; i < aryCorrectResponse.length; i++) {
        if (strCorrectResponse.length > 0) { strCorrectResponse += "[,]"; }
        if (strCorrectResponseLong.length > 0) { strCorrectResponseLong += "[,]"; }

        strCorrectResponse += aryCorrectResponse[i].Short;
        strCorrectResponseLong += aryCorrectResponse[i].Long;
    }

    return TCAPI_RecordInteraction(
        strID,
        strResponseLong,
        blnCorrect,
        strCorrectResponseLong,
        strDescription,
        intWeighting,
        intLatency,
        strLearningObjectiveID,
        dtmTime,
        TCAPI_INTERACTION_TYPE_SEQUENCING,
        strResponse,
        strCorrectResponse
    );
}

function TCAPI_RecordLikertInteraction (strID, response, blnCorrect, correctResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime) {
    WriteToDebug(
        "In TCAPI_RecordLikertInteraction strID=" + strID + ", response=" + response +
        ", blnCorrect=" + blnCorrect + ", correctResponse=" + correctResponse +
        ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
        ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime
    );

    var strResponse = null,
        strResponseLong = null,
        strCorrectResponse = null,
        strCorrectResponseLong = null
    ;

    if (response !== null) {
        strResponse = response.Short;
        strResponseLong = response.Long;
    }
    if (correctResponse !== null) {
        strCorrectResponse = correctResponse.Short;
        strCorrectResponseLong = correctResponse.Long;
    }

    return TCAPI_RecordInteraction(
        strID,
        strResponseLong,
        blnCorrect,
        strCorrectResponseLong,
        strDescription,
        intWeighting,
        intLatency,
        strLearningObjectiveID,
        dtmTime,
        TCAPI_INTERACTION_TYPE_LIKERT,
        strResponse,
        strCorrectResponse
    );
}

function TCAPI_RecordNumericInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime) {
    WriteToDebug(
        "In TCAPI_RecordNumericInteraction strID=" + strID + ", strResponse=" + strResponse +
        ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
        ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
        ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime
    );

    if (typeof strCorrectResponse !== "undefined" && strCorrectResponse !== null) {
        // Tin Can can handle numeric ranges or decimals. As long as we received one of those we are ok to continue
        if (! IsValidDecimalRange(strCorrectResponse) && ! IsValidDecimal(strCorrectResponse)) {
            WriteToDebug("Returning False -  TCAPI_RecordNumericInteraction received invalid correct response (not a decimal or range), strCorrectResponse=" + strCorrectResponse);
            return false;
        }
    }

    return TCAPI_RecordInteraction(
        strID,
        strResponse,
        blnCorrect,
        strCorrectResponse,
        strDescription,
        intWeighting,
        intLatency,
        strLearningObjectiveID,
        dtmTime,
        TCAPI_INTERACTION_TYPE_NUMERIC,
        strResponse,
        strCorrectResponse
    );
}

function TCAPI_GetEntryMode () {
    WriteToDebug("In TCAPI_GetEntryMode");
    /*
    var strEntry;

    TCAPI_ClearErrorInfo();

    strEntry = TCAPI_CallLMSGetValue("cmi.core.entry");

    WriteToDebug("strEntry=" + strEntry);

    if (strEntry == TCAPI_ENTRY_ABINITIO) {
        WriteToDebug("Returning first time");
        return ENTRY_FIRST_TIME;
    }
    else if (strEntry == TCAPI_ENTRY_RESUME) {
        WriteToDebug("Returning resume");
        return ENTRY_RESUME;
    }
    else if (strEntry == TCAPI_ENTRY_NORMAL) {
        WriteToDebug("returning normal");
        return ENTRY_REVIEW;
    }
    else{
        WriteToDebug("ERROR - invalide entry mode");
        TCAPI_SetErrorInfoManually(TCAPI_ERROR_INVALID_ENTRY,
                        "Invalid entry vocab received from LMS",
                        "strEntry=" + strEntry);
        return null;
    }
    */
    return null;
}

function TCAPI_GetLessonMode () {
    WriteToDebug("In TCAPI_GetLessonMode");
    /*
    var strLessonMode;

    TCAPI_ClearErrorInfo();

    strLessonMode = TCAPI_CallLMSGetValue("cmi.core.lesson_mode");

    WriteToDebug("strLessonMode=" + strLessonMode);

    if (strLessonMode == TCAPI_BROWSE) {
        WriteToDebug("Returning browse");
        return MODE_BROWSE;
    }
    else if(strLessonMode == TCAPI_NORMAL) {
        WriteToDebug("returning normal");
        return MODE_NORMAL;
    }
    else if(strLessonMode == TCAPI_REVIEW) {
        WriteToDebug("Returning Review");
        return MODE_REVIEW;
    }
    else{
        WriteToDebug("ERROR - invalid lesson mode");
        TCAPI_SetErrorInfoManually(TCAPI_ERROR_INVALID_LESSON_MODE,
                                "Invalid lesson_mode vocab received from LMS",
                                "strLessonMode=" + strLessonMode);
        return null;
    }
    */
    return null;
}

function TCAPI_GetTakingForCredit () {
    WriteToDebug("In TCAPI_GetTakingForCredit");
    /*
    var strCredit;

    TCAPI_ClearErrorInfo();

    strCredit = TCAPI_CallLMSGetValue("cmi.core.credit");

    WriteToDebug("strCredit=" + strCredit);

    if (strCredit == "credit") {
        WriteToDebug("Returning true");
        return true;
    }
    else if (strCredit == "no-credit") {
        WriteToDebug("Returning false");
        return false;
    }
    else{
        WriteToDebug("ERROR - invalid credit");
        TCAPI_SetErrorInfoManually(TCAPI_ERROR_INVALID_CREDIT,
                                  "Invalid credit vocab received from LMS",
                                  "strCredit=" + strCredit);
        return null;
    }
    */
    return null;
}

/*****************************
* Objectives
*****************************/

function TCAPI_SetObjectiveScore (strObjectiveID, intScore, intMaxScore, intMinScore) {
    WriteToDebug("In TCAPI_SetObjectiveScore, strObejctiveID=" + strObjectiveID + ", intScore=" + intScore + ", intMaxScore=" + intMaxScore + ", intMinScore=" + intMinScore);

    return false;
}

function TCAPI_SetObjectiveDescription (strObjectiveID, strObjectiveDescription) {
    WriteToDebug("In TCAPI_SetObjectiveDescription, strObjectiveDescription=" + strObjectiveDescription);

    return false;
}

function TCAPI_SetObjectiveStatus (strObjectiveID, Lesson_Status) {
    WriteToDebug("In TCAPI_SetObjectiveStatus strObjectiveID=" + strObjectiveID + ", Lesson_Status=" + Lesson_Status);

    return false;
}

function TCAPI_GetObjectiveScore (strObjectiveID) {
    WriteToDebug("In TCAPI_GetObjectiveScore, strObejctiveID=" + strObjectiveID);

    return false;
}

function TCAPI_GetObjectiveDescription (strObjectiveID) {
    WriteToDebug("In TCAPI_GetObjectiveDescription, strObejctiveID=" + strObjectiveID);

    return false;
}

function TCAPI_GetObjectiveStatus (strObjectiveID) {
    WriteToDebug("In TCAPI_GetObjectiveStatus, strObejctiveID=" + strObjectiveID);

    return false;
}

/*****************************
*  Status Management
*****************************/

function TCAPI_SetSuspended () {
    WriteToDebug("In TCAPI_SetSuspended");

    if (TCAPI_IN_PROGRESS) {
        TCAPI_IN_PROGRESS = false;
        TCAPI_UPDATES_PENDING = true;
    }

    return true;
}

function TCAPI_SetFailed () {
    WriteToDebug("In TCAPI_SetFailed");

    TCAPI_STATUS = TCAPI_VERB_FAILED;
    TCAPI_STATUS_CHANGED = true;
    TCAPI_SATISFACTION_STATUS = false;
    TCAPI_IN_PROGRESS = false;
    TCAPI_UPDATES_PENDING = true;

    return true;
}

function TCAPI_SetPassed () {
    WriteToDebug("In TCAPI_SetPassed");

    TCAPI_STATUS = TCAPI_VERB_PASSED;
    TCAPI_STATUS_CHANGED = true;
    TCAPI_SATISFACTION_STATUS = true;
    TCAPI_IN_PROGRESS = false;
    TCAPI_UPDATES_PENDING = true;

    return true;
}

function TCAPI_SetCompleted () {
    WriteToDebug("In TCAPI_SetCompleted");

    TCAPI_ClearErrorInfo();

    if (TCAPI_STATUS === TCAPI_INIT_VERB) {
        // only set verb to completed if not already passed or failed
        TCAPI_STATUS = TCAPI_VERB_COMPLETED;
        TCAPI_STATUS_CHANGED = true;
    }
    TCAPI_COMPLETION_STATUS = TCAPI_VERB_COMPLETED;
    TCAPI_IN_PROGRESS = false;
    TCAPI_UPDATES_PENDING = true;

    return true;
}

function TCAPI_ResetStatus () {
    WriteToDebug("In TCAPI_ResetStatus");

    TCAPI_ClearErrorInfo();

    TCAPI_STATUS = TCAPI_INIT_VERB;
    TCAPI_STATUS_CHANGED = true;
    TCAPI_COMPLETION_STATUS = '';
    TCAPI_SATISFACTION_STATUS = null;
    TCAPI_IN_PROGRESS = true;
    TCAPI_UPDATES_PENDING = true;

    return true;
}

function TCAPI_GetStatus () {
    WriteToDebug("In TCAPI_GetStatus");
    var strStatus = "";

    TCAPI_ClearErrorInfo();

    if (TCAPI_STATUS === TCAPI_VERB_COMPLETED) {
        strStatus = "completed";
    }
    else if (TCAPI_STATUS === TCAPI_VERB_ATTEMPTED) {
        strStatus = "attempted";
    }
    else if (TCAPI_STATUS === TCAPI_VERB_PASSED) {
        strStatus = "passed";
    }
    else if (TCAPI_STATUS === TCAPI_VERB_FAILED) {
        strStatus = "failed";
    }
    else {
        strStatus = TCAPI_STATUS;
    }

    WriteToDebug("In TCAPI_GetStatus - strStatus=" + strStatus);
    return strStatus;
}

function TCAPI_CreateValidIdentifier(str) {
    return tincan.activity.id + "-" + encodeURIComponent(str);
}

/*****************************
*  Unsupported Methods
*
* TODO: should these be supported through "state"?
*****************************/

function TCAPI_SetNavigationRequest (strNavRequest) {
    WriteToDebug("TCAPI_GetNavigationRequest - TCAPI does not support navigation requests, returning false");
    return false;
}

function TCAPI_GetNavigationRequest () {
    WriteToDebug("TCAPI_GetNavigationRequest - TCAPI does not support navigation requests, returning false");
    return false;
}

function TCAPI_CreateDataBucket (strBucketId, intMinSize, intMaxSize) {
    WriteToDebug("TCAPI_CreateDataBucket - TCAPI does not support SSP, returning false");
    return false;
}

function TCAPI_GetDataFromBucket (strBucketId) {
    WriteToDebug("TCAPI_GetDataFromBucket - TCAPI does not support SSP, returning empty string");
    return "";
}

function TCAPI_PutDataInBucket (strBucketId, strData, blnAppendToEnd) {
    WriteToDebug("TCAPI_PutDataInBucket - TCAPI does not support SSP, returning false");
    return false;
}

function TCAPI_DetectSSPSupport () {
    WriteToDebug("TCAPI_DetectSSPSupport - TCAPI does not support SSP, returning false");
    return false;
}

function TCAPI_GetBucketInfo (strBucketId) {
    WriteToDebug("TCAPI_GetBucketInfo - TCAPI does not support SSP, returning empty SSPBucketSize");
    return new SSPBucketSize(0, 0);
}

function TCAPI_WriteComment (strComment) {
    WriteToDebug("In TCAPI_WriteComment - TCAPI does not support comments");
    return false;
}

function TCAPI_GetComments () {
    WriteToDebug("In TCAPI_GetComments - TCAPI does not support comments");
    return "";
}

function TCAPI_GetLMSComments () {
    WriteToDebug("In TCAPI_GetLMSComments - TCAPI does not support LMS comments");
   return "";
}

function TCAPI_GetLaunchData () {
    WriteToDebug("In TCAPI_GetLaunchData - TCAPI does not support launch data");
    return false;
}

function TCAPI_DisplayMessageOnTimeout () {
    TCAPI_ClearErrorInfo();

    WriteToDebug("In TCAPI_DisplayMessageOnTimeout - TCAPI does not support MessageOnTimeout");
    return false;
}

function TCAPI_ExitOnTimeout () {
    WriteToDebug("In TCAPI_ExitOnTimeout - TCAPI does not support ExitOnTimeout");
    return false;
}

function TCAPI_GetPassingScore () {
    WriteToDebug("In TCAPI_GetPassingScore - TCAPI does not support GetPassingScore");
    return false;
}

function TCAPI_GetProgressMeasure () {
    WriteToDebug("TCAPI_GetProgressMeasure - TCAPI does not support progress_measure, returning false");
    return false;
}

function TCAPI_SetProgressMeasure () {
    WriteToDebug("TCAPI_SetProgressMeasure - TCAPI does not support progress_measure, returning false");
    return false;
}

function TCAPI_GetObjectiveProgressMeasure () {
    WriteToDebug("TCAPI_GetObjectiveProgressMeasure - TCAPI does not support progress_measure, returning false");
    return false;
}

function TCAPI_SetObjectiveProgressMeasure () {
    WriteToDebug("TCAPI_SetObjectiveProgressMeasure - TCAPI does not support progress_measure, returning false");
    return false;
}

function TCAPI_GetInteractionType(strInteractionID) {
    WriteToDebug("TCAPI_GetInteractionType - TCAPI does not support interaction retrieval, returning empty string");
    return '';
}

function TCAPI_GetInteractionTimestamp(strInteractionID) {
    WriteToDebug("TCAPI_GetInteractionTimestamp - TCAPI does not support interaction retrieval, returning empty string");
    return '';
}

function TCAPI_GetInteractionCorrectResponses(strInteractionID) {
    WriteToDebug("TCAPI_GetInteractionCorrectResponses - TCAPI does not support interaction retrieval, returning empty array");
    return [];
}

function TCAPI_GetInteractionWeighting(strInteractionID) {
    WriteToDebug("TCAPI_GetInteractionWeighting - TCAPI does not support interaction retrieval, returning empty string");
    return '';
}

function TCAPI_GetInteractionLearnerResponses(strInteractionID) {
    WriteToDebug("TCAPI_GetInteractionLearnerResponses - TCAPI does not support interaction retrieval, returning empty array");
    return [];
}

function TCAPI_GetInteractionResult(strInteractionID) {
    WriteToDebug("TCAPI_GetInteractionResult - TCAPI does not support interaction retrieval, returning empty string");
    return '';
}

function TCAPI_GetInteractionLatency(strInteractionID) {
    WriteToDebug("TCAPI_GetInteractionDescription - TCAPI does not support interaction retrieval, returning empty string");
    return '';
}

function TCAPI_GetInteractionDescription(strInteractionID) {
    WriteToDebug("TCAPI_GetInteractionDescription - TCAPI does not support interaction retrieval, returning empty string");
    return '';
}

/*****************************
*  Error Management
*****************************/

// TODO: global variables used that need to be declared
function TCAPI_ClearErrorInfo () {
    WriteToDebug("In TCAPI_ClearErrorInfo");

    intTCAPIError = TCAPI_NO_ERROR;
    strTCAPIErrorString = "";
    strTCAPIErrorDiagnostic = "";
}

function TCAPI_SetErrorInfoManually (intNum, strString, strDiagnostic) {
    WriteToDebug("In TCAPI_SetErrorInfoManually");
    WriteToDebug("ERROR-Num=" + intNum);
    WriteToDebug("      String=" + strString);
    WriteToDebug("      Diag=" + strDiagnostic);

    intTCAPIError = intNum;
    strTCAPIErrorString = strString;
    strTCAPIErrorDiagnostic = strDiagnostic;
}

function TCAPI_GetLastError () {
    WriteToDebug("In TCAPI_GetLastError");

    if (intTCAPIError === TCAPI_NO_ERROR) {
        WriteToDebug("Returning No Error");
        return NO_ERROR;
    }
    else {
        WriteToDebug("Returning " + intTCAPIError);
        return intTCAPIError;
    }
}

function TCAPI_GetLastErrorDesc () {
    WriteToDebug("In TCAPI_GetLastErrorDesc, " + strTCAPIErrorString + "\n" + strTCAPIErrorDiagnostic);
    return strTCAPIErrorString + "\n" + strTCAPIErrorDiagnostic;
}
/* declare other globals we'll use */

var CMI5_PENDING_STATUS = {
        completion: null,
        success: null,
        score: null
    },
    CMI5_COMMITTED_STATUS = {
        completion: null,
        success: null,
        score: null,
        launchModes: []
    },
    CMI5_STATEMENT_QUEUE = [],

    //
    // cmi5.js will calculate session duration automatically, but SD has the ability
    // to let the user manually set the duration of learning so we use it to override
    // the automatically handled duration by cmi5.js
    //
    CMI5_SESSION_DURATION = null,

    //
    // cmi5 doesn't have the concept of total duration across sessions so we manufacture
    // it using the State API
    //
    CMI5_TOTAL_PREV_DURATION = null,

    CMI5_ENTRY_MODE = null,
    CMI5_INTERACTIONS = {},
    CMI5_SSP_BUCKETS = {},

    CMI5_VERB_ID_FAILED = "http://adlnet.gov/expapi/verbs/failed",
    CMI5_VERB_ID_PASSED = "http://adlnet.gov/expapi/verbs/passed",
    CMI5_VERB_ID_COMPLETED = "http://adlnet.gov/expapi/verbs/completed",

    //
    // local cache of these values so only need to round trip once, they are updated
    // when calling set so no need to re-fetch
    //
    CMI5_PREF_AUDIO_PLAY = null,
    CMI5_PREF_AUDIO_VOLUME = null,
    CMI5_PREF_LANGUAGE = null,
    CMI5_PREF_SPEED = null,
    CMI5_PREF_TEXT = null,

    CMI5_INTERACTION_ACTIVITY_TYPE = "http://adlnet.gov/expapi/activities/cmi.interaction",
    CMI5_INTERACTION_TYPE_TRUE_FALSE = "true-false",
    CMI5_INTERACTION_TYPE_CHOICE = "choice",
    CMI5_INTERACTION_TYPE_FILL_IN = "fill-in",
    CMI5_INTERACTION_TYPE_LONG_FILL_IN = "long-fill-in",
    CMI5_INTERACTION_TYPE_MATCHING = "matching",
    CMI5_INTERACTION_TYPE_PERFORMANCE = "performance",
    CMI5_INTERACTION_TYPE_SEQUENCING = "sequencing",
    CMI5_INTERACTION_TYPE_LIKERT = "likert",
    CMI5_INTERACTION_TYPE_NUMERIC = "numeric",

    //
    // these match their counterparts in the TCAPI_Functions where possible
    //
    CMI5_STATE_BOOKMARK = "bookmark",
    CMI5_STATE_GENERIC_DATA = "generic_data",
    CMI5_STATE_TOTAL_TIME = "cumulative_time",
    CMI5_STATE_COMMITTED_STATUS = "status",
    CMI5_STATE_COMMENTS = "learner_comments",
    CMI5_STATE_SSP_BUCKET_PREFIX = "sspBucket",
    CMI5_STATE_LANGUAGE_PREFERENCE = "cmi.student_preference.language",
    CMI5_STATE_VOLUME_PREFERENCE = "cmi.student_preference.audio",
    CMI5_STATE_SPEED_PREFERENCE = "cmi.student_preference.speed",
    CMI5_STATE_TEXT_PREFERENCE = "cmi.student_preference.text",

    // reserve the 2xx-5xx space for HTTP return codes from cmi5.js
    CMI5_NO_ERROR = "",

    // this is a non-HTTP status code error from cmi5.js
    CMI5_DEP_ERROR = 999,

    CMI5_ERROR_NOT_IMPLEMENTED = 998,
    CMI5_ERROR_INVALID_SCORE = 1,
    CMI5_ERROR_UNRECOGNIZED_SSP_BUCKET = 2,
    CMI5_ERROR_UNRECOGNIZED_INTERACTION = 3,
    CMI5_ERROR_NON_NORMAL_MODE = 4,
    CMI5_ERROR_ALREADY_COMMITTED = 5,
    CMI5_ERROR_INVALID_STATUS_BASED_ON_SCORE = 6,
    CMI5_ERROR_SSP_BUCKET_LOAD_FAILED = 7,
    CMI5_ERROR_SSP_BUCKET_ALREADY_EXISTS = 8,
    CMI5_ERROR_SSP_BUCKET_INVALID_JSON = 9,
    CMI5_ERROR_INTERACTION_INVALID_JSON = 10,
    CMI5_ERROR_INTERACTION_LOAD_FAILED = 11,

    intCMI5Error = CMI5_NO_ERROR,
    strCMI5ErrorString = "",
    strCMI5ErrorDiagnostic = "",

    /* Declare global used for interacting via cmi5 */
    cmi5;

function CMI5_SaveState (key, value, contentType, includeRegistration) {
    WriteToDebug("In CMI5_SaveState - key: " + key + ", value: " + value);
    var saveStateCfg = {
            agent: cmi5.getActor(),
            activity: cmi5.getActivity(),
            contentType: contentType
        },
        result,
        errCode,
        errMesg,
        errDiag;

    if (typeof includeRegistration === "undefined" || includeRegistration === null || includeRegistration) {
        saveStateCfg.registration = cmi5.getRegistration();
    }

    result = cmi5.getLRS().saveState(key, value, saveStateCfg);
    if (result.err !== null) {
        WriteToDebug("Failed to save " + key + " in state: " + result.err);
        errMesg = "Failed to save state: " + key;

        //
        // in the case of errors result.err either contains an Error object (theoretically)
        // or the HTTP status code if the request actually made it to the LRS, to distinguish
        // we can check it for all digits (which is safer than checking for an Error object,
        // at least in this version of TinCanJS), which then allows us to look at the response
        // text which is generally a more helpful message than the status code
        //
        if (/^\d+$/.test(result.err)) {
            errCode = result.err;

            if (result.err === 0) {
                errDiag = "Aborted, offline, or invalid CORS endpoint";
            }
            else {
                errDiag = result.xhr.responseText;
            }
        }
        else {
            errCode = CMI5_DEP_ERROR;
            errDiag = result.err;
        }

        CMI5_SetErrorInfoManually(errCode, errMesg, errDiag);
        return false;
    }

    return true;
}

function CMI5_RetrieveState (key, includeRegistration) {
    WriteToDebug("In CMI5_RetrieveState - key: " + key);
    var retrieveStateCfg = {
            agent: cmi5.getActor(),
            activity: cmi5.getActivity()
        },
        result,

        //
        // all of the state values ultimately have to be strings because
        // the SCORM values are strings, so even on an error we are supposed
        // to return an empty string
        //
        value = "",
        errCode,
        errMesg,
        errDiag;

    if (typeof includeRegistration === "undefined" || includeRegistration === null || includeRegistration) {
        retrieveStateCfg.registration = cmi5.getRegistration();
    }

    result = cmi5.getLRS().retrieveState(key, retrieveStateCfg);
    if (result.err !== null) {
        WriteToDebug("Failed to retrieve " + key + " from state: " + result.err);
        errMesg = "Failed to retrieve state: " + key;

        //
        // in the case of errors result.err either contains an Error object (theoretically)
        // or the HTTP status code if the request actually made it to the LRS, to distinguish
        // we can check it for all digits (which is safer than checking for an Error object,
        // at least in this version of TinCanJS), which then allows us to look at the response
        // text which is generally a more helpful message than the status code
        //
        if (/^\d+$/.test(result.err)) {
            errCode = result.err;

            if (result.err === 0) {
                errDiag = "Aborted, offline, or invalid CORS endpoint";
            }
            else {
                errDiag = result.xhr.responseText;
            }
        }
        else {
            errCode = CMI5_DEP_ERROR;
            errDiag = result.err;
        }

        CMI5_SetErrorInfoManually(errCode, errMesg, errDiag);
        return "";
    }

    if (result.state !== null) {
        value = result.state.contents;
    }

    return value;
}

//
// this is just here to make it possible to mock the launch URL
// for testing purposes
//
function CMI5_GetLaunchUrl () {
    WriteToDebug("In CMI5_GetLaunchUrl");
    return document.location.href;
}

function CMI5_Initialize () {
    WriteToDebug("In CMI5_Initialize");
    var launchUrl;

    Cmi5.prototype.log = function () {
        var mesg = "cmi5.js:",
            i;

        for (i = 0; i < arguments.length; i += 1) {
            mesg += " ";
            mesg += typeof arguments[i] === "string" ? arguments[i] : JSON.stringify(arguments[i]);
        }

        WriteToDebug(mesg);
    };

    TinCan.LRS.prototype.log = function (msg, src) {
        src = src || this.LOG_SRC || "LRS";
        WriteToDebug("TinCan." + src + ": " + msg);
    };

    try {
        launchUrl = CMI5_GetLaunchUrl();

        WriteToDebug("CMI5_Initialize - instantiating cmi5 object from launch URL = " + launchUrl);
        cmi5 = new Cmi5(launchUrl);
        cmi5.includeSourceActivity(false);
    }
    catch (ex) {
        InitializeExecuted(false, "Failed to initialize cmi5 object (caught exception): " + ex);
        return;
    }

    WriteToDebug("CMI5_Initialize - calling start on cmi5 instance");
    cmi5.start(
        function (err) {
            var committedState,
                result;

            if (err !== null) {
                WriteToDebug("CMI5_Initialize - cmi5 start failed: " + err);
                InitializeExecuted(false, "Failed to initialize: " + err.message);
                return;
            }
            WriteToDebug("CMI5_Initialize - cmi5 start succeeded");

            committedState = CMI5_RetrieveState(CMI5_STATE_COMMITTED_STATUS);

            //
            // retrieve state doesn't provide a return value for detecting errors
            // so check to see if we have manually set errors (cause we shouldn't
            // unless the retrieve failed)
            //
            if (intCMI5Error) {
                WriteToDebug("CMI5_Initialize - failed to retrieve state for committed status: " + strCMI5ErrorString);
                InitializeExecuted(false, "Failed to initialize: " + strCMI5ErrorString);
                return;
            }

            if (committedState !== "") {
                //
                // check committedState object for launchModes property because retrieveState
                // in TinCanJS tries to parse the JSON, and on failure eats the parse failure
                // and leaves the result contents as a string, but we always expect it to be
                // an object, and we know we shouldn't have saved bad JSON anyways
                //
                if (typeof committedState.launchModes === "undefined") {
                    WriteToDebug("CMI5_Initialize - failed to retrieve state for committed status: likely invalid JSON parse failure");
                    InitializeExecuted(false, "Failed to initialize: likely invalid JSON parse failure");
                    return;
                }

                CMI5_COMMITTED_STATUS = committedState;
            }

            if (CMI5_COMMITTED_STATUS.completion === true) {
                CMI5_ENTRY_MODE = ENTRY_REVIEW;
            }
            else if (CMI5_COMMITTED_STATUS.launchModes.length > 0) {
                CMI5_ENTRY_MODE = ENTRY_RESUME;
            }
            else {
                CMI5_ENTRY_MODE = ENTRY_FIRST_TIME;
            }

            CMI5_COMMITTED_STATUS.launchModes.push(cmi5.getLaunchMode());

            result = CMI5_SaveState(CMI5_STATE_COMMITTED_STATUS, CMI5_COMMITTED_STATUS, "application/json");
            if (! result) {
                WriteToDebug("CMI5_Initialize - failed to store committed status");
                InitializeExecuted(false, "Failed to initialize - failed to store committed status");
                return;
            }

            WriteToDebug("CMI5_Initialize - succeeded");
            InitializeExecuted(true, "");

            //
            // go ahead and try to cache this now, but don't have initialization
            // pending while trying to retrieve the state since it is only used
            // when Finish'ing which shouldn't happen immediately
            //
            window.setTimeout(
                function () {
                    CMI5_GetPreviouslyAccumulatedTime();
                },
                200
            );
        },
        {
            postFetch: function (err, response, parsed) {
                var rootActivity;

                if (err !== null) {
                    return;
                }

                //
                // now that we have a credential into the LRS see if the LRS has more info
                // on our Activity for statement capture, leaving this async, which could
                // result in a return *after* the initialized statement is recorded which
                // isn't really a problem, and experience indicates it usually doesn't happen
                //
                rootActivity = cmi5.getActivity();
                cmi5.getLRS().retrieveActivity(
                    rootActivity.id,
                    {
                        callback: function (err, result) {
                            if (err === null) {
                                cmi5.setActivity(result);
                            }
                        }
                    }
                );
            }
        }
    );

    return true;
}

function CMI5_CommitData () {
    WriteToDebug("In CMI5_CommitData");
    var i,
        st,
        auActivityId,
        errMesg,
        errCode,
        errDiag,
        saveCommittedStatus = false,
        sourceActivity = new TinCan.Activity(
            {
                id: "http://id.tincanapi.com/activity/software/scormdriver/" + VERSION,
                definition: {
                    name: {
                        und: "ScormDriver (" + VERSION + ")"
                    },
                    description: {
                        en: "ScormDriver (" + VERSION + ")"
                    },
                    type: "http://id.tincanapi.com/activitytype/source"
                }
            }
        );

    CMI5_ClearErrorInfo();

    //
    // commit the status updates first because the statement queue may have
    // a termination statement in it that ends the session preventing us from
    // making further calls
    //
    if (CMI5_PENDING_STATUS.completion !== null) {
        saveCommittedStatus = true;

        CMI5_COMMITTED_STATUS.completion = CMI5_PENDING_STATUS.completion;
    }
    if (CMI5_PENDING_STATUS.success !== null) {
        saveCommittedStatus = true;

        CMI5_COMMITTED_STATUS.success = CMI5_PENDING_STATUS.success;
        if (CMI5_PENDING_STATUS.score !== null) {
            CMI5_COMMITTED_STATUS.score = CMI5_PENDING_STATUS.score;
        }
    }

    if (saveCommittedStatus) {
        result = CMI5_SaveState(CMI5_STATE_COMMITTED_STATUS, CMI5_COMMITTED_STATUS, "application/json");
        if (! result) {
            WriteToDebug("CMI5_CommitData - failed to commit status");
            return result;
        }
    }

    if (CMI5_STATEMENT_QUEUE.length > 0) {
        if (CMI5_PENDING_STATUS.score !== null && CMI5_PENDING_STATUS.success !== null) {
            //
            // need to loop statements looking for passed/failed with this AU id
            // and set the score into the statement - this is what allows us to
            // allow the user to call SetScore multiple times
            //
            auActivityId = cmi5.getActivity().id;

            for (i = 0; i < CMI5_STATEMENT_QUEUE.length; i += 1) {
                st = CMI5_STATEMENT_QUEUE[i];

                if (st.target.id === auActivityId && (st.verb.id === CMI5_VERB_ID_FAILED || st.verb.id === CMI5_VERB_ID_PASSED)) {
                    st.result = st.result || new TinCan.Result();
                    st.result.score = new TinCan.Score(CMI5_PENDING_STATUS.score);
                }
            }
        }

        for (i = 0; i < CMI5_STATEMENT_QUEUE.length; i += 1) {
            st = CMI5_STATEMENT_QUEUE[i];

            st.context.contextActivities.grouping = st.context.contextActivities.grouping || [];
            st.context.contextActivities.grouping.push(sourceActivity);
        }

        result = cmi5.getLRS().saveStatements(CMI5_STATEMENT_QUEUE);
        if (result.err !== null) {
            errMesg = "Failed to commit data: statements";

            //
            // in the case of errors result.err either contains an Error object (theoretically)
            // or the HTTP status code if the request actually made it to the LRS, to distinguish
            // we can check it for all digits (which is safer than checking for an Error object,
            // at least in this version of TinCanJS), which then allows us to look at the response
            // text which is generally a more helpful message than the status code
            //
            if (/^\d+$/.test(result.err)) {
                errCode = result.err;

                if (result.err === 0) {
                    errDiag = "Aborted, offline, or invalid CORS endpoint";
                }
                else {
                    errDiag = result.xhr.responseText;
                }
            }
            else {
                errCode = CMI5_DEP_ERROR;
                errDiag = result.err;
            }

            CMI5_SetErrorInfoManually(errCode, errMesg, errDiag);
            return false;
        }

        CMI5_STATEMENT_QUEUE = [];
    }

    return true;
}

function CMI5_Finish (exitType, statusWasSet) {
    WriteToDebug("In CMI5_Finish - exitType: " + exitType);
    var st = cmi5.terminatedStatement(),
        result;

    CMI5_ClearErrorInfo();

    CMI5_SetDurationInStatement(st);

    // FEATURE: record something related to the exit type, for instance on timeout set extension?

    CMI5_STATEMENT_QUEUE.push(st);

    result = CMI5_SaveState(CMI5_STATE_TOTAL_TIME, CMI5_GetPreviouslyAccumulatedTime() + GetSessionAccumulatedTime(), "text/plain");
    if (! result) {
        WriteToDebug("CMI5_Finish - failed to record total time to State");
        return false;
    }

    result = CMI5_CommitData();
    if (! result) {
        WriteToDebug("CMI5_Finish - failed to commit data");
        return false;
    }

    return true;
}

function CMI5_ConcedeControl () {
    WriteToDebug("In CMI5_ConcedeControl");
    var returnURL = cmi5.getReturnURL(),
        contentRoot = SearchParentsForContentRoot();

    //
    // if the LMS provided a return URL then the AU must honor it
    //
    if (returnURL !== null && contentRoot !== null) {
        contentRoot.location.assign(returnURL);
        return true;
    }

    //
    // no return URL was provided so try to be a good citizen and
    // close ourselves, if this fails to close then we weren't
    // opened as part of a script call so there isn't much else
    // we can be expected to do
    //
    if (contentRoot !== null) {
        contentRoot.close();
        return true;
    }

    return true;
}

function CMI5_GetStudentID () {
    WriteToDebug("In CMI5_GetStudentID");
    var actor = cmi5.getActor();

    //
    // it was discussed about whether returning just the account name
    // was a proper approach and decided because SD is really only
    // supported by LMS launch that the account would normally only
    // be used with the launching system's account and in that
    // case the homePage isn't strictly necessary for ID determination
    //
    // more advanced use cases should avoid using this method if they
    // require the homePage as well
    //
    return actor.account.name;
}

function CMI5_GetStudentName () {
    WriteToDebug("In CMI5_GetStudentName");

    return cmi5.getActor().toString();
}

function CMI5_GetBookmark () {
    WriteToDebug("In CMI5_GetBookmark");

    return CMI5_RetrieveState(CMI5_STATE_BOOKMARK);
}

//
// description is optional and not part of the original interface, so GetBookmark need only
// support the 'location' argument, so no need to store description which was added to support
// capturing xAPI data for it
//
function CMI5_SetBookmark (location, description) {
    WriteToDebug("In CMI5_SetBookmark - location: " + location + ", description: " + description);
    var st,
        targetId = CreateValidIdentifier(location);

    //
    // Driver's interface has always (well, long enough anyways) returned
    // empty string for "valid identifier" when passed `undefined`, `null`
    // or "" which is clearly not a valid identifier, so prevent writing
    // the statement in that case
    //
    if (targetId !== "") {
        // FEATURE: allow them to disable capture of these statements via launchParameters
        st = cmi5.prepareStatement("http://adlnet.gov/expapi/verbs/experienced");

        //
        // preparing a cmi5 statement sets the target as the AU's Activity
        // but we want to make that the parent and instead use the bookmarked Activity
        // as the target of the allowed statement
        //
        st.context.contextActivities = st.context.contextActivities || new TinCan.ContextActivities();
        st.context.contextActivities.parent = st.context.contextActivities.parent || [];
        st.context.contextActivities.parent.push(st.target);

        //
        // now with the parent set to the AU Activity set up our bookmarked Activity
        // as the target of the statement
        //
        st.target = new TinCan.Activity(
            {
                id: targetId
            }
        );

        CMI5_SetDurationInStatement(st);

        CMI5_STATEMENT_QUEUE.push(st);
    }
    else {
        WriteToDebug("WARNING - No bookmarking statement written, empty location");
    }

    return CMI5_SaveState(CMI5_STATE_BOOKMARK, location, "text/plain");
}

function CMI5_GetDataChunk () {
    WriteToDebug("In CMI5_GetDataChunk");

    return CMI5_RetrieveState(CMI5_STATE_GENERIC_DATA);
}

function CMI5_SetDataChunk (value) {
    WriteToDebug("In CMI5_SetDataChunk");

    return CMI5_SaveState(CMI5_STATE_GENERIC_DATA, value, "text/plain");
}

function CMI5_GetLaunchData () {
    WriteToDebug("In CMI5_GetLaunchData");
    var result = cmi5.getLaunchParameters();

    if (result === null) {
        result = "";
    }

    return result;
}

/*****************************
* Preferences
*****************************/

function CMI5_GetAudioPlayPreference () {
    WriteToDebug("In CMI5_GetAudioPlayPreference");
    var audioPref;

    CMI5_ClearErrorInfo();

    if (CMI5_PREF_AUDIO_PLAY === null) {
        audioPref = cmi5.getAudioPreference();

        if (audioPref === "on") {
            WriteToDebug("    Caching On");
            CMI5_PREF_AUDIO_PLAY = PREFERENCE_ON;
        }
        else if (audioPref === "off") {
            WriteToDebug("    Caching Off");
            CMI5_PREF_AUDIO_PLAY = PREFERENCE_OFF;
        }
        else {
            WriteToDebug("    Caching Default");
            CMI5_PREF_AUDIO_PLAY = PREFERENCE_DEFAULT;
        }
    }

    WriteToDebug("Returning preference: " + CMI5_PREF_AUDIO_PLAY);
    return CMI5_PREF_AUDIO_PLAY;
}

function CMI5_GetAudioVolumePreference () {
    WriteToDebug("In CMI5_GetAudioVolumePreference");
    var volumePref;

    CMI5_ClearErrorInfo();

    if (CMI5_PREF_AUDIO_VOLUME === null) {
        volumePref = CMI5_RetrieveState(CMI5_STATE_VOLUME_PREFERENCE, false);
        if (volumePref === "") {
            volumePref = 100;
        }

        CMI5_PREF_AUDIO_VOLUME = parseInt(volumePref, 10);
    }

    WriteToDebug("Returning " + CMI5_PREF_AUDIO_VOLUME);
    return CMI5_PREF_AUDIO_VOLUME;
}

function CMI5_SetAudioPreference (PlayPreference, intPercentOfMaxVolume) {
    WriteToDebug("In CMI5_SetAudioPreference PlayPreference=" + PlayPreference + ", intPercentOfMaxVolume=" + intPercentOfMaxVolume);
    var currentPlayPreference = cmi5.getAudioPreference(),
        overallResult = true,
        prefChanged = false,
        result;

    CMI5_ClearErrorInfo();

    // update the local caches
    CMI5_PREF_AUDIO_PLAY = PlayPreference;
    CMI5_PREF_AUDIO_VOLUME = intPercentOfMaxVolume;

    if ((PlayPreference == PREFERENCE_OFF) && (currentPlayPreference === null || currentPlayPreference === "on")) {
        // set preference to off
        cmi5.setAudioPreference("off");
        prefChanged = true;
    }
    else if ((PlayPreference != PREFERENCE_OFF) && (currentPlayPreference === null || currentPlayPreference === "off")) {
        // set preference to on
        cmi5.setAudioPreference("on");
        prefChanged = true;
    }

    if (prefChanged) {
        result = cmi5.saveLearnerPrefs();
        if (result) {
            WriteToDebug("  failed to save learner prefs, setting overall result to false");
            overallResult = false;

            CMI5_SetErrorInfoManually(CMI5_DEP_ERROR, "Failed to save learner preferences", result);
        }
    }

    result = CMI5_SaveState(CMI5_STATE_VOLUME_PREFERENCE, intPercentOfMaxVolume, "text/plain", false);
    if (! result) {
        WriteToDebug("  failed to save volume preference in state, setting overall result to false");
        overallResult = false;
    }

    return overallResult;
}

//
// implemented the language preference using state instead of cmi5's built in language
// preference because Driver's interface allows for any arbitrary string, but cmi5 doesn't;
// don't save with this registration because a user might want it across multiple registrations
//
function CMI5_SetLanguagePreference (strLanguage) {
    WriteToDebug("In CMI5_SetLanguagePreference strLanguage=" + strLanguage);

    CMI5_ClearErrorInfo();

    CMI5_PREF_LANGUAGE = strLanguage;

    return CMI5_SaveState(CMI5_STATE_LANGUAGE_PREFERENCE, strLanguage, "text/plain", false);
}

function CMI5_GetLanguagePreference () {
    WriteToDebug("In CMI5_GetLanguagePreference");

    CMI5_ClearErrorInfo();

    if (CMI5_PREF_LANGUAGE === null) {
        CMI5_PREF_LANGUAGE = CMI5_RetrieveState(CMI5_STATE_LANGUAGE_PREFERENCE, false);
    }

    return CMI5_PREF_LANGUAGE;
}

function CMI5_SetSpeedPreference (intPercentOfMax) {
    WriteToDebug("In CMI5_SetSpeedPreference intPercentOfMax=" + intPercentOfMax);

    CMI5_ClearErrorInfo();

    CMI5_PREF_SPEED = intPercentOfMax;

    return CMI5_SaveState(CMI5_STATE_SPEED_PREFERENCE, intPercentOfMax, "text/plain", false);
}

function CMI5_GetSpeedPreference () {
    WriteToDebug("In CMI5_GetSpeedPreference");
    var pref;

    CMI5_ClearErrorInfo();

    if (CMI5_PREF_SPEED === null) {
        pref = CMI5_RetrieveState(CMI5_STATE_SPEED_PREFERENCE, false);
        if (pref === "") {
            pref = 100;
        }

        CMI5_PREF_SPEED = pref;
    }

    return CMI5_PREF_SPEED;
}

function CMI5_SetTextPreference (intPreference) {
    WriteToDebug("In CMI5_SetTextPreference intPreference=" + intPreference);

    CMI5_ClearErrorInfo();

    CMI5_PREF_TEXT = intPreference;

    return CMI5_SaveState(CMI5_STATE_TEXT_PREFERENCE, intPreference, "text/plain", false);
}

function CMI5_GetTextPreference () {
    WriteToDebug("In CMI5_GetTextPreference");

    CMI5_ClearErrorInfo();

    if (CMI5_PREF_TEXT === null) {
        CMI5_PREF_TEXT = CMI5_RetrieveState(CMI5_STATE_TEXT_PREFERENCE, false);
        if (CMI5_PREF_TEXT === "") {
            CMI5_PREF_TEXT = PREFERENCE_DEFAULT;
        }
    }

    return CMI5_PREF_TEXT;
}

function CMI5_GetEntryMode () {
    WriteToDebug("In CMI5_GetEntryMode");

    CMI5_ClearErrorInfo();

    // this should be set during initialize
    return CMI5_ENTRY_MODE;
}

function CMI5_GetLessonMode () {
    WriteToDebug("In CMI5_GetLessonMode - launchMode: " + cmi5.getLaunchMode());
    var launchMode = cmi5.getLaunchMode();

    CMI5_ClearErrorInfo();

    if (launchMode === "Normal") {
        return MODE_NORMAL;
    }
    else if (launchMode === "Browse") {
        return MODE_BROWSE;
    }
    else if (launchMode === "Review") {
        return MODE_REVIEW;
    }

    CMI5_SetErrorInfoManually(CMI5_DEP_ERROR, "Invalid launch mode from LMS", "launchMode = " + launchMode);

    return null;
}

function CMI5_GetTakingForCredit () {
    WriteToDebug("In CMI5_GetTakingForCredit - launchMode: " + cmi5.getLaunchMode());

    if (cmi5.getLaunchMode() === "Normal") {
        return true;
    }
    return false;
}

/*****************************
*  Time management
*****************************/

//
// returns number of milliseconds duration of all launches previous
// to this session
//
function CMI5_GetPreviouslyAccumulatedTime () {
    var retrievedValue;

    WriteToDebug("In CMI5_GetPreviouslyAccumulatedTime - cached: " + CMI5_TOTAL_PREV_DURATION);
    if (CMI5_TOTAL_PREV_DURATION === null) {
        retrievedValue = CMI5_RetrieveState(CMI5_STATE_TOTAL_TIME);
        WriteToDebug("   fetched previous duration for caching: " + retrievedValue);

        if (retrievedValue === "") {
            WriteToDebug("   empty value converting to zero");
            CMI5_TOTAL_PREV_DURATION = 0;
        }
        else {
            CMI5_TOTAL_PREV_DURATION = parseInt(retrievedValue, 10);
        }
    }

    return CMI5_TOTAL_PREV_DURATION;
}

function CMI5_SaveTime (intMilliSeconds) {
    WriteToDebug("In CMI5_SaveTime");

    CMI5_SESSION_DURATION = intMilliSeconds;

    return true;
}

function CMI5_GetMaxTimeAllowed () {
    WriteToDebug("In CMI5_GetMaxTimeAllowed");
    return MAX_CMI_TIME;
}

/*****************************
*  Scoring
*****************************/

function CMI5_SetScore (intScore, intMaxScore, intMinScore) {
    WriteToDebug("In CMI5_SetScore intScore=" + intScore + ", intMaxScore=" + intMaxScore + ", intMinScore=" + intMinScore);
    var score,
        masteryScore;

    CMI5_ClearErrorInfo();

    if (cmi5.getLaunchMode() !== "Normal") {
        CMI5_SetErrorInfoManually(CMI5_ERROR_NON_NORMAL_MODE, "Failed to SetScore: " + intScore, "launch mode (" + cmi5.getLaunchMode() + ") not a recording mode");
        return false;
    }

    //
    // check if passed has already been committed, if so can't set a score to get recorded
    //
    if (CMI5_COMMITTED_STATUS.success === true) {
        CMI5_SetErrorInfoManually(CMI5_ERROR_ALREADY_COMMITTED, "Failed to SetScore: " + intScore, "success status already committed");
        return false;
    }

    score = {
        raw: intScore,
        max: intMaxScore,
        min: intMinScore,

        //
        // SCORM 1.2 used integer values between 0 and 100 for score, 2004 introduced scaled value
        // so Driver's API is based on the raw value that gets normalized for the scaled value, so
        // we divide by 100 and then round to 2 decimal places because theoretically an integer
        // couldn't have more precision
        //
        // of course this is very silly in the case when min isn't 0, and/or max isn't 100 because
        // then the scaled score is completely out of whack, for example:
        //
        // min 0, max 4, score 2 === 0.02
        // min 0, max 4, masteryScore = 0.5, so passing score === 50
        //
        // *but* we can't really change Driver's implementation at this point as those ships have
        // sailed, so just perpetuate the awfulness
        //
        scaled: RoundToPrecision(intScore / 100, 2)
    };

    try {
        cmi5.validateScore(score);
    }
    catch (ex) {
        CMI5_SetErrorInfoManually(CMI5_ERROR_INVALID_SCORE, "Failed to SetScore: " + intScore, "invalid score: " + ex.message);
        return false;
    }

    CMI5_PENDING_STATUS.score = score;

    //
    // to mimic the SCORM behavior, check to see if there is a masteryScore and if so
    // determine pass/fail and set that into the pending state
    //
    masteryScore = cmi5.getMasteryScore();
    if (masteryScore !== null) {
        if (score.scaled >= masteryScore) {
            WriteToDebug("   automatically calling SetPassed based on masteryScore");
            return CMI5_SetPassed();
        }
        else if (score.scaled < masteryScore) {
            WriteToDebug("   automatically calling SetFailed based on masteryScore");
            return CMI5_SetFailed();
        }
    }

    return true;
}

function CMI5_GetScore () {
    WriteToDebug("In CMI5_GetScore");
    var result = "";

    CMI5_ClearErrorInfo();

    if (CMI5_COMMITTED_STATUS !== null && typeof CMI5_COMMITTED_STATUS.score !== "undefined" && CMI5_COMMITTED_STATUS.score !== null) {
        result = CMI5_COMMITTED_STATUS.score.raw;
    }
    else if (CMI5_PENDING_STATUS.score !== null) {
        result = CMI5_PENDING_STATUS.score.raw;
    }

    return result;
}

function CMI5_GetScaledScore () {
    WriteToDebug("CMI5_GetScaledScore");
    var result = "";

    CMI5_ClearErrorInfo();

    if (CMI5_COMMITTED_STATUS !== null && typeof CMI5_COMMITTED_STATUS.score !== "undefined" && CMI5_COMMITTED_STATUS.score !== null) {
        result = CMI5_COMMITTED_STATUS.score.scaled;
    }
    else if (CMI5_PENDING_STATUS.score !== null) {
        result = CMI5_PENDING_STATUS.score.scaled;
    }

    return result;
}

function CMI5_SetPointBasedScore (intScore, intMaxScore, intMinScore) {
    WriteToDebug("CMI5_SetPointBasedScore - cmi5 does not support SetPointBasedScore, falling back to SetScore");
    return CMI5_SetScore(intScore, intMaxScore, intMinScore);
}

/***********************
*    Interactions
************************/

//
// strID and strLearningObjectiveID come in from CreateValidIdentifier so already
// have the AU id attached when needed
//
// strAlternateResponse and strAlternateCorrectResponse are for LMSs that can't
// handle longer values, we never need them
//
// dtmTime is just the current date/time which we capture as the 'timestamp' in
// the statements anyways so no need to use it (cause the API user can't pass it)
//
function CMI5_RecordInteraction (
    strID,
    strResponse,
    blnCorrect,
    strCorrectResponse,
    strDescription,
    intWeighting,
    intLatency,
    strLearningObjectiveID,
    dtmTime,
    interactionType,
    strAlternateResponse,
    strAlternateCorrectResponse
) {
    var st;

    CMI5_ClearErrorInfo();

    if (cmi5.getLaunchMode() !== "Normal") {
        CMI5_SetErrorInfoManually(CMI5_ERROR_NON_NORMAL_MODE, "Failed to RecordInteraction: launch mode (" + cmi5.getLaunchMode() + ") not a recording mode");
        return false;
    }

    st = cmi5.prepareStatement("http://adlnet.gov/expapi/verbs/answered");

    st.result = st.result || new TinCan.Result();
    if (strResponse !== null) {
        st.result.response = strResponse;
    }

    //
    // for INTERACTION_RESULT_UNANTICIPATED or INTERACTION_RESULT_NEUTRAL just
    // leave .success out
    //
    if (blnCorrect === true || blnCorrect === INTERACTION_RESULT_CORRECT) {
        st.result.success = true;
    }
    else if (blnCorrect === false || blnCorrect === "" || blnCorrect === "false" || blnCorrect === INTERACTION_RESULT_WRONG) {
        st.result.success = false;
    }

    if (intLatency !== null && intLatency !== "") {
        st.result.duration = TinCan.Utils.convertMillisecondsToISO8601Duration(intLatency);
    }
    if (intWeighting !== null && intWeighting !== "") {
        st.result.extensions = st.result.extensions || {};
        st.result.extensions["http://id.tincanapi.com/extension/cmi-interaction-weighting"] = intWeighting;
    }

    //
    // preparing a cmi5 statement sets the target as the AU's Activity
    // but we want to make that the parent and instead use the interaction Activity
    // as the target of the allowed statement
    //
    st.context.contextActivities = st.context.contextActivities || new TinCan.ContextActivities();
    st.context.contextActivities.parent = st.context.contextActivities.parent || [];
    st.context.contextActivities.parent.push(st.target);

    if (strLearningObjectiveID !== "") {
        st.context.contextActivities.grouping = st.context.contextActivities.grouping || [];
        st.context.contextActivities.grouping.push(
            new TinCan.Activity(
                {
                    id: strLearningObjectiveID
                }
            )
        );
    }

    //
    // now with the parent set to the AU Activity set up our interaction Activity
    // as the target of the statement
    //
    st.target = new TinCan.Activity(
        {
            id: strID,
            definition: {
                type: CMI5_INTERACTION_ACTIVITY_TYPE,
                interactionType: interactionType
            }
        }
    );

    // FEATURE: provide a way to configure what language is in use?
    if (strDescription !== null && strDescription !== "") {
        st.target.definition.description = {
            und: strDescription
        };
    }

    //
    // this makes it so that "" can never be a correct response, but that's the way
    // the other standards in Driver work, and is potentially a reasonable expectation
    //
    if (strCorrectResponse !== null && strCorrectResponse !== "") {
        st.target.definition.correctResponsesPattern = [strCorrectResponse];
    }

    CMI5_STATEMENT_QUEUE.push(st);

    CMI5_CreateInteraction.apply(this, arguments);
    CMI5_SaveInteraction(strID);

    return true;
}

function CMI5_RecordTrueFalseInteraction (strID, blnResponse, blnCorrect, blnCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime) {
    WriteToDebug(
        "In CMI5_RecordTrueFalseInteraction strID=" + strID + ", strResponse=" + strResponse +
        ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
        ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
        ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime
    );

    var strResponse = null,
        strCorrectResponse = null;

    if (blnResponse === true) {
        strResponse = "true";
    }
    // test for false b/c it could be null in which case we want to leave it as `null`
    else if (blnResponse === false) {
        strResponse = "false";
    }

    if (blnCorrectResponse === true) {
        strCorrectResponse = "true";
    }
    // test for false b/c it could be null in which case we want to leave it as `null`
    else if (blnCorrectResponse === false) {
        strCorrectResponse = "false";
    }

    return CMI5_RecordInteraction(
        strID,
        strResponse,
        blnCorrect,
        strCorrectResponse,
        strDescription,
        intWeighting,
        intLatency,
        strLearningObjectiveID,
        dtmTime,
        CMI5_INTERACTION_TYPE_TRUE_FALSE
    );
}

function CMI5_RecordMultipleChoiceInteraction (strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime) {
    WriteToDebug(
        "In CMI5_RecordMultipleChoiceInteraction strID=" + strID + ", aryResponse=" + aryResponse +
        ", blnCorrect=" + blnCorrect + ", aryCorrectResponse=" + aryCorrectResponse +
        ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
        ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime
    );

    var strResponse = null,
        strCorrectResponse = "",
        i;

    if (aryResponse !== null) {
        strResponse = "";

        for (i = 0; i < aryResponse.length; i += 1) {
            if (strResponse.length > 0) {
                strResponse += "[,]";
            }

            strResponse += (aryResponse[i].Long !== "" ? aryResponse[i].Long : aryResponse[i].Short);
        }
    }

    for (i = 0; i < aryCorrectResponse.length; i += 1) {
        if (strCorrectResponse.length > 0) {
            strCorrectResponse += "[,]";
        }

        strCorrectResponse += (aryCorrectResponse[i].Long !== "" ? aryCorrectResponse[i].Long : aryCorrectResponse[i].Short);
    }

    return CMI5_RecordInteraction(
        strID,
        strResponse,
        blnCorrect,
        strCorrectResponse,
        strDescription,
        intWeighting,
        intLatency,
        strLearningObjectiveID,
        dtmTime,
        CMI5_INTERACTION_TYPE_CHOICE
    );
}

function CMI5_RecordFillInInteraction (strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime) {
    WriteToDebug(
        "In CMI5_RecordFillInInteraction strID=" + strID + ", strResponse=" + strResponse +
        ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
        ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
        ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime
    );

    var interactionType = CMI5_INTERACTION_TYPE_FILL_IN;

    if (strResponse !== null) {
        strResponse = new String(strResponse);
    }

    if (strCorrectResponse !== null) {
        strCorrectResponse = new String(strCorrectResponse);

        if (strCorrectResponse.length > 250) {
            interactionType = CMI5_INTERACTION_TYPE_LONG_FILL_IN;
        }
    }

    return CMI5_RecordInteraction(
        strID,
        strResponse,
        blnCorrect,
        strCorrectResponse,
        strDescription,
        intWeighting,
        intLatency,
        strLearningObjectiveID,
        dtmTime,
        interactionType
    );
}

function CMI5_RecordMatchingInteraction (strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime) {
    WriteToDebug(
        "In CMI5_RecordMatchingInteraction strID=" + strID + ", aryResponse=" + aryResponse +
        ", blnCorrect=" + blnCorrect + ", aryCorrectResponse=" + aryCorrectResponse +
        ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
        ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime
    );

    var strResponse = null,
        strCorrectResponse = "",
        i;

    if (aryResponse !== null) {
        strResponse = "";

        for (i = 0; i < aryResponse.length; i += 1) {
            if (strResponse.length > 0) {
                strResponse += "[,]";
            }

            strResponse += (aryResponse[i].Source.Long !== "" ? aryResponse[i].Source.Long : aryResponse[i].Source.Short) + "[.]" + (aryResponse[i].Target.Long !== "" ? aryResponse[i].Target.Long : aryResponse[i].Target.Short);
        }
    }

    for (i = 0; i < aryCorrectResponse.length; i += 1) {
        if (strCorrectResponse.length > 0) {
            strCorrectResponse += "[,]";
        }

        strCorrectResponse += (aryCorrectResponse[i].Source.Long !== "" ? aryCorrectResponse[i].Source.Long : aryCorrectResponse[i].Source.Short) + "[.]" + (aryCorrectResponse[i].Target.Long !== "" ? aryCorrectResponse[i].Target.Long : aryCorrectResponse[i].Target.Short);
    }

    return CMI5_RecordInteraction(
        strID,
        strResponse,
        blnCorrect,
        strCorrectResponse,
        strDescription,
        intWeighting,
        intLatency,
        strLearningObjectiveID,
        dtmTime,
        CMI5_INTERACTION_TYPE_MATCHING
    );
}

function CMI5_RecordPerformanceInteraction (strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime) {
    WriteToDebug(
        "In CMI5_RecordPerformanceInteraction strID=" + strID + ", strResponse=" + strResponse +
        ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
        ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
        ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime
    );

    if (strResponse !== null) {
        strResponse = new String(strResponse);
    }

    if (strCorrectResponse !== null) {
        strCorrectResponse = new String(strCorrectResponse);
    }

    return CMI5_RecordInteraction(
        strID,
        strResponse,
        blnCorrect,
        strCorrectResponse,
        strDescription,
        intWeighting,
        intLatency,
        strLearningObjectiveID,
        dtmTime,
        CMI5_INTERACTION_TYPE_PERFORMANCE
    );
}

function CMI5_RecordSequencingInteraction (strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime) {
    WriteToDebug(
        "In CMI5_RecordSequencingInteraction strID=" + strID + ", aryResponse=" + aryResponse +
        ", blnCorrect=" + blnCorrect + ", aryCorrectResponse=" + aryCorrectResponse +
        ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
        ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime
    );

    var strResponse = null,
        strCorrectResponse = "",
        i;

    if (aryResponse !== null) {
        strResponse = "";

        for (i = 0; i < aryResponse.length; i += 1) {
            if (strResponse.length > 0) {
                strResponse += "[,]";
            }

            strResponse += (aryResponse[i].Long !== "" ? aryResponse[i].Long : aryResponse[i].Short);
        }
    }

    for (i = 0; i < aryCorrectResponse.length; i += 1) {
        if (strCorrectResponse.length > 0) {
            strCorrectResponse += "[,]";
        }

        strCorrectResponse += (aryCorrectResponse[i].Long !== "" ? aryCorrectResponse[i].Long : aryCorrectResponse[i].Short);
    }

    return CMI5_RecordInteraction(
        strID,
        strResponse,
        blnCorrect,
        strCorrectResponse,
        strDescription,
        intWeighting,
        intLatency,
        strLearningObjectiveID,
        dtmTime,
        CMI5_INTERACTION_TYPE_SEQUENCING
    );
}

function CMI5_RecordLikertInteraction (strID, response, blnCorrect, correctResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime) {
    WriteToDebug(
        "In CMI5_RecordLikertInteraction strID=" + strID + ", response=" + response +
        ", blnCorrect=" + blnCorrect + ", correctResponse=" + correctResponse +
        ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
        ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime
    );

    var strResponse = null,
        strCorrectResponse = "";

    if (response !== null) {
         strResponse = (response.Long !== "" ? response.Long : response.Short);
    }
    if (correctResponse !== null) {
        strCorrectResponse = (correctResponse.Long !== "" ? correctResponse.Long : correctResponse.Short);
    }

    return CMI5_RecordInteraction(
        strID,
        strResponse,
        blnCorrect,
        strCorrectResponse,
        strDescription,
        intWeighting,
        intLatency,
        strLearningObjectiveID,
        dtmTime,
        CMI5_INTERACTION_TYPE_LIKERT
    );
}

function CMI5_RecordNumericInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime) {
    WriteToDebug(
        "In CMI5_RecordNumericInteraction strID=" + strID + ", strResponse=" + strResponse +
        ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
        ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency +
        ", strLearningObjectiveID=" + strLearningObjectiveID + ", dtmTime=" + dtmTime
    );

    if (typeof strCorrectResponse !== "undefined" && strCorrectResponse !== null) {
        // cmi5 can handle numeric ranges or decimals. As long as we received one of those we are ok to continue
        if (! IsValidDecimalRange(strCorrectResponse) && ! IsValidDecimal(strCorrectResponse)) {
            WriteToDebug("Returning False - CMI5_RecordNumericInteraction received invalid correct response (not a decimal or range), strCorrectResponse=" + strCorrectResponse);
            return false;
        }
    }

    return CMI5_RecordInteraction(
        strID,
        strResponse,
        blnCorrect,
        strCorrectResponse,
        strDescription,
        intWeighting,
        intLatency,
        strLearningObjectiveID,
        dtmTime,
        CMI5_INTERACTION_TYPE_NUMERIC
    );
}

//
// Previously recorded interactions have metadata that can be accessed
// by the following functions for some standards, implemented this via
// the state mechanism and a local cache, if the interation hasn't been
// previously recorded then the content should not expect to use these
//
function CMI5_CreateInteraction (
    strID,
    strResponse,
    blnCorrect,
    strCorrectResponse,
    strDescription,
    intWeighting,
    intLatency,
    strLearningObjectiveID,
    dtmTime,
    interactionType,
    strAlternateResponse,
    strAlternateCorrectResponse
) {
    CMI5_INTERACTIONS[strID] = {
        type: interactionType,
        timestamp: dtmTime.toJSON(),
        correctResponses: strCorrectResponse,
        learnerResponses: strResponse,
        weighting: intWeighting,
        result: blnCorrect,
        latency: intLatency,
        description: strDescription
    };

    return;
}

function CMI5_LoadInteraction (strInteractionId) {
    WriteToDebug("CMI5_LoadInteraction - strInteractionId = " + strInteractionId);
    var result;

    if (typeof CMI5_INTERACTIONS[strInteractionId] !== "undefined") {
        WriteToDebug("    already locally cached, returning");
        return true;
    }

    result = CMI5_RetrieveState(strInteractionId);

    //
    // retrieve state doesn't provide a return value for detecting errors
    // so check to see if we have manually set errors (cause we shouldn't
    // unless the retrieve failed)
    //
    if (intCMI5Error) {
        WriteToDebug("    failed to retrieve state for interaction: " + strCMI5ErrorString);
        return false;
    }

    if (result !== "" && typeof result.timestamp === "undefined") {
        // interaction was found but most likely failed to parse as JSON
        // which retrieveState in TinCanJS eats as an error, but make it
        // one here
        CMI5_SetErrorInfoManually(CMI5_ERROR_INTERACTION_INVALID_JSON, "Invalid JSON for interaction in State", "result= " + result);
        return false;
    }

    if (result === "") {
        // interaction doesn't exist which is okay here,
        // just don't store anything in our local cache
        return true;
    }

    CMI5_INTERACTIONS[strInteractionId] = result;

    WriteToDebug("    cached interaction locally");
    return true;
}

function CMI5_SaveInteraction (strInteractionId) {
    WriteToDebug("CMI5_SaveInteraction - strInteractionId = " + strInteractionId);

    if (typeof CMI5_INTERACTIONS[strInteractionId] === "undefined") {
        CMI5_SetErrorInfoManually(CMI5_ERROR_UNRECOGNIZED_INTERACTION, "Cannot save unknown interaction: " + strInteractionId);
        return false;
    }

    return CMI5_SaveState(strInteractionId, CMI5_INTERACTIONS[strInteractionId], "application/json");
}

function CMI5_GetInteractionType(strInteractionID) {
    WriteToDebug("CMI5_GetInteractionType - strInteractionID = " + strInteractionID);
    var result;

    CMI5_ClearErrorInfo();

    result = CMI5_LoadInteraction(strInteractionID);
    if (! result) {
        WriteToDebug("Failed to load interaction: " + strInteractionID);
        CMI5_SetErrorInfoManually(CMI5_ERROR_INTERACTION_LOAD_FAILED, "Cannot get interaction type: " + strInteractionID, "Failed to load interaction: " + strCMI5ErrorString + " (" + strCMI5ErrorDiagnostic + ")");
        return false;
    }

    if (typeof CMI5_INTERACTIONS[strInteractionID] !== "undefined") {
        return CMI5_INTERACTIONS[strInteractionID].type;
    }

    return "";
}

function CMI5_GetInteractionTimestamp(strInteractionID) {
    WriteToDebug("CMI5_GetInteractionTimestamp - strInteractionID = " + strInteractionID);
    var result;

    CMI5_ClearErrorInfo();

    result = CMI5_LoadInteraction(strInteractionID);
    if (! result) {
        WriteToDebug("Failed to load interaction: " + strInteractionID);
        CMI5_SetErrorInfoManually(CMI5_ERROR_INTERACTION_LOAD_FAILED, "Cannot get interaction timestamp: " + strInteractionID, "Failed to load interaction: " + strCMI5ErrorString + " (" + strCMI5ErrorDiagnostic + ")");
        return false;
    }

    if (typeof CMI5_INTERACTIONS[strInteractionID] !== "undefined") {
        return new Date(CMI5_INTERACTIONS[strInteractionID].timestamp);
    }

    return "";
}

function CMI5_GetInteractionCorrectResponses(strInteractionID) {
    WriteToDebug("CMI5_GetInteractionCorrectResponses - strInteractionID = " + strInteractionID);
    var result,
        responses = [];

    CMI5_ClearErrorInfo();

    result = CMI5_LoadInteraction(strInteractionID);
    if (! result) {
        WriteToDebug("Failed to load interaction: " + strInteractionID);
        CMI5_SetErrorInfoManually(CMI5_ERROR_INTERACTION_LOAD_FAILED, "Cannot get interaction correct responses: " + strInteractionID, "Failed to load interaction: " + strCMI5ErrorString + " (" + strCMI5ErrorDiagnostic + ")");
        return false;
    }

    if (typeof CMI5_INTERACTIONS[strInteractionID] !== "undefined") {
        responses = CMI5_INTERACTIONS[strInteractionID].correctResponses.split("[,]");

        if (CMI5_INTERACTIONS[strInteractionID].type === CMI5_INTERACTION_TYPE_MATCHING) {
            responses = CMI5_ProcessResponseArray(responses);
        }
    }

    return responses;
}

function CMI5_GetInteractionLearnerResponses(strInteractionID) {
    WriteToDebug("CMI5_GetInteractionLearnerResponses - strInteractionID = " + strInteractionID);
    var result,
        responses = [];

    CMI5_ClearErrorInfo();

    result = CMI5_LoadInteraction(strInteractionID);
    if (! result) {
        WriteToDebug("Failed to load interaction: " + strInteractionID);
        CMI5_SetErrorInfoManually(CMI5_ERROR_INTERACTION_LOAD_FAILED, "Cannot get interaction learner responses: " + strInteractionID, "Failed to load interaction: " + strCMI5ErrorString + " (" + strCMI5ErrorDiagnostic + ")");
        return false;
    }

    if (typeof CMI5_INTERACTIONS[strInteractionID] !== "undefined") {
        responses = CMI5_INTERACTIONS[strInteractionID].learnerResponses.split("[,]");

        if (CMI5_INTERACTIONS[strInteractionID].type === CMI5_INTERACTION_TYPE_MATCHING) {
            responses = CMI5_ProcessResponseArray(responses);
        }
    }

    return responses;
}

function CMI5_ProcessResponseArray(responses) {
    var i;

    for (i = 0; i < responses.length; i += 1) {
        responses[i] = CreateMatchingResponse(responses[i]);
    }

    return responses;
}

function CMI5_GetInteractionWeighting(strInteractionID) {
    WriteToDebug("CMI5_GetInteractionWeighting - strInteractionID = " + strInteractionID);
    var result;

    CMI5_ClearErrorInfo();

    result = CMI5_LoadInteraction(strInteractionID);
    if (! result) {
        WriteToDebug("Failed to load interaction: " + strInteractionID);
        CMI5_SetErrorInfoManually(CMI5_ERROR_INTERACTION_LOAD_FAILED, "Cannot get interaction weighting: " + strInteractionID, "Failed to load interaction: " + strCMI5ErrorString + " (" + strCMI5ErrorDiagnostic + ")");
        return false;
    }

    if (typeof CMI5_INTERACTIONS[strInteractionID] !== "undefined") {
        return CMI5_INTERACTIONS[strInteractionID].weighting;
    }

    return "";
}

function CMI5_GetInteractionResult(strInteractionID) {
    WriteToDebug("CMI5_GetInteractionResult - strInteractionID = " + strInteractionID);
    var result;

    CMI5_ClearErrorInfo();

    result = CMI5_LoadInteraction(strInteractionID);
    if (! result) {
        WriteToDebug("Failed to load interaction: " + strInteractionID);
        CMI5_SetErrorInfoManually(CMI5_ERROR_INTERACTION_LOAD_FAILED, "Cannot get interaction result: " + strInteractionID, "Failed to load interaction: " + strCMI5ErrorString + " (" + strCMI5ErrorDiagnostic + ")");
        return false;
    }

    if (typeof CMI5_INTERACTIONS[strInteractionID] !== "undefined") {
        return CMI5_INTERACTIONS[strInteractionID].result;
    }

    return "";
}

function CMI5_GetInteractionLatency(strInteractionID) {
    WriteToDebug("CMI5_GetInteractionLatency - strInteractionID = " + strInteractionID);
    var result;

    CMI5_ClearErrorInfo();

    result = CMI5_LoadInteraction(strInteractionID);
    if (! result) {
        WriteToDebug("Failed to load interaction: " + strInteractionID);
        CMI5_SetErrorInfoManually(CMI5_ERROR_INTERACTION_LOAD_FAILED, "Cannot get interaction latency: " + strInteractionID, "Failed to load interaction: " + strCMI5ErrorString + " (" + strCMI5ErrorDiagnostic + ")");
        return false;
    }

    if (typeof CMI5_INTERACTIONS[strInteractionID] !== "undefined") {
        return CMI5_INTERACTIONS[strInteractionID].latency;
    }

    return "";
}

function CMI5_GetInteractionDescription(strInteractionID) {
    WriteToDebug("CMI5_GetInteractionDescription - strInteractionID = " + strInteractionID);
    var result;

    CMI5_ClearErrorInfo();

    result = CMI5_LoadInteraction(strInteractionID);
    if (! result) {
        WriteToDebug("Failed to load interaction: " + strInteractionID);
        CMI5_SetErrorInfoManually(CMI5_ERROR_INTERACTION_LOAD_FAILED, "Cannot get interaction description: " + strInteractionID, "Failed to load interaction: " + strCMI5ErrorString + " (" + strCMI5ErrorDiagnostic + ")");
        return false;
    }

    if (typeof CMI5_INTERACTIONS[strInteractionID] !== "undefined") {
        return CMI5_INTERACTIONS[strInteractionID].description;
    }

    return "";
}

/*****************************
* Objectives
*****************************/

function CMI5_SetObjectiveScore (strObjectiveID, intScore, intMaxScore, intMinScore) {
    WriteToDebug("In CMI5_SetObjectiveScore, strObjectiveID=" + strObjectiveID + ", intScore=" + intScore + ", intMaxScore=" + intMaxScore + ", intMinScore=" + intMinScore);

    ClearErrorInfo();
    CMI5_SetErrorInfoManually(CMI5_ERROR_NOT_IMPLEMENTED, "SetObjectiveScore not implemented in cmi5", "");

    return false;
}

function CMI5_SetObjectiveDescription (strObjectiveID, strObjectiveDescription) {
    WriteToDebug("In CMI5_SetObjectiveDescription, strObjectiveDescription=" + strObjectiveDescription);

    ClearErrorInfo();
    CMI5_SetErrorInfoManually(CMI5_ERROR_NOT_IMPLEMENTED, "SetObjectiveDescription not implemented in cmi5", "");

    return false;
}

function CMI5_SetObjectiveStatus (strObjectiveID, Lesson_Status) {
    WriteToDebug("In CMI5_SetObjectiveStatus strObjectiveID=" + strObjectiveID + ", Lesson_Status=" + Lesson_Status);

    ClearErrorInfo();
    CMI5_SetErrorInfoManually(CMI5_ERROR_NOT_IMPLEMENTED, "SetObjectiveStatus not implemented in cmi5", "");

    return false;
}

function CMI5_SetObjectiveProgressMeasure () {
    WriteToDebug("In CMI5_SetObjectiveProgressMeasure");

    ClearErrorInfo();
    CMI5_SetErrorInfoManually(CMI5_ERROR_NOT_IMPLEMENTED, "SetObjectiveProgressMeasure not implemented in cmi5", "");

    return false;
}

function CMI5_GetObjectiveScore (strObjectiveID) {
    WriteToDebug("In CMI5_GetObjectiveScore, strObjectiveID=" + strObjectiveID);

    ClearErrorInfo();
    CMI5_SetErrorInfoManually(CMI5_ERROR_NOT_IMPLEMENTED, "GetObjectiveScore not implemented in cmi5", "");

    return false;
}

function CMI5_GetObjectiveDescription (strObjectiveID) {
    WriteToDebug("In CMI5_GetObjectiveDescription, strObjectiveID=" + strObjectiveID);

    ClearErrorInfo();
    CMI5_SetErrorInfoManually(CMI5_ERROR_NOT_IMPLEMENTED, "GetObjectiveDescription not implemented in cmi5", "");

    return false;
}

function CMI5_GetObjectiveStatus (strObjectiveID) {
    WriteToDebug("In CMI5_GetObjectiveStatus, strObjectiveID=" + strObjectiveID);

    ClearErrorInfo();
    CMI5_SetErrorInfoManually(CMI5_ERROR_NOT_IMPLEMENTED, "GetObjectiveStatus not implemented in cmi5", "");

    return false;
}

function CMI5_GetObjectiveProgressMeasure () {
    WriteToDebug("CMI5_GetObjectiveProgressMeasure");

    ClearErrorInfo();
    CMI5_SetErrorInfoManually(CMI5_ERROR_NOT_IMPLEMENTED, "GetObjectiveProgressMeasure not implemented in cmi5", "");

    return false;
}

/*****************************
*  Status Management
*****************************/

function CMI5_SetFailed () {
    WriteToDebug("In CMI5_SetFailed");
    var masteryScore,
        auActivityId,
        tempSt,
        i,
        st;

    CMI5_ClearErrorInfo();

    if (cmi5.getLaunchMode() !== "Normal") {
        CMI5_SetErrorInfoManually(CMI5_ERROR_NON_NORMAL_MODE, "Failed to SetFailed", "Launch mode (" + cmi5.getLaunchMode() + ") not a recording mode");
        return false;
    }

    //
    // check to see if success has already been permanently set
    //
    if (CMI5_COMMITTED_STATUS.success === true) {
        CMI5_SetErrorInfoManually(CMI5_ERROR_ALREADY_COMMITTED, "Failed to SetFailed", "Success status already committed");
        return false;
    }

    //
    // if pending status already set nothing to do
    //
    if (CMI5_PENDING_STATUS.success === false) {
        // FEATURE: should we update the duration/timestamp of the pending statement?
        return true;
    }

    //
    // check to see if a pending score has been set and if so make sure it works with masteryScore
    //
    masteryScore = cmi5.getMasteryScore();
    if (CMI5_PENDING_STATUS.score !== null && masteryScore !== null && CMI5_PENDING_STATUS.score.scaled >= masteryScore) {
        CMI5_SetErrorInfoManually(CMI5_ERROR_INVALID_STATUS_BASED_ON_SCORE, "Failed to SetFailed", "Pending score conflicts with failure");
        return false;
    }

    //
    // if not permanently set but is pending set then need to remove passed statement
    //
    if (CMI5_PENDING_STATUS.success === true) {
        auActivityId = cmi5.getActivity().id;

        for (i = 0; i < CMI5_STATEMENT_QUEUE.length; i += 1) {
            tempSt = CMI5_STATEMENT_QUEUE[i];

            if (tempSt.target.id === auActivityId && tempSt.verb.id === CMI5_VERB_ID_PASSED) {
                CMI5_STATEMENT_QUEUE.splice(i, i + 1);
            }
        }
    }
    CMI5_PENDING_STATUS.success = false;

    st = cmi5.failedStatement();

    CMI5_SetDurationInStatement(st);

    CMI5_STATEMENT_QUEUE.push(st);

    return true;
}

function CMI5_SetPassed () {
    WriteToDebug("In CMI5_SetPassed");
    var masteryScore,
        auActivityId,
        tempSt,
        i,
        st;

    CMI5_ClearErrorInfo();

    if (cmi5.getLaunchMode() !== "Normal") {
        CMI5_SetErrorInfoManually(CMI5_ERROR_NON_NORMAL_MODE, "Failed to SetPassed", "Launch mode (" + cmi5.getLaunchMode() + ") not a recording mode");
        return false;
    }

    //
    // check to see if success has already been permanently set
    //
    if (CMI5_COMMITTED_STATUS.success === true) {
        CMI5_SetErrorInfoManually(CMI5_ERROR_ALREADY_COMMITTED, "Failed to SetPassed", "Success status already committed");
        return false;
    }

    //
    // if pending status already set nothing to do
    //
    if (CMI5_PENDING_STATUS.success === true) {
        // FEATURE: should we update the duration/timestamp of the pending statement?
        return true;
    }

    //
    // check to see if a pending score has been set and if so make sure it works with masteryScore
    //
    masteryScore = cmi5.getMasteryScore();
    if (CMI5_PENDING_STATUS.score !== null && masteryScore !== null && CMI5_PENDING_STATUS.score.scaled < masteryScore) {
        CMI5_SetErrorInfoManually(CMI5_ERROR_INVALID_STATUS_BASED_ON_SCORE, "Failed to SetPassed", "Pending score conflicts with failure");
        return false;
    }

    CMI5_PENDING_STATUS.success = true;

    st = cmi5.passedStatement();

    CMI5_SetDurationInStatement(st);

    CMI5_STATEMENT_QUEUE.push(st);

    return true;
}

function CMI5_SetCompleted () {
    WriteToDebug("In CMI5_SetCompleted");
    var st;

    CMI5_ClearErrorInfo();

    if (cmi5.getLaunchMode() !== "Normal") {
        CMI5_SetErrorInfoManually(CMI5_ERROR_NON_NORMAL_MODE, "Failed to SetCompleted", "Launch mode (" + cmi5.getLaunchMode() + ") not a recording mode");
        return false;
    }

    //
    // check to see if completion has already been permanently set
    //
    if (CMI5_COMMITTED_STATUS.completion !== null) {
        CMI5_SetErrorInfoManually(CMI5_ERROR_ALREADY_COMMITTED, "Failed to SetCompleted", "Completion status already committed");
        return false;
    }

    //
    // if pending status already set nothing to do
    //
    if (CMI5_PENDING_STATUS.completion !== null) {
        // FEATURE: should we update the duration/timestamp of the pending statement?
        return true;
    }
    CMI5_PENDING_STATUS.completion = true;

    st = cmi5.completedStatement();

    CMI5_STATEMENT_QUEUE.push(st);

    CMI5_SetDurationInStatement(st);

    return true;
}

function CMI5_ResetStatus () {
    WriteToDebug("In CMI5_ResetStatus");
    var auActivityId,
        i,
        tempSt;

    CMI5_ClearErrorInfo();

    if (cmi5.getLaunchMode() !== "Normal") {
        CMI5_SetErrorInfoManually(CMI5_ERROR_NON_NORMAL_MODE, "Failed to ResetStatus", "Launch mode (" + cmi5.getLaunchMode() + ") not a recording mode");
        return false;
    }

    //
    // check to see if any previous status information has already
    // been committed, if it has then don't allow resetting the status
    //
    if (CMI5_COMMITTED_STATUS.completion !== null || CMI5_COMMITTED_STATUS.success === true) {
        CMI5_SetErrorInfoManually(CMI5_ERROR_ALREADY_COMMITTED, "Failed to ResetStatus", "Success and/or completion status has already been committed");
        return false;
    }

    // FEATURE: make it configurable to do partial resets?

    CMI5_PENDING_STATUS.completion = null;
    CMI5_PENDING_STATUS.success = null;

    auActivityId = cmi5.getActivity().id;
    for (i = 0; i < CMI5_STATEMENT_QUEUE.length; i += 1) {
        tempSt = CMI5_STATEMENT_QUEUE[i];

        if (
            tempSt.target.id = auActivityId
            &&
            (tempSt.verb.id === CMI5_VERB_ID_COMPLETED || tempSt.verb.id === CMI5_VERB_ID_PASSED || tempSt.verb.id === CMI5_VERB_ID_FAILED)
        ) {
            CMI5_STATEMENT_QUEUE.splice(i, i + 1);
        }
    }

    return true;
}

function CMI5_GetStatus () {
    WriteToDebug("In CMI5_GetStatus");
    var result,
        i,
        numAttempts = 0;

    CMI5_ClearErrorInfo();

    if (CMI5_COMMITTED_STATUS.success === true || CMI5_PENDING_STATUS.success === true) {
        result = LESSON_STATUS_PASSED;
    }
    else if (CMI5_COMMITTED_STATUS.success === false || CMI5_PENDING_STATUS.success === false) {
        result = LESSON_STATUS_FAILED;
    }
    else if (CMI5_COMMITTED_STATUS.completion === true || CMI5_PENDING_STATUS.completion === true) {
        result = LESSON_STATUS_COMPLETED;
    }
    else {
        for (i = 0; i < CMI5_COMMITTED_STATUS.launchModes.length; i += 1) {
            if (CMI5_COMMITTED_STATUS.launchModes[i] === "Normal") {
                numAttempts += 1;
            }
        }

        if (numAttempts === 0) {
            result = LESSON_STATUS_BROWSED;
        }
        else {
            if (CMI5_ENTRY_MODE === ENTRY_FIRST_TIME) {
                result = LESSON_STATUS_NOT_ATTEMPTED;
            }
            else {
                result = LESSON_STATUS_INCOMPLETE;
            }
        }
    }

    WriteToDebug("In CMI5_GetStatus - result = " + result);
    return result;
}

function CMI5_GetPassingScore () {
    WriteToDebug("In CMI5_GetPassingScore");
    var result;

    CMI5_ClearErrorInfo();

    result = cmi5.getMasteryScore();
    if (result === null) {
        return 0;
    }

    //
    // Driver's interface assumes an integer in the range of 0 to 100 for SCORM 1.2
    // conformance, so multiply by 100 (see comment in CMI5_SetScore why this is silly)
    //
    result = result * 100;

    return result;
}

/*****************************
*  Shared State Persistence
*****************************/

function CMI5_DetectSSPSupport () {
    WriteToDebug("CMI5_DetectSSPSupport - implemented via state");

    ClearErrorInfo();

    return true;
}

function CMI5_CreateDataBucket (strBucketId, intMinSize, intMaxSize) {
    WriteToDebug("CMI5_CreateDataBucket - strBucketId = " + strBucketId);
    var result;

    ClearErrorInfo();

    //
    // theoretically buckets are unique so make sure it doesn't already exist,
    // but it is possible this session hasn't laoded it yet, so make sure to
    // have fetched it first
    //
    result = CMI5_LoadBucket(strBucketId);
    if (! result) {
        WriteToDebug("Failed pre-existing bucket check, bucket can't be allocated");
        CMI5_SetErrorInfoManually(CMI5_ERROR_SSP_BUCKET_LOAD_FAILED, "Failed to create bucket: " + strBucketId, "pre-existence check load failure: " + strCMI5ErrorString + " (" + strCMI5ErrorDiagnostic + ")");
        return false;
    }

    if (typeof CMI5_SSP_BUCKETS[strBucketId] !== "undefined") {
        WriteToDebug("Bucket already exists and can't be re-allocated.");
        CMI5_SetErrorInfoManually(CMI5_ERROR_SSP_BUCKET_ALREADY_EXISTS, "Failed to create bucket: " + strBucketId, "");
        return false;
    }

    //
    // the bucket size is relatively limited and allowed to be set by the LMS,
    // an LRS is unlikely to have that small of a limit on state so we assume
    // that the max requested size is the amount that is always available
    //
    // the value stored in a bucket is always a string, so go ahead and initialize
    // it in an empty state
    //
    CMI5_SSP_BUCKETS[strBucketId] = {
        allocatedSpace: intMaxSize,
        contents: ""
    };
    return CMI5_SaveBucket(strBucketId);
}

function CMI5_GetDataFromBucket (strBucketId) {
    WriteToDebug("CMI5_GetDataFromBucket - strBucketId = " + strBucketId);
    var result;

    ClearErrorInfo();

    result = CMI5_LoadBucket(strBucketId);
    if (! result) {
        WriteToDebug("Failed to load bucket: " + strBucketId);
        CMI5_SetErrorInfoManually(CMI5_ERROR_SSP_BUCKET_LOAD_FAILED, "Cannot get data from bucket: " + strBucketId, "Failed to load bucket: " + strCMI5ErrorString + " (" + strCMI5ErrorDiagnostic + ")");
        return false;
    }

    if (typeof CMI5_SSP_BUCKETS[strBucketId] === "undefined") {
        CMI5_SetErrorInfoManually(CMI5_ERROR_UNRECOGNIZED_SSP_BUCKET, "Cannot get data from bucket: " + strBucketId, "unknown after load");
        return false;
    }

    return CMI5_SSP_BUCKETS[strBucketId].contents;
}

function CMI5_PutDataInBucket (strBucketId, strData, blnAppendToEnd) {
    WriteToDebug("CMI5_PutDataInBucket - strBucketId = " + strBucketId);
    var result;

    ClearErrorInfo();

    result = CMI5_LoadBucket(strBucketId);
    if (! result) {
        WriteToDebug("Failed to load bucket: " + strBucketId);
        CMI5_SetErrorInfoManually(CMI5_ERROR_SSP_BUCKET_LOAD_FAILED, "Cannot put data into bucket: " + strBucketId, "Failed to load bucket: " + strCMI5ErrorString + " (" + strCMI5ErrorDiagnostic + ")");
        return false;
    }

    if (typeof CMI5_SSP_BUCKETS[strBucketId] === "undefined") {
        CMI5_SetErrorInfoManually(CMI5_ERROR_UNRECOGNIZED_SSP_BUCKET, "Cannot put data into bucket: " + strBucketId, "unknown after load");
        return false;
    }

    if (blnAppendToEnd) {
        CMI5_SSP_BUCKETS[strBucketId].contents = CMI5_SSP_BUCKETS[strBucketId].contents + strData;
    }
    else {
        CMI5_SSP_BUCKETS[strBucketId].contents = strData;
    }

    return CMI5_SaveBucket(strBucketId);
}

function CMI5_GetBucketInfo (strBucketId) {
    WriteToDebug("CMI5_GetBucketInfo - strBucketId = " + strBucketId);
    var result;

    ClearErrorInfo();

    result = CMI5_LoadBucket(strBucketId);
    if (! result) {
        WriteToDebug("Failed to load bucket: " + strBucketId);
        CMI5_SetErrorInfoManually(CMI5_ERROR_SSP_BUCKET_LOAD_FAILED, "Cannot get info for bucket: " + strBucketId, "Failed to load bucket: " + strCMI5ErrorString + " (" + strCMI5ErrorDiagnostic + ")");
        return false;
    }

    if (typeof CMI5_SSP_BUCKETS[strBucketId] === "undefined") {
        CMI5_SetErrorInfoManually(CMI5_ERROR_UNRECOGNIZED_SSP_BUCKET, "Cannot get info for bucket: " + strBucketId, "unknown after load");
        return false;
    }

    return new SSPBucketSize(CMI5_SSP_BUCKETS[strBucketId].allocatedSpace, CMI5_SSP_BUCKETS[strBucketId].contents.length);
}

function CMI5_LoadBucket (strBucketId) {
    WriteToDebug("CMI5_LoadBucket - strBucketId = " + strBucketId);
    var result;

    if (typeof CMI5_SSP_BUCKETS[strBucketId] !== "undefined") {
        WriteToDebug("    already locally cached, returning");
        return true;
    }

    result = CMI5_RetrieveState(CMI5_STATE_SSP_BUCKET_PREFIX + "-" + strBucketId);

    //
    // retrieve state doesn't provide a return value for detecting errors
    // so check to see if we have manually set errors (cause we shouldn't
    // unless the retrieve failed)
    //
    if (intCMI5Error) {
        WriteToDebug("    failed to retrieve state for possible existing bucket: " + strCMI5ErrorString);
        return false;
    }

    if (result !== "" && typeof result.allocatedSpace === "undefined") {
        // bucket was found but most likely failed to parse as JSON
        // which retrieveState in TinCanJS eats as an error, but make it
        // one here
        CMI5_SetErrorInfoManually(CMI5_ERROR_INTERACTION_INVALID_JSON, "Invalid JSON for bucket in State", "result= " + result);
        return false;
    }

    if (result === "") {
        // bucket doesn't exist which is okay here,
        // just don't store anything in our local cache
        return true;
    }

    CMI5_SSP_BUCKETS[strBucketId] = result;

    WriteToDebug("    cached bucket locally");
    return true;
}

function CMI5_SaveBucket (strBucketId) {
    WriteToDebug("CMI5_SaveBucket - strBucketId = " + strBucketId);

    if (typeof CMI5_SSP_BUCKETS[strBucketId] === "undefined") {
        CMI5_SetErrorInfoManually(CMI5_ERROR_UNRECOGNIZED_SSP_BUCKET, "Cannot save unknown bucket: " + strBucketId);
        return false;
    }

    return CMI5_SaveState(CMI5_STATE_SSP_BUCKET_PREFIX + "-" + strBucketId, CMI5_SSP_BUCKETS[strBucketId], "application/json");
}

/*****************************
*  Miscellaneous
*****************************/

function CMI5_SetDurationInStatement(st) {
    var duration;

    if (CMI5_SESSION_DURATION !== null) {
        duration = CMI5_SESSION_DURATION;
    }
    else {
        duration = GetSessionAccumulatedTime();
    }

    st.result = st.result || new TinCan.Result();
    st.result.duration = TinCan.Utils.convertMillisecondsToISO8601Duration(duration);

    return;
}

function CMI5_CreateValidIdentifier (str) {
    if (str === undefined || str === "" || str === null) {
        WriteToDebug("WARNING - returning invalid identifier, invalid argument provided: " + str);
        return "";
    }

    //
    // switch URI to IRI mode temporarily, then capture the `encodeIRIComponent` function
    // off of the object for our local use, build and use our IRI and then return it
    // to its normal default state
    //
    // we need `encodeIRIComponent` to be able to encode the fragment which is otherwise
    // left alone by the URI normalization
    //
    URI.iri();

    var uri = new URI(str),
        encodeIRIComponent = URI.encode,
        id;

    if (! uri.is("absolute")) {
        //
        // getting here means it is not an absolute URI so we need to
        // manufacture one, to make it unique we'll add it to the AU's
        // Activity id
        //
        uri = new URI(cmi5.getActivity().id);
        if (uri.fragment() === "") {
            // no existing fragment, so make our identifier the fragment
            uri.hash(encodeIRIComponent(str));
        }
        else {
            // existing fragment, make our identifier part of it
            uri.hash(uri.fragment() + "-" + encodeIRIComponent(str));
        }
    }

    // .iri is a "normalizer" (calls .normalize internally)
    uri.iri();

    id = uri.toString();

    URI.unicode();

    return id;
}

function CMI5_GetComments () {
    WriteToDebug("In CMI5_GetComments");

    return CMI5_RetrieveState(CMI5_STATE_COMMENTS);
}

function CMI5_WriteComment (strComment) {
    WriteToDebug("In CMI5_WriteComment - comment: " + strComment);

    return CMI5_SaveState(CMI5_STATE_COMMENTS, strComment, "text/plain");
}

function CMI5_GetLMSComments () {
    WriteToDebug("In CMI5_GetLMSComments - cmi5 does not support LMS comments");

    ClearErrorInfo();
    CMI5_SetErrorInfoManually(CMI5_ERROR_NOT_IMPLEMENTED, "GetLMSComments not implemented in cmi5", "");

    return "";
}

function CMI5_SetNavigationRequest (strNavRequest) {
    WriteToDebug("CMI5_GetNavigationRequest - cmi5 does not support navigation requests, returning false");

    ClearErrorInfo();
    CMI5_SetErrorInfoManually(CMI5_ERROR_NOT_IMPLEMENTED, "SetNavigationRequest not implemented in cmi5", "");

    return false;
}

function CMI5_GetNavigationRequest () {
    WriteToDebug("CMI5_GetNavigationRequest - cmi5 does not support navigation requests, returning false");

    ClearErrorInfo();
    CMI5_SetErrorInfoManually(CMI5_ERROR_NOT_IMPLEMENTED, "GetNavigationRequest not implemented in cmi5", "");

    return false;
}

function CMI5_GetProgressMeasure () {
    WriteToDebug("CMI5_GetProgressMeasure - cmi5 does not support progress_measure, returning false");

    ClearErrorInfo();
    CMI5_SetErrorInfoManually(CMI5_ERROR_NOT_IMPLEMENTED, "GetProgressMeasure not implemented in cmi5", "");

    return false;
}

function CMI5_SetProgressMeasure () {
    WriteToDebug("CMI5_SetProgressMeasure - cmi5 does not support progress_measure, returning false");

    ClearErrorInfo();
    CMI5_SetErrorInfoManually(CMI5_ERROR_NOT_IMPLEMENTED, "SetProgressMeasure not implemented in cmi5", "");

    return false;
}

function CMI5_DisplayMessageOnTimeout () {
    WriteToDebug("In CMI5_DisplayMessageOnTimeout - cmi5 does not support MessageOnTimeout");

    ClearErrorInfo();
    CMI5_SetErrorInfoManually(CMI5_ERROR_NOT_IMPLEMENTED, "DisplayMessageOnTimeout not implemented in cmi5", "");

    return false;
}

function CMI5_ExitOnTimeout () {
    WriteToDebug("In CMI5_ExitOnTimeout - cmi5 does not support ExitOnTimeout");

    ClearErrorInfo();
    CMI5_SetErrorInfoManually(CMI5_ERROR_NOT_IMPLEMENTED, "ExitOnTimeout not implemented in cmi5", "");

    return false;
}

/*****************************
*  Error Management
*****************************/

function CMI5_ClearErrorInfo () {
    WriteToDebug("In CMI5_ClearErrorInfo");

    intCMI5Error = CMI5_NO_ERROR;
    strCMI5ErrorString = "";
    strCMI5ErrorDiagnostic = "";
}

function CMI5_SetErrorInfoManually (intNum, strString, strDiagnostic) {
    WriteToDebug("In CMI5_SetErrorInfoManually");
    WriteToDebug("ERROR-Num=" + intNum);
    WriteToDebug("      String=" + strString);
    WriteToDebug("      Diag=" + strDiagnostic);

    intCMI5Error = intNum;
    strCMI5ErrorString = strString;
    strCMI5ErrorDiagnostic = strDiagnostic;
}

function CMI5_GetLastError () {
    WriteToDebug("In CMI5_GetLastError");

    if (intCMI5Error === CMI5_NO_ERROR) {
        WriteToDebug("Returning No Error");
        return NO_ERROR;
    }

    WriteToDebug("Returning " + intCMI5Error);
    return intCMI5Error;
}

function CMI5_GetLastErrorDesc () {
    WriteToDebug("In CMI5_GetLastErrorDesc, " + strCMI5ErrorString + "\n" + strCMI5ErrorDiagnostic);
    return strCMI5ErrorString + "\n" + strCMI5ErrorDiagnostic;
}

function LMSStandardAPI(strStandard){

    WriteToDebug("In LMSStandardAPI strStandard=" + strStandard);

    if (strStandard == ""){
        WriteToDebug("No standard specified, using NONE");
        strStandard = "NONE";
    }

    eval ("this.Initialize = " + strStandard + "_Initialize");
    eval ("this.Finish = " + strStandard + "_Finish");
    eval ("this.CommitData = " + strStandard + "_CommitData");
    eval ("this.GetStudentID = " + strStandard + "_GetStudentID");
    eval ("this.GetStudentName = " + strStandard + "_GetStudentName");
    eval ("this.GetBookmark = " + strStandard + "_GetBookmark");
    eval ("this.SetBookmark = " + strStandard + "_SetBookmark");
    eval ("this.GetDataChunk = " + strStandard + "_GetDataChunk");
    eval ("this.SetDataChunk = " + strStandard + "_SetDataChunk");
    eval ("this.GetLaunchData = " + strStandard + "_GetLaunchData");
    eval ("this.GetComments = " + strStandard + "_GetComments");
    eval ("this.WriteComment = " + strStandard + "_WriteComment");
    eval ("this.GetLMSComments = " + strStandard + "_GetLMSComments");
    eval ("this.GetAudioPlayPreference = " + strStandard + "_GetAudioPlayPreference");
    eval ("this.GetAudioVolumePreference = " + strStandard + "_GetAudioVolumePreference");
    eval ("this.SetAudioPreference = " + strStandard + "_SetAudioPreference");
    eval ("this.SetLanguagePreference = " + strStandard + "_SetLanguagePreference");
    eval ("this.GetLanguagePreference = " + strStandard + "_GetLanguagePreference");
    eval ("this.SetSpeedPreference = " + strStandard + "_SetSpeedPreference");
    eval ("this.GetSpeedPreference = " + strStandard + "_GetSpeedPreference");
    eval ("this.SetTextPreference = " + strStandard + "_SetTextPreference");
    eval ("this.GetTextPreference = " + strStandard + "_GetTextPreference");
    eval ("this.GetPreviouslyAccumulatedTime = " + strStandard + "_GetPreviouslyAccumulatedTime");
    eval ("this.SaveTime = " + strStandard + "_SaveTime");
    eval ("this.GetMaxTimeAllowed = " + strStandard + "_GetMaxTimeAllowed");
    eval ("this.DisplayMessageOnTimeout = " + strStandard + "_DisplayMessageOnTimeout");
    eval ("this.ExitOnTimeout = " + strStandard + "_ExitOnTimeout");
    eval ("this.GetPassingScore = " + strStandard + "_GetPassingScore");
    eval ("this.SetScore = " + strStandard + "_SetScore");
    eval ("this.GetScore = " + strStandard + "_GetScore");
    eval ("this.GetScaledScore = " + strStandard + "_GetScaledScore");

    eval ("this.RecordTrueFalseInteraction = " + strStandard + "_RecordTrueFalseInteraction");
    eval ("this.RecordMultipleChoiceInteraction = " + strStandard + "_RecordMultipleChoiceInteraction");
    eval ("this.RecordFillInInteraction = " + strStandard + "_RecordFillInInteraction");
    eval ("this.RecordMatchingInteraction = " + strStandard + "_RecordMatchingInteraction");
    eval ("this.RecordPerformanceInteraction = " + strStandard + "_RecordPerformanceInteraction");
    eval ("this.RecordSequencingInteraction = " + strStandard + "_RecordSequencingInteraction");
    eval ("this.RecordLikertInteraction = " + strStandard + "_RecordLikertInteraction");
    eval ("this.RecordNumericInteraction = " + strStandard + "_RecordNumericInteraction");

    eval ("this.GetEntryMode = " + strStandard + "_GetEntryMode");
    eval ("this.GetLessonMode = " + strStandard + "_GetLessonMode");
    eval ("this.GetTakingForCredit = " + strStandard + "_GetTakingForCredit");
    eval ("this.SetObjectiveScore = " + strStandard + "_SetObjectiveScore");
    eval ("this.SetObjectiveStatus = " + strStandard + "_SetObjectiveStatus");
    eval ("this.GetObjectiveScore = " + strStandard + "_GetObjectiveScore");
    eval ("this.GetObjectiveStatus = " + strStandard + "_GetObjectiveStatus");
    eval ("this.SetObjectiveDescription = " + strStandard + "_SetObjectiveDescription");
    eval ("this.GetObjectiveDescription = " + strStandard + "_GetObjectiveDescription");
    eval ("this.SetFailed = " + strStandard + "_SetFailed");
    eval ("this.SetPassed = " + strStandard + "_SetPassed");
    eval ("this.SetCompleted = " + strStandard + "_SetCompleted");
    eval ("this.ResetStatus = " + strStandard + "_ResetStatus");
    eval ("this.GetStatus = " + strStandard + "_GetStatus");
    eval ("this.GetLastError = " + strStandard + "_GetLastError");
    eval ("this.GetLastErrorDesc = " + strStandard + "_GetLastErrorDesc");

    eval ("this.GetInteractionType = " + strStandard + "_GetInteractionType");
    eval ("this.GetInteractionTimestamp = " + strStandard + "_GetInteractionTimestamp");
    eval ("this.GetInteractionCorrectResponses = " + strStandard + "_GetInteractionCorrectResponses");
    eval ("this.GetInteractionWeighting = " + strStandard + "_GetInteractionWeighting");
    eval ("this.GetInteractionLearnerResponses = " + strStandard + "_GetInteractionLearnerResponses");
    eval ("this.GetInteractionResult = " + strStandard + "_GetInteractionResult");
    eval ("this.GetInteractionLatency = " + strStandard + "_GetInteractionLatency");
    eval ("this.GetInteractionDescription = " + strStandard + "_GetInteractionDescription");

    eval ("this.CreateDataBucket = " + strStandard + "_CreateDataBucket");
    eval ("this.GetDataFromBucket = " + strStandard + "_GetDataFromBucket");
    eval ("this.PutDataInBucket = " + strStandard + "_PutDataInBucket");
    eval ("this.DetectSSPSupport = " + strStandard + "_DetectSSPSupport");
    eval ("this.GetBucketInfo = " + strStandard + "_GetBucketInfo");

    eval ("this.GetProgressMeasure = " + strStandard + "_GetProgressMeasure");
    eval ("this.SetProgressMeasure = " + strStandard + "_SetProgressMeasure");

    eval ("this.SetPointBasedScore = " + strStandard + "_SetPointBasedScore");

    eval ("this.SetNavigationRequest = " + strStandard + "_SetNavigationRequest");
    eval ("this.GetNavigationRequest = " + strStandard + "_GetNavigationRequest");

	eval ("this.SetObjectiveProgressMeasure = " + strStandard + "_SetObjectiveProgressMeasure");
	eval ("this.GetObjectiveProgressMeasure = " + strStandard + "_GetObjectiveProgressMeasure");

	eval ("this.CreateValidIdentifier = " + strStandard + "_CreateValidIdentifier");

    if (! (typeof window[strStandard + "_ConcedeControl"] === "undefined")) {
        eval ("this.ConcedeControl = " + strStandard + "_ConcedeControl");
    }

    this.Standard = strStandard;
}
//API functions - each function validates inputs, then passes call to Standard function

//global status variables
var blnCalledFinish = false;
var blnStandAlone = false;
var blnLoaded = false;
var blnReachedEnd = false;
var blnStatusWasSet = false;
var blnLmsPresent = false;

//time tracking variables
var dtmStart = null;
var dtmEnd = null;
var intAccumulatedMS = 0;
var blnOverrodeTime = false;
var intTimeOverrideMS = null;

//debug info
var aryDebug = new Array();
var strDebug = "";
var winDebug;

var intError = NO_ERROR;
var strErrorDesc = "";

var objLMS = null;

//public
function Start(){

    var strStandAlone;
    var strShowInteractiveDebug;
    var objTempAPI = null;
    var aiccUrl = "";
    var endpoint = "";
    var cmi5FetchUrl = "";

    WriteToDebug("<h1>SCORM Driver starting up</h1>");
    WriteToDebug("----------------------------------------");
    WriteToDebug("----------------------------------------");
    WriteToDebug("In Start - Version: " + VERSION + "  Last Modified=" + window.document.lastModified);
    WriteToDebug("Browser Info (" + navigator.appName + " " + navigator.appVersion + ")");
    WriteToDebug("URL: " + window.document.location.href);
    WriteToDebug("----------------------------------------");
    WriteToDebug("----------------------------------------");

    ClearErrorInfo();

    strStandAlone           = GetQueryStringValue("StandAlone", window.location.search);
    strShowInteractiveDebug = GetQueryStringValue("ShowDebug", window.location.search);

    WriteToDebug("strStandAlone=" + strStandAlone + "  strShowInteractiveDebug=" + strShowInteractiveDebug);

    if (ConvertStringToBoolean(strStandAlone)){
        WriteToDebug("Entering Stand Alone Mode");
        blnStandAlone = true;
    }

    if (blnStandAlone){
        //allow NONE standard here to override config setting
        WriteToDebug("Using NONE Standard");
        objLMS = new LMSStandardAPI("NONE");
    }
    else{
        WriteToDebug("Standard From Configuration File - " + strLMSStandard);

        if (strLMSStandard.toUpperCase() == "AUTO") {
            //to try to overcome some unsafe javascript warnings in AUTO mode,
            //let's check for AICC first:
            WriteToDebug("Searching for recognized querystring parameters");
            aiccUrl = GetQueryStringValue("AICC_URL", document.location.search);
            endpoint = GetQueryStringValue("endpoint", document.location.search);
            cmi5FetchUrl = GetQueryStringValue("fetch", document.location.search);

            if (aiccUrl != null && aiccUrl != "") {
                WriteToDebug("Found AICC querystring parameters, using AICC");
                objLMS = new LMSStandardAPI("AICC");
                blnLmsPresent = true;
            }
            else if (endpoint != null && endpoint != "") {
                WriteToDebug("Found endpoint querystring parameter - checking cmi5 or Tin Can");
                if (cmi5FetchUrl != null && cmi5FetchUrl != "") {
                    WriteToDebug("Found fetch querystring parameter, using cmi5");
                    objLMS = new LMSStandardAPI("CMI5");
                    blnLmsPresent = true;
                }
                else {
                    WriteToDebug("Did not find fetch querystring parameter, using Tin Can");
                    objLMS = new LMSStandardAPI("TCAPI");
                    blnLmsPresent = true;

                    // overwrite this so that tc-config.js is loaded for initialization below
                    strLMSStandard = "TCAPI";
                }
            }
            else {
                WriteToDebug("Auto-detecting standard - Searching for SCORM 2004 API");
                /*
                We wrap the API grabbers in try blocks because we don't know what kind of envrionment we were
                launched in (especially in AUTO mode). The LMS may have taken away the opener/parent window (like
                the Saba preview mode does in AICC) or there could be pages from different domains to deal with.
                */
                try{
                    objTempAPI = SCORM2004_GrabAPI();
                }
                catch (e){
                    WriteToDebug("Error grabbing 2004 API-" + e.name + ":" + e.message);
                }
                if (! (typeof(objTempAPI) == "undefined" || objTempAPI == null)){
                    WriteToDebug("Found SCORM 2004 API, using SCORM 2004");
                    objLMS = new LMSStandardAPI("SCORM2004");
                    blnLmsPresent = true;
                }
                else {
                    WriteToDebug("Searching for SCORM 1.2 API");
                    try{
                        objTempAPI = SCORM_GrabAPI();
                    }
                    catch (e){
                        WriteToDebug("Error grabbing 1.2 API-" + e.name + ":" + e.message);
                    }

                    if (! (typeof(objTempAPI) == "undefined" || objTempAPI == null)) {
                        WriteToDebug("Found SCORM API, using SCORM");
                        objLMS = new LMSStandardAPI("SCORM");
                        blnLmsPresent = true;
                    }
                    else {
                        if (ALLOW_NONE_STANDARD === true) {
                            WriteToDebug("Could not determine standard, defaulting to Stand Alone");
                            objLMS = new LMSStandardAPI("NONE");
                        }
                        else {
                            WriteToDebug("Could not determine standard, Stand Alone is disabled in configuration");
                            DisplayError("Could not determine standard. SCORM, AICC, Tin Can, and CMI5 APIs could not be found");
                            return;
                        }
                    }
                }
            }
        }
        else {
            WriteToDebug("Using Standard From Configuration File - " + strLMSStandard);
            objLMS = new LMSStandardAPI(strLMSStandard);

            blnLmsPresent = true;   //set to true here and revert back to false if Initialize fails (in InitiaizeExecuted)
        }
    }

    if (ConvertStringToBoolean(strShowInteractiveDebug) ||
            (  !(typeof(SHOW_DEBUG_ON_LAUNCH) == "undefined") && SHOW_DEBUG_ON_LAUNCH === true )
    ){
        WriteToDebug("Showing Interactive Debug Windows");
        ShowDebugWindow();
    }

    WriteToDebug("Calling Standard Initialize");

    if (strLMSStandard.toUpperCase() == "TCAPI"){
        // dynamically load the tc_config.js file before calling init;
        // load from up one directory so that the content developer
        // does not need to edit anything inside of scormdriver/ and
        // this puts tc-config.js at the same level as the tincan.xml
        loadScript("../tc-config.js", objLMS.Initialize);
    }else{
        objLMS.Initialize();
    }

    TouchCloud();

    return;
}



function InitializeExecuted(blnSuccess, strErrorMessage){

    WriteToDebug("In InitializeExecuted, blnSuccess=" + blnSuccess + ", strErrorMessage=" + strErrorMessage);

    if (!blnSuccess){
        WriteToDebug("ERROR - LMS Initialize Failed");
        if (strErrorMessage == ""){
            strErrorMessage = "An Error Has Occurred";
        }
        blnLmsPresent = false;
        DisplayError(strErrorMessage);
        return;
    }

    // AICC needs to check for completion here instead of on Initialize so the GetParam has time to finish processing
    if(objLMS.Standard=='AICC'){
        AICC_InitializeExecuted();
    }

    // continue on for everyone else

    blnLoaded = true;
    dtmStart = new Date();

    LoadContent();

    return;
}


//private - no need to call this directly, use "Finish", "Suspend" or "Timeout"
function ExecFinish(ExitType){

    WriteToDebug("In ExecFinish, ExiType=" + ExitType);

    ClearErrorInfo();

    if (blnLoaded && ! blnCalledFinish ){

        WriteToDebug("Haven't called finish before, finishing");

        blnCalledFinish = true;

        if (blnReachedEnd && (!EXIT_SUSPEND_IF_COMPLETED)){
            WriteToDebug("Reached End, overiding exit type to FINISH");
            ExitType = EXIT_TYPE_FINISH;
        }

        if(EXIT_NORMAL_IF_PASSED == true && objLMS.GetStatus()==LESSON_STATUS_PASSED){
            WriteToDebug("Passed status and config value set, overiding exit type to FINISH");
            ExitType = EXIT_TYPE_FINISH;
        }

        //MR - 5/31/05 - changed this call to happen in the SetSessionTime function so we can be sure it is always called
        //if (blnOverrodeTime){
        //  WriteToDebug("Overrode Time");
        //  objLMS.SaveTime(intTimeOverrideMS);
        //}

        //DE - 5/6/2010 - we should only accumulate and save the time
        //                on exit if it wasn't set already using SetSessionTime
        if(!blnOverrodeTime){
            WriteToDebug("Did not override time");
            dtmEnd = new Date();
            AccumulateTime();
            objLMS.SaveTime(intAccumulatedMS);
        }

        blnLoaded = false;

        WriteToDebug("Calling LMS Finish");
        return objLMS.Finish(ExitType, blnStatusWasSet);

    }

    return true;
}


//Utilities

//public
//tells child frames when the API is ready
function IsLoaded(){
    WriteToDebug("In IsLoaded, returning -" + blnLoaded);
    return blnLoaded;
}


//public
function WriteToDebug(strInfo){

    if (blnDebug){

        var dtm = new Date();
        var strLine;

        strLine = aryDebug.length + ":" + dtm.toString() + " - " + strInfo;

        aryDebug[aryDebug.length] = strLine;

        if (winDebug && !winDebug.closed){
            winDebug.document.body.appendChild( winDebug.document.createTextNode(strLine) );
            winDebug.document.body.appendChild( winDebug.document.createElement("br") );
        }

    }
    return;
}

//public
function ShowDebugWindow(){
    var renderLog = function () {
            var i,
                len = aryDebug.length;

            winDebug.document.body.innerHTML = "";
            for (i = 0; i < len; i += 1) {
                winDebug.document.body.appendChild( winDebug.document.createTextNode(aryDebug[i]) );
                winDebug.document.body.appendChild( winDebug.document.createElement("br") );
            }
        };

    if (winDebug && !winDebug.closed){
        winDebug.close();
    }

    winDebug = window.open("lms/blank.html", "Debug", "width=600,height=300,resizable,scrollbars");

    if (winDebug === null) {
        alert("Debug window could not be opened, popup blocker in place?");
    }
    else {
        if (winDebug.addEventListener || winDebug.attachEvent) {
            winDebug[winDebug.addEventListener ? 'addEventListener' : 'attachEvent'](
                (winDebug.attachEvent ? 'on' : '') + 'load',
                renderLog,
                false
            );
        }

        renderLog();

        winDebug.document.close();
        winDebug.focus();
    }

    return;
}

//public
function DisplayError(strMessage){

    var blnShowDebug;

    WriteToDebug("In DisplayError, strMessage=" + strMessage);

    blnShowDebug = confirm("An error has occurred:\n\n" + strMessage + "\n\nPress 'OK' to view debug information to send to technical support.");

    if (blnShowDebug){
        ShowDebugWindow();
    }

}


//public
//combines API error information with Standard's error information
function GetLastError(){

    WriteToDebug("In GetLastError, intError=" + intError);

    if (intError != NO_ERROR){
        WriteToDebug("Returning API Error");
        return intError;
    }
    else if (IsLoaded() && objLMS.GetLastError() != NO_ERROR){
        WriteToDebug("Returning LMS Error");
        return ERROR_LMS;
    }

    WriteToDebug("Returning No Error");
    return NO_ERROR;
}

function GetLastLMSErrorCode(){

    WriteToDebug("In GetLastLMSErrorCode, intError=" + intError);

    var LMSError = objLMS.GetLastError();

    if (IsLoaded() && LMSError != NO_ERROR){


        WriteToDebug("Returning LMS Error: " + LMSError);
        return LMSError;
    }

    WriteToDebug("Returning No Error");
    return NO_ERROR;
}

//public
function GetLastErrorDesc(){
    WriteToDebug("In GetLastErrorDesc");

    if (intError != NO_ERROR){
        WriteToDebug("Returning API Error - " + strErrorDesc);
        return strErrorDesc;
    }
    else if (IsLoaded() && objLMS.GetLastError() != NO_ERROR){
        WriteToDebug("Returning LMS Error");
        return objLMS.GetLastErrorDesc();
    }

    WriteToDebug("Returning No Error");
    return "";
}

//private
function SetErrorInfo(intErrorNumToSet, strErrorDescToSet){


    WriteToDebug("In SetErrorInfo - Num=" + intErrorNumToSet + " Desc=" + strErrorDescToSet);

    intError = intErrorNumToSet;
    strErrorDesc = strErrorDescToSet;
}

//private
function ClearErrorInfo(){
    WriteToDebug("In ClearErrorInfo");

    var intError = NO_ERROR;
    var strErrorDesc = "";
}


//public
function CommitData(){
    WriteToDebug("In CommitData");

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    //MR - 5/31/05 - We want to make sure this call happens someplace other than just in the onunload event, so periodically save the data as it accumulates
    if (! blnOverrodeTime){
        WriteToDebug("Did not override time, saving incremental time");
        dtmEnd = new Date();
        AccumulateTime();
        dtmStart = new Date();
        objLMS.SaveTime(intAccumulatedMS);
    }

    return objLMS.CommitData();
}


//Finish functions

//public
function Suspend(){
    WriteToDebug("In Suspend");
    ClearErrorInfo();

    return ExecFinish(EXIT_TYPE_SUSPEND);
}

//public
function Finish(){
    WriteToDebug("In Finish");
    ClearErrorInfo();

    return ExecFinish(EXIT_TYPE_FINISH);
}

//public
function TimeOut(){
    WriteToDebug("In TimeOut");
    ClearErrorInfo();

    return ExecFinish(EXIT_TYPE_TIMEOUT);
}

//public
function Unload(){
    WriteToDebug("In Unload");
    ClearErrorInfo();

    return ExecFinish(DEFAULT_EXIT_TYPE);
}

//public
function SetReachedEnd(){
    WriteToDebug("In SetReachedEnd");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    if (blnStatusWasSet == false){
        objLMS.SetCompleted();
    }

    blnReachedEnd = true;

    return true;
}

//public
function ConcedeControl(userAction)
{
    WriteToDebug("Conceding control with type: " + EXIT_BEHAVIOR);
    ClearErrorInfo();

    //It's ok to always call Suspend in here and not Finish. The proper way to get an exit type
    //of Finish is to call SetReachedEnd before calling ConcedeControl. This will ensure that
    //the ExecFinish function always uses the ExitTypeFinish behavior (which is to set the status to completed
    //if no status was previously set)

    if (typeof objLMS.ConcedeControl !== "undefined") {
        Suspend();
        return objLMS.ConcedeControl();
    }

    if (userAction && objLMS.Standard === "AICC") {
        objLMS.actionConceded = true;
        Suspend();
        return;
    }

    Suspend();
    PerformConcedeActions();
}

function PerformConcedeActions() {
    var contentRoot = null;
    var urlBase = null;

    switch (EXIT_BEHAVIOR)
    {
        case "SCORM_RECOMMENDED":
            contentRoot = SearchParentsForContentRoot();

            if (contentRoot==window.top)
            {
                contentRoot.window.close();
            }
            else
            {
                if (contentRoot != null){
                    //DE - 10/26/2010 - Add ability to specify absolute URL for EXIT_TARGET
                    if(IsAbsoluteUrl(EXIT_TARGET)){
                        contentRoot.scormdriver_content.location.href = EXIT_TARGET;
                    } else {
                        urlBase = GetContentRootUrlBase(contentRoot);
                        contentRoot.scormdriver_content.location.href= urlBase + EXIT_TARGET;
                    }
                }
            }
            break;
        case "ALWAYS_CLOSE":
            window.close();
            break;
        case "ALWAYS_CLOSE_TOP":
            window.top.close();
            break;
        case "ALWAYS_CLOSE_PARENT":
            window.parent.close();
            break;
        case "NOTHING":
            break;
        case "REDIR_CONTENT_FRAME":
            contentRoot = SearchParentsForContentRoot();
            if (contentRoot != null){
                //DE - 10/26/2010 - Add ability to specify absolute URL for EXIT_TARGET
                if(IsAbsoluteUrl(EXIT_TARGET)){
                    contentRoot.scormdriver_content.location.href = EXIT_TARGET;
                } else {
                    urlBase = GetContentRootUrlBase(contentRoot);
                    contentRoot.scormdriver_content.location.href= urlBase + EXIT_TARGET;
                }
            }
            break;
    }

    return true;

}

function GetContentRootUrlBase(contentRoot){

    //Must remove the querystring before working with the url, or parameters that contain
    //a slash in them will break this (as with AICC_URL)
    var urlParts = contentRoot.location.href.split("?")[0].split("/");
    delete urlParts[urlParts.length - 1];
    contentRoot = urlParts.join("/");
    return contentRoot;
}

function SearchParentsForContentRoot(){

    var contentRoot = null;
    var wnd = window;
    var i=0;    //safety guard to prevent infinite loop

    if (wnd.scormdriver_content){
        contentRoot = wnd;
        return contentRoot;
    }

    while (contentRoot == null && wnd != window.top && (i++ < 100)){
        if (wnd.scormdriver_content){
            contentRoot = wnd;
            return contentRoot;
        }
        else{
            wnd = wnd.parent;
        }
    }
    WriteToDebug("Unable to locate content root");
    return null;
}


//Storing and retrieving data

//public
function GetStudentID(){
    WriteToDebug("In GetStudentID");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return "";
    }

    return objLMS.GetStudentID();

}

//public
function GetStudentName(){
    WriteToDebug("In GetStudentName");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return "";
    }

    return objLMS.GetStudentName();
}

//public
function GetBookmark(){
    WriteToDebug("In GetBookmark");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return "";
    }

    return objLMS.GetBookmark();

}

//public
function SetBookmark(strBookmark, strDesc){
    WriteToDebug("In SetBookmark - strBookmark=" + strBookmark + ", strDesc=" + strDesc);
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.SetBookmark(strBookmark, strDesc);
}

//public
function GetDataChunk(){
    WriteToDebug("In GetDataChunk");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return "";
    }

    return objLMS.GetDataChunk();
}

//public
function SetDataChunk(strData){
    WriteToDebug("In SetDataChunk strData=" + strData);
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.SetDataChunk(strData);
}

//public
function GetLaunchData(){
    WriteToDebug("In GetLaunchData");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return "";
    }

    return objLMS.GetLaunchData();
}

//public
function GetComments(){

    var strCommentString;
    var aryComments;
    var i;

    WriteToDebug("In GetComments");

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return null;
    }

    strCommentString = objLMS.GetComments();

    WriteToDebug("strCommentString=" + strCommentString);

    strCommentString = new String(strCommentString);

    if (strCommentString != ""){

        aryComments = strCommentString.split(" | ");

        for (i=0; i < aryComments.length; i++){
            WriteToDebug("Returning Comment #" + i);
            aryComments[i] = new String(aryComments[i]);
            aryComments[i] = aryComments[i].replace(/\|\|/g, "|");
            WriteToDebug("Comment #" + i + "=" + aryComments[i]);
        }
    }
    else{
        aryComments = new Array(0);
    }

    return aryComments;
}

//public
function WriteComment(strComment){

    var strExistingCommentString;

    WriteToDebug("In WriteComment strComment=" + strComment);

    ClearErrorInfo();

    strComment = new String(strComment);

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    strComment = strComment.replace(/\|/g, "||");

    strExistingCommentString = objLMS.GetComments();

    if (strExistingCommentString != "" && strExistingCommentString != 'undefined'){
        strComment = strExistingCommentString + " | " + strComment;
    }

    return objLMS.WriteComment(strComment);
}


//public
function GetLMSComments(){
    WriteToDebug("In GetLMSComments");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return "";
    }

    return objLMS.GetLMSComments();
}


//Preferences

//public
function GetAudioPlayPreference(){
    WriteToDebug("In GetAudioPlayPreference");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return PREFERENCE_DEFAULT;
    }

    return objLMS.GetAudioPlayPreference();
}

//public
//returns int 1-100
function GetAudioVolumePreference(){
    WriteToDebug("GetAudioVolumePreference");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return 100;
    }

    return objLMS.GetAudioVolumePreference();
}

//public
//percent is int 1-100
//PlayPreference is On/Off
function SetAudioPreference(PlayPreference, intPercentOfMaxVolume){

    WriteToDebug("In SetAudioPreference PlayPreference=" + PlayPreference + " intPercentOfMaxVolume=" + intPercentOfMaxVolume);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    if (PlayPreference != PREFERENCE_OFF &&
        PlayPreference != PREFERENCE_ON){

        WriteToDebug("Error Invalid PlayPreference");

        SetErrorInfo(ERROR_INVALID_PREFERENCE, "Invalid PlayPreference passed to SetAudioPreference, PlayPreference=" + PlayPreference);

        return false;
    }

    if ( ! ValidInteger(intPercentOfMaxVolume) ){
        WriteToDebug("Error Invalid PercentOfMaxVolume - not an integer");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid PercentOfMaxVolume passed to SetAudioPreference (not an integer), intPercentOfMaxVolume=" + intPercentOfMaxVolume);
        return false;
    }

    intPercentOfMaxVolume = parseInt(intPercentOfMaxVolume, 10);

    if (intPercentOfMaxVolume < 1 || intPercentOfMaxVolume > 100){
        WriteToDebug("Error Invalid PercentOfMaxVolume - out of range");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid PercentOfMaxVolume passed to SetAudioPreference (must be between 1 and 100), intPercentOfMaxVolume=" + intPercentOfMaxVolume);
        return false;
    }

    WriteToDebug("Calling to LMS");
    return objLMS.SetAudioPreference(PlayPreference, intPercentOfMaxVolume);

}

//public
function GetLanguagePreference(){
    WriteToDebug("In GetLanguagePreference");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return "";
    }

    return objLMS.GetLanguagePreference();
}

//public
function SetLanguagePreference(strLanguage){
    WriteToDebug("In SetLanguagePreference strLanguage=" + strLanguage);
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.SetLanguagePreference(strLanguage);

}

//public
function GetSpeedPreference(){
    WriteToDebug("In GetSpeedPreference");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return 100;
    }

    return objLMS.GetSpeedPreference();
}


//public
function SetSpeedPreference(intPercentOfMax){
    WriteToDebug("In SetSpeedPreference intPercentOfMax=" + intPercentOfMax);
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    if ( ! ValidInteger(intPercentOfMax) ){
        WriteToDebug("ERROR Invalid Percent of MaxSpeed, not an integer");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid PercentOfMaxSpeed passed to SetSpeedPreference (not an integer), intPercentOfMax=" + intPercentOfMax);
        return false;
    }

    intPercentOfMax = parseInt(intPercentOfMax, 10);

    if (intPercentOfMax < 0 || intPercentOfMax > 100){
        WriteToDebug("ERROR Invalid Percent of MaxSpeed, out of range");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid PercentOfMaxSpeed passed to SetSpeedPreference (must be between 1 and 100), intPercentOfMax=" + intPercentOfMax);
        return false;
    }

    WriteToDebug("Calling to LMS");
    return objLMS.SetSpeedPreference(intPercentOfMax);

}

//public
function GetTextPreference(){
    WriteToDebug("In GetTextPreference");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.GetTextPreference();
}

//public
function SetTextPreference(intPreference){
    WriteToDebug("In SetTextPreference intPreference=" + intPreference);
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    if (intPreference != PREFERENCE_DEFAULT &&
        intPreference != PREFERENCE_OFF &&
        intPreference != PREFERENCE_ON){
        WriteToDebug("Error - Invalid Preference");
        SetErrorInfo(ERROR_INVALID_PREFERENCE, "Invalid Preference passed to SetTextPreference, intPreference=" + intPreference);

        return false;
    }

    return objLMS.SetTextPreference(intPreference);

}




//Timing

//public
function GetPreviouslyAccumulatedTime(){
    WriteToDebug("In GetPreviouslyAccumulatedTime");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return 0;
    }

    return objLMS.GetPreviouslyAccumulatedTime();
}



//private
function AccumulateTime(){
    WriteToDebug("In AccumulateTime dtmStart=" + dtmStart + " dtmEnd=" + dtmEnd + " intAccumulatedMS=" + intAccumulatedMS);
    if (dtmEnd != null && dtmStart != null){
        WriteToDebug("Accumulating Time");
        intAccumulatedMS += (dtmEnd.getTime() - dtmStart.getTime());
        WriteToDebug("intAccumulatedMS=" + intAccumulatedMS);
    }

}



//public
function GetSessionAccumulatedTime(){

    WriteToDebug("In GetSessionAccumulatedTime");

    ClearErrorInfo();

    WriteToDebug("Setting dtmEnd to now");

    dtmEnd = new Date();

    WriteToDebug("Accumulating Time");

    AccumulateTime();

    if (dtmStart != null){
        WriteToDebug("Resetting dtmStart");
        dtmStart = new Date();
    }

    WriteToDebug("Setting dtmEnd to null");
    dtmEnd = null;

    WriteToDebug("Returning " + intAccumulatedMS);

    return intAccumulatedMS;
}


//public
function SetSessionTime(intMilliseconds){

    WriteToDebug("In SetSessionTime");

    ClearErrorInfo();

    if ( ! ValidInteger(intMilliseconds)){
        WriteToDebug("ERROR parameter is not an integer");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid intMilliseconds passed to SetSessionTime (not an integer), intMilliseconds=" + intMilliseconds);
        return false;
    }

    intMilliseconds = parseInt(intMilliseconds, 10);

    if (intMilliseconds < 0){
        WriteToDebug("Error, parameter is less than 0");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid intMilliseconds passed to SetSessionTime (must be greater than 0), intMilliseconds=" + intMilliseconds);
        return false;
    }

    blnOverrodeTime = true;
    intTimeOverrideMS = intMilliseconds;

    //MR - 5/31/05 - added this immediate call because in AICC the onunload call isn't always reliable
    objLMS.SaveTime(intTimeOverrideMS);

    return true;
}

//public
function PauseTimeTracking(){

    WriteToDebug("In PauseTimeTracking");

    ClearErrorInfo();

    WriteToDebug("Setting dtmEnd to now");
    dtmEnd = new Date();

    WriteToDebug("Accumulating Time");
    AccumulateTime();

    WriteToDebug("Setting Start and End times to null");
    dtmStart = null;
    dtmEnd = null;

    return true;
}

//public
//note in docs - can be used to start tracking at a point other than beginning
function ResumeTimeTracking(){

    WriteToDebug("In ResumeTimeTracking");

    ClearErrorInfo();

    WriteToDebug("Setting dtmStart to now");

    dtmStart = new Date();

    return true;

}


//public
function GetMaxTimeAllowed(){

    WriteToDebug("In GetMaxTimeAllowed");

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return MAX_CMI_TIME;
    }

    return objLMS.GetMaxTimeAllowed();

}


//public
function DisplayMessageOnTimeout(){

    WriteToDebug("In DisplayMessageOnTimeOut");

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.DisplayMessageOnTimeout();

}

//public
function ExitOnTimeout(){

    WriteToDebug("In ExitOnTimeOut");

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.ExitOnTimeout();

}


//Testing

//public
function GetPassingScore(){
    WriteToDebug("In GetPassingScore");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return 0;
    }

    return objLMS.GetPassingScore();

}

//public
function GetScore(){

    WriteToDebug("In GetScore");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return 0;
    }

    return objLMS.GetScore();

}

//public
function GetScaledScore(){

    WriteToDebug("In GetScaledScore");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return 0;
    }

    return objLMS.GetScaledScore();

}

//public
function SetScore(intScore, intMaxScore, intMinScore){

    WriteToDebug("In SetScore, intScore=" + intScore + ", intMaxScore=" + intMaxScore + ", intMinScore=" + intMinScore);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    if (! IsValidDecimal(intScore)){
        WriteToDebug("ERROR - intScore not a valid decimal");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Score passed to SetScore (not a valid decimal), intScore=" + intScore);
        return false;
    }

    if (! IsValidDecimal(intMaxScore)){
        WriteToDebug("ERROR - intMaxScore not a valid decimal");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Max Score passed to SetScore (not a valid decimal), intMaxScore=" + intMaxScore);
        return false;
    }

    if (! IsValidDecimal(intMinScore)){
        WriteToDebug("ERROR - intMinScore not a valid decimal");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Min Score passed to SetScore (not a valid decimal), intMinScore=" + intMinScore);
        return false;
    }

    WriteToDebug("Converting SCORES to floats");
    intScore = parseFloat(intScore);
    intMaxScore = parseFloat(intMaxScore);
    intMinScore = parseFloat(intMinScore);

    //check the ranges for SCORM 1.2
    if(strLMSStandard == 'SCORM')
    {
        WriteToDebug("DEBUG - SCORM 1.2 so checking max score length");
        if (intScore < 0 || intScore > 100){
            WriteToDebug("ERROR - intScore out of range");
            SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Score passed to SetScore (must be between 0-100), intScore=" + intScore);
            return false;
        }

        if (intMaxScore < 0 || intMaxScore > 100){
            WriteToDebug("ERROR - intMaxScore out of range");
            SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Max Score passed to SetScore (must be between 0-100), intMaxScore=" + intMaxScore);
            return false;
        }

        if (intMinScore < 0 || intMinScore > 100){
            WriteToDebug("ERROR - intMinScore out of range");
            SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Min Score passed to SetScore (must be between 0-100), intMinScore=" + intMinScore);
            return false;
        }
    }

    if (SCORE_CAN_ONLY_IMPROVE === true){

        var previousScore = GetScore();

        if (previousScore != null && previousScore != "" && previousScore > intScore){
            WriteToDebug("Previous score was greater than new score, configuration only allows scores to improve, returning.");
            return true;
        }
    }

    WriteToDebug("Calling to LMS");
    return objLMS.SetScore(intScore, intMaxScore, intMinScore);

}

//public
function SetPointBasedScore(intScore, intMaxScore, intMinScore){

    WriteToDebug("In SetPointBasedScore, intScore=" + intScore + ", intMaxScore=" + intMaxScore + ", intMinScore=" + intMinScore);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    if (! IsValidDecimal(intScore)){
        WriteToDebug("ERROR - intScore not a valid decimal");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Score passed to SetScore (not a valid decimal), intScore=" + intScore);
        return false;
    }

    if (! IsValidDecimal(intMaxScore)){
        WriteToDebug("ERROR - intMaxScore not a valid decimal");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Max Score passed to SetScore (not a valid decimal), intMaxScore=" + intMaxScore);
        return false;
    }

    if (! IsValidDecimal(intMinScore)){
        WriteToDebug("ERROR - intMinScore not a valid decimal");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Min Score passed to SetScore (not a valid decimal), intMinScore=" + intMinScore);
        return false;
    }

    WriteToDebug("Converting SCORES to floats");
    intScore = parseFloat(intScore);
    intMaxScore = parseFloat(intMaxScore);
    intMinScore = parseFloat(intMinScore);

    //check the ranges for SCORM 1.2
    if(strLMSStandard == 'SCORM')
    {
        if (intScore < 0 || intScore > 100){
            WriteToDebug("ERROR - intScore out of range");
            SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Score passed to SetScore (must be between 0-100), intScore=" + intScore);
            return false;
        }

        if (intMaxScore < 0 || intMaxScore > 100){
            WriteToDebug("ERROR - intMaxScore out of range");
            SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Max Score passed to SetScore (must be between 0-100), intMaxScore=" + intMaxScore);
            return false;
        }

        if (intMinScore < 0 || intMinScore > 100){
            WriteToDebug("ERROR - intMinScore out of range");
            SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Min Score passed to SetScore (must be between 0-100), intMinScore=" + intMinScore);
            return false;
        }
    }

    if (SCORE_CAN_ONLY_IMPROVE === true){

        var previousScore = GetScore();

        if (previousScore != null && previousScore != "" && previousScore > intScore){
            WriteToDebug("Previous score was greater than new score, configuration only allows scores to improve, returning.");
            return true;
        }
    }

    WriteToDebug("Calling to LMS");
    return objLMS.SetPointBasedScore(intScore, intMaxScore, intMinScore);

}


//class to hold a short and long version of an identifier for an interaction response
function CreateResponseIdentifier(strShort, strLong){
    // manipulate the short identifier into something that will work
    strShort = strShort.replace(/\W|_/g, '');
    strShort = strShort.length > 0 ? strShort.substr(0, 1) : 'z';

    //validate that the short id is a single alphanumeric character

    if (strShort.replace(" ", "") == ""){
        WriteToDebug("Short Identifier is empty");
        SetErrorInfo(ERROR_INVALID_ID, "Invalid short identifier, strShort=" + strShort);
        return false;
    }

    if (strShort.length != 1){
        WriteToDebug("ERROR - Short Identifier  not 1 character");
        SetErrorInfo(ERROR_INVALID_ID, "Invalid short identifier, strShort=" + strShort);
        return false;
    }

    if ( ! IsAlphaNumeric(strShort) ){
        WriteToDebug("ERROR - Short Identifier  not alpha numeric");
        SetErrorInfo(ERROR_INVALID_ID, "Invalid short identifier, strShort=" + strShort);
        return false;
    }

    //convert the short identifier to lower case because it is unclear in the SCORM 1.2 spec whether or not
    //the response is allowed to be upper case and some LMS's may only allow lower case
    strShort = strShort.toLowerCase();

    strLong = CreateValidIdentifier(strLong);


    return new ResponseIdentifier(strShort, strLong);
}




function ResponseIdentifier(strShort, strLong){
    this.Short = new String(strShort);
    this.Long = new String(strLong);

    this.toString = function (){
        return "[Response Identifier " + this.Short + ", " + this.Long + "]";
    };
}

//Represents a response to a matching interaction
//Contains two values, a source and a target, each of which can be a string or a ResponseIdentifier
function MatchingResponse(source, target){

    if (source.constructor == String){
        source = CreateResponseIdentifier(source, source);
    }

    if (target.constructor == String){
        target = CreateResponseIdentifier(target, target);
    }

    this.Source = source;
    this.Target = target;

    this.toString = function (){
        return "[Matching Response " + this.Source + ", " + this.Target + "]";
    };
}

function CreateMatchingResponse(pattern)
{
    var aryPairs = new Array();
    var aryEachPair = new Array();
    pattern = new String(pattern);


    aryPairs = pattern.split("[,]");

    for(var i=0; i<aryPairs.length; i++)
    {
        var thisPair = new String(aryPairs[i]);
        aryEachPair = thisPair.split("[.]");
        WriteToDebug("Matching Response [" + i + "]  source: " + aryEachPair[0] + "  target: " + aryEachPair[1]);
        aryPairs[i] = new MatchingResponse(aryEachPair[0], aryEachPair[1]);
    }

    WriteToDebug("pattern: " + pattern + " becomes " + aryPairs[0]);

    if (aryPairs.length==0) return aryPairs[0];
    else return aryPairs;
}

/**
 * Convert this string into something that would be a valid identifier in this
 * learning standard.
 */
function CreateValidIdentifier(str) {
    return objLMS.CreateValidIdentifier(str);
}

function CreateUriIdentifier(str, iri) {
    // I'm not sure this is valid, in any learning standard, but this is what
    // we had in earlier versions of driver
    if (str === undefined || str === null || str === "")
    {
        return "";
    }

    str = Trim(str);

    var uri = new URI(str);

    // note that URNs return true here
    if (!uri.is('absolute'))
    {
        // If we're here, then the string we received probably wasn't a URN. So
        // let's attempt to create one in the 'scormdriver' namespace.
        str = 'urn:scormdriver:' + encodeURIComponent(str);
        uri = new URI(str);
    }

    uri.normalize();

    if (iri) {
        uri.iri();
    }

    return uri.toString();
}

//make sure all the criteria are met to allow this value to be an identifier
function CreateValidIdentifierLegacy(str){
    if (str != null || str != "") {
        str = new String(str);
        str = Trim(str);

        //if the string starts with "urn:" then it has special requirements
        if (str.toLowerCase().indexOf("urn:") == 0) {
            str = str.substr(4);
        }

        //urns may only contain the following characters: letters, numbers - ( ) + . : = @ ; $ _ ! * ' %
        //if anything else is found, replace it with _
        str = str.replace(/[^\w\-\(\)\+\.\:\=\@\;\$\_\!\*\'\%]/g, "_");

        return str;
    }

    return "";
}

function Trim(str){
    str = str + '';
    str = str.replace(/^\s*/, "");
    str = str.replace(/\s*$/, "");
    return str;
}



function RecordTrueFalseInteraction(strID, blnResponse, blnCorrect, blnCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID){

    //clean the ID if needed
    strID = CreateValidIdentifier(strID);
    strLearningObjectiveID = CreateValidIdentifier(strLearningObjectiveID);

    WriteToDebug("In RecordTrueFalseInteraction strID=" + strID + ", blnResponse=" + blnResponse +
                    ", blnCorrect=" + blnCorrect + ", blnCorrectResponse=" + blnCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency + ", strLearningObjectiveID=" + strLearningObjectiveID);


    if ( !(typeof(DO_NOT_REPORT_INTERACTIONS) == "undefined") && DO_NOT_REPORT_INTERACTIONS === true){
        WriteToDebug("Configuration specifies interactions should not be reported, exiting.");
        return true;
    }

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    //blnResponse and blnCorrectResponse must be valid boolean values
    if ((blnResponse != true && blnResponse != false && blnResponse !== null) || (blnResponse === null && ! ALLOW_INTERACTION_NULL_LEARNER_RESPONSE)){
        SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The Response parameter must be a valid boolean value.");
        return false;
    }
    if (blnCorrectResponse != null && blnCorrectResponse != true && blnCorrectResponse != false){
        SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The Correct Response parameter must be a valid boolean value or null.");
        return false;
    }

    var dtmTime = new Date();

    WriteToDebug("Calling to LMS");
    return objLMS.RecordTrueFalseInteraction(strID, blnResponse, blnCorrect, blnCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime);
}

//public
//strResponse and strCorrectResponse can be either:
//  -a single character alphanumeric string representing the response
//  -a single ResponseIdentifier object
//  -an array of ResponseIdentifier objects representing multiple selections that must be/were made

function RecordMultipleChoiceInteraction(strID, response, blnCorrect, correctResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID){

    //clean the ID if needed
    strID = CreateValidIdentifier(strID);
    strLearningObjectiveID = CreateValidIdentifier(strLearningObjectiveID);

    WriteToDebug("In RecordMultipleChoiceInteraction strID=" + strID + ", response=" + response +
                    ", blnCorrect=" + blnCorrect + ", correctResponse=" + correctResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency + ", strLearningObjectiveID=" + strLearningObjectiveID);

    if (!(typeof(DO_NOT_REPORT_INTERACTIONS) == "undefined") && DO_NOT_REPORT_INTERACTIONS === true){
        WriteToDebug("Configuration specifies interactions should not be reported, exiting.");
        return true;
    }

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    strID = new String(strID);

    var aryResponse;
    var aryCorrectResponse;

    //translate the 3 possible argument types into to an array of ResponseIdentifier objects to be passed to the standard functions
    if (response === null) {
        if (! ALLOW_INTERACTION_NULL_LEARNER_RESPONSE) {
            SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The response is not in the correct format (null response not allowed)");
            return false;
        }

        aryResponse = null;
    }
    else if (response.constructor == String){

        aryResponse = new Array();
        var responseIdentifier = CreateResponseIdentifier(response, response);

        if (responseIdentifier == false){
            SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The response is not in the correct format");
            return false;
        }

        aryResponse[0] = responseIdentifier;
    }
    else if (response.constructor == ResponseIdentifier){
        aryResponse = new Array();
        aryResponse[0] = response;
    }
    else if (response.constructor == Array || response.constructor.toString().search("Array") > 0){     //sometimes the array constructor isn't handled correctly so we check its string
        aryResponse = response;
    }
    else if(window.console && response.constructor.toString() == '(Internal Function)' && response.length > 0){
        // we're in Safari. Safari doesn't return the Array function as a string like IE and FireFox. It does however return the ResponseIdentifier function as a string
        aryResponse = response;
    }
    else{
        if(window.console){
            window.console.log("ERROR_INVALID_INTERACTION_RESPONSE :: The response is not in the correct format.");
        }
        SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The response is not in the correct format");
        return false;
    }


    if (correctResponse != null && correctResponse != undefined && correctResponse != ""){
        if (correctResponse.constructor == String){

            aryCorrectResponse = new Array();
            responseIdentifier = CreateResponseIdentifier(correctResponse, correctResponse);

            if (responseIdentifier == false){
                SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The correct response is not in the correct format");
                return false;
            }

            aryCorrectResponse[0] = responseIdentifier;

        }
        else if (correctResponse.constructor == ResponseIdentifier){
            aryCorrectResponse = new Array();
            aryCorrectResponse[0] = correctResponse;
        }
        else if (correctResponse.constructor == Array || correctResponse.constructor.toString().search("Array") > 0){       //sometimes the array constructor isn't handled correctly so we check its string
            aryCorrectResponse = correctResponse;
        }
        else if(window.console && correctResponse.constructor.toString() == '(Internal Function)' && correctResponse.length > 0){
        // we're in Safari. Safari doesn't return the Array function as a string like IE and FireFox. It does however return the ResponseIdentifier function as a string
            aryCorrectResponse = correctResponse;
        }
        else{
            SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The correct response is not in the correct format");
            return false;
        }
    }
    else{
        aryCorrectResponse = new Array();
    }

    var dtmTime = new Date();

    WriteToDebug("Calling to LMS");
    return objLMS.RecordMultipleChoiceInteraction(strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime);


}


function RecordFillInInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID){

    //clean the ID if needed
    strID = CreateValidIdentifier(strID);
    strLearningObjectiveID = CreateValidIdentifier(strLearningObjectiveID);

    WriteToDebug("In RecordFillInInteraction strID=" + strID + ", strResponse=" + strResponse +
                    ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency + ", strLearningObjectiveID=" + strLearningObjectiveID);

    if (!(typeof(DO_NOT_REPORT_INTERACTIONS) == "undefined") && DO_NOT_REPORT_INTERACTIONS === true){
        WriteToDebug("Configuration specifies interactions should not be reported, exiting.");
        return true;
    }

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    var dtmTime = new Date();

    WriteToDebug("Calling to LMS");
    return objLMS.RecordFillInInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime);

}


//public
//strResponse and strCorrectResponse can be either:
//  -a single MatchingResponse object
//  -an array of MatchingResponse objects representing multiple selections that must be/were made

function RecordMatchingInteraction(strID, response, blnCorrect, correctResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID){

    //clean the ID if needed
    strID = CreateValidIdentifier(strID);
    strLearningObjectiveID = CreateValidIdentifier(strLearningObjectiveID);

    WriteToDebug("In RecordMatchingInteraction strID=" + strID + ", response=" + response +
                    ", blnCorrect=" + blnCorrect + ", correctResponse=" + correctResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency + ", strLearningObjectiveID=" + strLearningObjectiveID);

    if (!(typeof(DO_NOT_REPORT_INTERACTIONS) == "undefined") && DO_NOT_REPORT_INTERACTIONS === true){
        WriteToDebug("Configuration specifies interactions should not be reported, exiting.");
        return true;
    }

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    var aryResponse;
    var aryCorrectResponse;

    if (response === null) {
        if (! ALLOW_INTERACTION_NULL_LEARNER_RESPONSE) {
            SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The response is not in the correct format (null response not allowed)");
            return false;
        }

        aryResponse = null;
    }
    else if (response.constructor == MatchingResponse){
        aryResponse = new Array();
        aryResponse[0] = response;
    }
    else if (response.constructor == Array || response.constructor.toString().search("Array") > 0){     //sometimes the array constructor isn't handled correctly so we check its string
        aryResponse = response;
    }
    else if(window.console && response.constructor.toString() == '(Internal Function)' && response.length > 0){
        // we're in Safari. Safari doesn't return the Array function as a string like IE and FireFox. It does however return the ResponseIdentifier function as a string
        aryResponse = response;
    }
    else{
        SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The response is not in the correct format");
        return false;
    }

    if (correctResponse != null && correctResponse != undefined){
        if (correctResponse.constructor == MatchingResponse){
            aryCorrectResponse = new Array();
            aryCorrectResponse[0] = correctResponse;
        }
        else if (correctResponse.constructor == Array || correctResponse.constructor.toString().search("Array") > 0){       //sometimes the array constructor isn't handled correctly so we check its string
            aryCorrectResponse = correctResponse;
        }
        else if(window.console && correctResponse.constructor.toString() == '(Internal Function)' && correctResponse.length > 0){
        // we're in Safari. Safari doesn't return the Array function as a string like IE and FireFox. It does however return the ResponseIdentifier function as a string
            aryCorrectResponse = correctResponse;
        }
        else{
            SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The response is not in the correct format");
            return false;
        }
    }
    else{
        aryCorrectResponse = new Array();
    }

    var dtmTime = new Date();

    WriteToDebug("Calling to LMS");
    return objLMS.RecordMatchingInteraction(strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime);

}

function RecordPerformanceInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID){

    //clean the ID if needed
    strID = CreateValidIdentifier(strID);
    strLearningObjectiveID = CreateValidIdentifier(strLearningObjectiveID);

    WriteToDebug("In RecordPerformanceInteraction strID=" + strID + ", strResponse=" + strResponse +
                    ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency + ", strLearningObjectiveID=" + strLearningObjectiveID);

    if (!(typeof(DO_NOT_REPORT_INTERACTIONS) == "undefined") && DO_NOT_REPORT_INTERACTIONS === true){
        WriteToDebug("Configuration specifies interactions should not be reported, exiting.");
        return true;
    }

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    var dtmTime = new Date();

    WriteToDebug("Calling to LMS");
    return objLMS.RecordPerformanceInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime);
}

//public
//response and correctResponse can be either:
//  -a string representing the response
//  -a single ResponseIdentifier object
//  -an array of ResponseIdentifier objects representing multiple steps that must be/were made

function RecordSequencingInteraction(strID, response, blnCorrect, correctResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID){

    //clean the ID if needed
    strID = CreateValidIdentifier(strID);
    strLearningObjectiveID = CreateValidIdentifier(strLearningObjectiveID);

    WriteToDebug("In RecordSequencingInteraction strID=" + strID + ", response=" + response +
                    ", blnCorrect=" + blnCorrect + ", correctResponse=" + correctResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency + ", strLearningObjectiveID=" + strLearningObjectiveID);

    if (!(typeof(DO_NOT_REPORT_INTERACTIONS) == "undefined") && DO_NOT_REPORT_INTERACTIONS === true){
        WriteToDebug("Configuration specifies interactions should not be reported, exiting.");
        return true;
    }

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }


    var aryResponse;
    var aryCorrectResponse;

    //translate the 3 possible argument types into to an array of ResponseIdentifier objects to be passed to the standard functions
    if (response === null) {
        if (! ALLOW_INTERACTION_NULL_LEARNER_RESPONSE) {
            SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The response is not in the correct format (null response not allowed)");
            return false;
        }

        aryResponse = null;
    }
    else if (response.constructor == String){

        aryResponse = new Array();
        var responseIdentifier = CreateResponseIdentifier(response, response);

        if (responseIdentifier == false){
            SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The response is not in the correct format");
            return false;
        }

        aryResponse[0] = responseIdentifier;
    }
    else if (response.constructor == ResponseIdentifier){
        aryResponse = new Array();
        aryResponse[0] = response;
    }
    else if (response.constructor == Array || response.constructor.toString().search("Array") > 0){     //sometimes the array constructor isn't handled correctly so we check its string
        aryResponse = response;
    }
    else if(window.console && response.constructor.toString() == '(Internal Function)' && response.length > 0){
        // we're in Safari. Safari doesn't return the Array function as a string like IE and FireFox. It does however return the ResponseIdentifier function as a string
        aryResponse = response;
    }
    else{
        SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The response is not in the correct format");
        return false;
    }


    if (correctResponse != null && correctResponse != undefined && correctResponse != ""){
        if (correctResponse.constructor == String){

            aryCorrectResponse = new Array();
            responseIdentifier = CreateResponseIdentifier(correctResponse, correctResponse);

            if (responseIdentifier == false){
                SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The correct response is not in the correct format");
                return false;
            }

            aryCorrectResponse[0] = responseIdentifier;

        }
        else if (correctResponse.constructor == ResponseIdentifier){
            aryCorrectResponse = new Array();
            aryCorrectResponse[0] = correctResponse;
        }
        else if (correctResponse.constructor == Array || correctResponse.constructor.toString().search("Array") > 0){       //sometimes the array constructor isn't handled correctly so we check its string
            aryCorrectResponse = correctResponse;
        }
        else if(window.console && correctResponse.constructor.toString() == '(Internal Function)' && correctResponse.length > 0){
        // we're in Safari. Safari doesn't return the Array function as a string like IE and FireFox. It does however return the ResponseIdentifier function as a string
            aryCorrectResponse = correctResponse;
        }
        else{
            SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The correct response is not in the correct format");
            return false;
        }
    }
    else{
        aryCorrectResponse = new Array();
    }


    var dtmTime = new Date();

    WriteToDebug("Calling to LMS");
    return objLMS.RecordSequencingInteraction(strID, aryResponse, blnCorrect, aryCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime);

}

//public
//response and correctResponse can be either:
//  -a string representing the response
//  -a single ResponseIdentifier object

function RecordLikertInteraction(strID, response, blnCorrect, correctResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID){

    //clean the ID if needed
    strID = CreateValidIdentifier(strID);
    strLearningObjectiveID = CreateValidIdentifier(strLearningObjectiveID);

    WriteToDebug("In RecordLikertInteraction strID=" + strID + ", response=" + response +
                    ", blnCorrect=" + blnCorrect + ", correctResponse=" + correctResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency + ", strLearningObjectiveID=" + strLearningObjectiveID);

    if (!(typeof(DO_NOT_REPORT_INTERACTIONS) == "undefined") && DO_NOT_REPORT_INTERACTIONS === true){
        WriteToDebug("Configuration specifies interactions should not be reported, exiting.");
        return true;
    }

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    var riResponse;
    var riCorrectResponse;

    //translate the 3 possible argument types into to an array of ResponseIdentifier objects to be passed to the standard functions
    if (response === null) {
        if (! ALLOW_INTERACTION_NULL_LEARNER_RESPONSE) {
            SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The response is not in the correct format (null response not allowed)");
            return false;
        }

        riResponse = null;
    }
    else if (response.constructor == String){
        riResponse = CreateResponseIdentifier(response, response);
    }
    else if (response.constructor == ResponseIdentifier){
        riResponse = response;
    }
    else{
        SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The response is not in the correct format");
        return false;
    }


    if (correctResponse == null || correctResponse == undefined){
        riCorrectResponse = null;
    }
    else if (correctResponse.constructor == ResponseIdentifier){
        riCorrectResponse = correctResponse;
    }
    else if (correctResponse.constructor == String){
        riCorrectResponse = CreateResponseIdentifier(correctResponse, correctResponse);
    }
    else{
        SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE, "The response is not in the correct format");
        return false;
    }

    var dtmTime = new Date();

    WriteToDebug("Calling to LMS");
    return objLMS.RecordLikertInteraction(strID, riResponse, blnCorrect, riCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime);

}

function RecordNumericInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID){

    //clean the IDs if needed
    strID = CreateValidIdentifier(strID);
    strLearningObjectiveID = CreateValidIdentifier(strLearningObjectiveID);

    WriteToDebug("In RecordNumericInteraction strID=" + strID + ", strResponse=" + strResponse +
                    ", blnCorrect=" + blnCorrect + ", strCorrectResponse=" + strCorrectResponse +
                    ", strDescription=" + strDescription + ", intWeighting=" + intWeighting + ", intLatency=" + intLatency + ", strLearningObjectiveID=" + strLearningObjectiveID);

    if (!(typeof(DO_NOT_REPORT_INTERACTIONS) == "undefined") && DO_NOT_REPORT_INTERACTIONS === true){
        WriteToDebug("Configuration specifies interactions should not be reported, exiting.");
        return true;
    }

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    if ((strResponse === null && ! ALLOW_INTERACTION_NULL_LEARNER_RESPONSE) || (strResponse !== null && ! IsValidDecimal(strResponse))){
        WriteToDebug("ERROR - Invalid Response, not a valid decmial");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Response passed to RecordNumericInteraction (not a valid decimal), strResponse=" + strResponse);
        return false;
    }

    var dtmTime = new Date();

    WriteToDebug("Calling to LMS");
    return objLMS.RecordNumericInteraction(strID, strResponse, blnCorrect, strCorrectResponse, strDescription, intWeighting, intLatency, strLearningObjectiveID, dtmTime);
}


//State Functions

//public
function GetStatus(){

    WriteToDebug("In GetStatus");

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return LESSON_STATUS_INCOMPLETE;
    }

    return objLMS.GetStatus();

}

//public
function ResetStatus(){
    WriteToDebug("In ResetStatus");

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    WriteToDebug("Setting blnStatusWasSet to false");

    blnStatusWasSet = false;

    return objLMS.ResetStatus();
}

//public
function GetProgressMeasure(){
    WriteToDebug("In GetProgressMeasure");

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return LESSON_STATUS_INCOMPLETE;
    }

    return objLMS.GetProgressMeasure();
}
//public
function SetProgressMeasure(numMeasure){
    WriteToDebug("In SetProgressMeasure, passing in: "+ numMeasure);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return LESSON_STATUS_INCOMPLETE;
    }

    return objLMS.SetProgressMeasure(numMeasure);
}

//public
//optional to call
function SetPassed(){
    WriteToDebug("In SetPassed");

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    WriteToDebug("Setting blnStatusWasSet to true");

    blnStatusWasSet = true;

    return objLMS.SetPassed();

}

//public
function SetFailed(){
    WriteToDebug("In SetFailed");

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    WriteToDebug("Setting blnStatusWasSet to true");

    blnStatusWasSet = true;

    return objLMS.SetFailed();

}


//public
function GetEntryMode(){
    WriteToDebug("In GetEntryMode");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return ENTRY_FIRST_TIME;
    }

    return objLMS.GetEntryMode();

}

//public
function GetLessonMode(){
    WriteToDebug("In GetLessonMode");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return MODE_NORMAL;
    }

    return objLMS.GetLessonMode();

}

//public
function GetTakingForCredit(){
    WriteToDebug("In GetTakingForCredit");
    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.GetTakingForCredit();

}


//Objectives

//public
function SetObjectiveScore(strObjectiveID, intScore, intMaxScore, intMinScore){

    WriteToDebug("In SetObjectiveScore, intObjectiveID=" + strObjectiveID + ", intScore=" + intScore + ", intMaxScore=" + intMaxScore + ", intMinScore=" + intMinScore);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    strObjectiveID = new String(strObjectiveID);
    if (strObjectiveID.replace(" ", "") == ""){
        WriteToDebug("ERROR - Invalid ObjectiveID, empty string");
        SetErrorInfo(ERROR_INVALID_ID, "Invalid ObjectiveID passed to SetObjectiveScore (must have a value), strObjectiveID=" + strObjectiveID);
        return false;
    }

    if (! IsValidDecimal(intScore)){
        WriteToDebug("ERROR - Invalid Score, not a valid decmial");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Score passed to SetObjectiveScore (not a valid decimal), intScore=" + intScore);
        return false;
    }

    if (! IsValidDecimal(intMaxScore)){
        WriteToDebug("ERROR - Invalid Max Score, not a valid decmial");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Max Score passed to SetObjectiveScore (not a valid decimal), intMaxScore=" + intMaxScore);
        return false;
    }

    if (! IsValidDecimal(intMinScore)){
        WriteToDebug("ERROR - Invalid Min Score, not a valid decmial");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Min Score passed to SetObjectiveScore (not a valid decimal), intMinScore=" + intMinScore);
        return false;
    }

    WriteToDebug("Converting Scores to floats");
    intScore = parseFloat(intScore);
    intMaxScore = parseFloat(intMaxScore);
    intMinScore = parseFloat(intMinScore);

    if (intScore < 0 || intScore > 100){
        WriteToDebug("ERROR - Invalid Score, out of range");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Score passed to SetObjectiveScore (must be between 0-100), intScore=" + intScore);
        return false;
    }

    if (intMaxScore < 0 || intMaxScore > 100){
        WriteToDebug("ERROR - Invalid Max Score, out of range");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Max Score passed to SetObjectiveScore (must be between 0-100), intMaxScore=" + intMaxScore);
        return false;
    }

    if (intMinScore < 0 || intMinScore > 100){
        WriteToDebug("ERROR - Invalid Min Score, out of range");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Min Score passed to SetObjectiveScore (must be between 0-100), intMinScore=" + intMinScore);
        return false;
    }

    WriteToDebug("Calling To LMS");
    return objLMS.SetObjectiveScore(strObjectiveID, intScore, intMaxScore, intMinScore);

}

//public
function SetObjectiveStatus(strObjectiveID, Lesson_Status){

    WriteToDebug("In SetObjectiveStatus strObjectiveID=" + strObjectiveID + ", Lesson_Status=" + Lesson_Status);

    ClearErrorInfo();


    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }


    strObjectiveID = new String(strObjectiveID);
    if (strObjectiveID.replace(" ", "") == ""){
        WriteToDebug("ERROR - Invalid ObjectiveID, empty string");
        SetErrorInfo(ERROR_INVALID_ID, "Invalid ObjectiveID passed to SetObjectiveStatus (must have a value), strObjectiveID=" + strObjectiveID);
        return false;
    }

    if (
       (Lesson_Status != LESSON_STATUS_PASSED) &&
       (Lesson_Status != LESSON_STATUS_COMPLETED) &&
       (Lesson_Status != LESSON_STATUS_FAILED) &&
       (Lesson_Status != LESSON_STATUS_INCOMPLETE) &&
       (Lesson_Status != LESSON_STATUS_BROWSED) &&
       (Lesson_Status != LESSON_STATUS_NOT_ATTEMPTED)
       ){
        WriteToDebug("ERROR - Invalid Status");
        SetErrorInfo(ERROR_INVALID_STATUS, "Invalid status passed to SetObjectiveStatus, Lesson_Status=" + Lesson_Status);
        return false;

    }

    WriteToDebug("Calling To LMS");
    return objLMS.SetObjectiveStatus(strObjectiveID, Lesson_Status);


}


//public
function GetObjectiveStatus(strObjectiveID){

    WriteToDebug("In GetObjectiveStatus, strObjectiveID=" + strObjectiveID);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.GetObjectiveStatus(strObjectiveID);
}




//public
function SetObjectiveDescription(strObjectiveID, strObjectiveDescription){

    WriteToDebug("In SetObjectiveDescription strObjectiveID=" + strObjectiveID + ", strObjectiveDescription=" + strObjectiveDescription);

    ClearErrorInfo();


    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }


    strObjectiveID = new String(strObjectiveID);
    if (strObjectiveID.replace(" ", "") == ""){
        WriteToDebug("ERROR - Invalid ObjectiveID, empty string");
        SetErrorInfo(ERROR_INVALID_ID, "Invalid ObjectiveID passed to SetObjectiveStatus (must have a value), strObjectiveID=" + strObjectiveID);
        return false;
    }


    //include validation for string length?

    WriteToDebug("Calling To LMS");
    return objLMS.SetObjectiveDescription(strObjectiveID, strObjectiveDescription);


}


//public
function GetObjectiveDescription(strObjectiveID){

    WriteToDebug("In GetObjectiveDescription, strObjectiveID=" + strObjectiveID);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.GetObjectiveDescription(strObjectiveID);
}





//public
function GetObjectiveScore(strObjectiveID){

    WriteToDebug("In GetObjectiveScore, strObjectiveID=" + strObjectiveID);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.GetObjectiveScore(strObjectiveID);
}


//public
function IsLmsPresent(){
    return blnLmsPresent;
}

//public
function SetObjectiveProgressMeasure(strObjectiveID, strObjectiveProgressMeasure){

    WriteToDebug("In SetObjectiveProgressMeasure strObjectiveID=" + strObjectiveID + ", strObjectiveProgressMeasure=" + strObjectiveProgressMeasure);

    ClearErrorInfo();


    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }


    strObjectiveID = new String(strObjectiveID);
    if (strObjectiveID.replace(" ", "") == ""){
        WriteToDebug("ERROR - Invalid ObjectiveID, empty string");
        SetErrorInfo(ERROR_INVALID_ID, "Invalid ObjectiveID passed to SetObjectiveProgressMeasure (must have a value), strObjectiveID=" + strObjectiveID);
        return false;
    }


    //include validation for string length?

    WriteToDebug("Calling To LMS");
    return objLMS.SetObjectiveProgressMeasure(strObjectiveID, strObjectiveProgressMeasure);


}


//public
function GetObjectiveProgressMeasure(strObjectiveID){

    WriteToDebug("In GetObjectiveProgressMeasure, strObjectiveID=" + strObjectiveID);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.GetObjectiveProgressMeasure(strObjectiveID);
}


//public
function SetNavigationRequest(strNavRequest){
    WriteToDebug("In SetNavigationRequest");

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.SetNavigationRequest(strNavRequest);
}
//public
function GetNavigationRequest(){
    WriteToDebug("In GetNavigationRequest");

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.GetNavigationRequest();
}

//NOTE ON INTERACTION RETRIEVAL
//A.  It is only available in certain standards, standards where it is unavailable will return nothing
//B.  The interaction records are currently reported using "journaling", whereby each entry is appended
//      Retrieval methods will retrieve only the most recent value
//public
function GetInteractionType(strInteractionID)
{
    // Clean the ID if needed
    strInteractionID = CreateValidIdentifier(strInteractionID);

    WriteToDebug("In GetInteractionType, strInteractionID=" + strInteractionID);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.GetInteractionType(strInteractionID);
}

//public
function GetInteractionTimestamp(strInteractionID)
{
    // Clean the ID if needed
    strInteractionID = CreateValidIdentifier(strInteractionID);

    WriteToDebug("In GetInteractionTimestamp, strInteractionID=" + strInteractionID);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.GetInteractionTimestamp(strInteractionID);
}



//public
function GetInteractionCorrectResponses(strInteractionID)
{
    // Clean the ID if needed
    strInteractionID = CreateValidIdentifier(strInteractionID);

    WriteToDebug("In GetInteractionCorrectResponses, strInteractionID=" + strInteractionID);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.GetInteractionCorrectResponses(strInteractionID);
}



//public
function GetInteractionWeighting(strInteractionID)
{
    // Clean the ID if needed
    strInteractionID = CreateValidIdentifier(strInteractionID);

    WriteToDebug("In GetInteractionWeighting, strInteractionID=" + strInteractionID);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.GetInteractionWeighting(strInteractionID);
}



//public
function GetInteractionLearnerResponses(strInteractionID)
{
    // Clean the ID if needed
    strInteractionID = CreateValidIdentifier(strInteractionID);

    WriteToDebug("In GetInteractionLearnerResponses, strInteractionID=" + strInteractionID);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.GetInteractionLearnerResponses(strInteractionID);
}



//public
function GetInteractionResult(strInteractionID)
{
    // Clean the ID if needed
    strInteractionID = CreateValidIdentifier(strInteractionID);

    WriteToDebug("In GetInteractionResult, strInteractionID=" + strInteractionID);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.GetInteractionResult(strInteractionID);
}



//public
function GetInteractionLatency(strInteractionID)
{
    // Clean the ID if needed
    strInteractionID = CreateValidIdentifier(strInteractionID);

    WriteToDebug("In GetInteractionLatency, strInteractionID=" + strInteractionID);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.GetInteractionLatency(strInteractionID);
}



//public
function GetInteractionDescription(strInteractionID)
{
    // Clean the ID if needed
    strInteractionID = CreateValidIdentifier(strInteractionID);

    WriteToDebug("In GetInteractionDescription, strInteractionID=" + strInteractionID);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    return objLMS.GetInteractionDescription(strInteractionID);
}


//public
/*
Bucket sizes are listed in characters. In SCORM, all transmitted data is represented as JavaScript strings.
JavaScript strings use two bytes for each character so the actual size (in bytes) of what is allocated will be
double the value that is passed to this function.
*/
function CreateDataBucket(strBucketId, intMinSize, intMaxSize){

    WriteToDebug("In CreateDataBucket, strBucketId=" + strBucketId + ", intMinSize=" + intMinSize + ", intMaxSize=" + intMaxSize);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    strBucketId = new String(strBucketId);
    if (strBucketId.replace(" ", "") == ""){
        WriteToDebug("ERROR - Invalid BucketId, empty string");
        SetErrorInfo(ERROR_INVALID_ID, "Invalid strBucketId passed to CreateDataBucket (must have a value), strBucketId=" + strBucketId);
        return false;
    }

    if ( ! ValidInteger(intMinSize) ){
        WriteToDebug("ERROR Invalid Min Size, not an integer");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid intMinSize passed to CreateDataBucket (not an integer), intMinSize=" + intMinSize);
        return false;
    }

    if ( ! ValidInteger(intMaxSize) ){
        WriteToDebug("ERROR Invalid Max Size, not an integer");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid intMaxSize passed to CreateDataBucket (not an integer), intMaxSize=" + intMaxSize);
        return false;
    }

    intMinSize = parseInt(intMinSize, 10);
    intMaxSize = parseInt(intMaxSize, 10);

    if (intMinSize < 0){
        WriteToDebug("ERROR Invalid Min Size, must be greater than or equal to 0");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Min Size passed to CreateDataBucket (must be greater than or equal to 0), intMinSize=" + intMinSize);
        return false;
    }

    if (intMaxSize <= 0){
        WriteToDebug("ERROR Invalid Max Size, must be greater than 0");
        SetErrorInfo(ERROR_INVALID_NUMBER, "Invalid Max Size passed to CreateDataBucket (must be greater than 0), intMaxSize=" + intMaxSize);
        return false;
    }

    //need to double the values to get from "characters" (the representation exposed by this API) to "octets" the value used by SSP
    intMinSize = (intMinSize * 2);
    intMaxSize = (intMaxSize * 2);

    return objLMS.CreateDataBucket(strBucketId, intMinSize, intMaxSize);
}

//public
function GetDataFromBucket(strBucketId){

    WriteToDebug("In GetDataFromBucket, strBucketId=" + strBucketId);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    strBucketId = new String(strBucketId);
    if (strBucketId.replace(" ", "") == ""){
        WriteToDebug("ERROR - Invalid BucketId, empty string");
        SetErrorInfo(ERROR_INVALID_ID, "Invalid strBucketId passed to GetDataFromBucket (must have a value), strBucketId=" + strBucketId);
        return false;
    }

    return objLMS.GetDataFromBucket(strBucketId);
}

//public
function PutDataInBucket(strBucketId, strData, blnAppendToEnd){

    WriteToDebug("In PutDataInBucket, strBucketId=" + strBucketId + ", blnAppendToEnd=" + blnAppendToEnd + ", strData=" + strData);

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    strBucketId = new String(strBucketId);
    if (strBucketId.replace(" ", "") == ""){
        WriteToDebug("ERROR - Invalid BucketId, empty string");
        SetErrorInfo(ERROR_INVALID_ID, "Invalid strBucketId passed to PutDataInBucket (must have a value), strBucketId=" + strBucketId);
        return false;
    }

    if (blnAppendToEnd != true){
        WriteToDebug("blnAppendToEnd was not explicitly true so setting it to false, blnAppendToEnd=" + blnAppendToEnd);
        blnAppendToEnd = false;
    }

    return objLMS.PutDataInBucket(strBucketId, strData, blnAppendToEnd);
}

//public
function DetectSSPSupport(){
    return objLMS.DetectSSPSupport();
}

//public returns a SSPBucketSize object containing information about the allocated size of the specified bucket
function GetBucketInfo(strBucketId){

    WriteToDebug("In GetBucketInfo, strBucketId=" + strBucketId );

    ClearErrorInfo();

    if (! IsLoaded()){
        SetErrorInfo(ERROR_NOT_LOADED, "Cannot make calls to the LMS before calling Start");
        return false;
    }

    strBucketId = new String(strBucketId);
    if (strBucketId.replace(" ", "") == ""){
        WriteToDebug("ERROR - Invalid BucketId, empty string");
        SetErrorInfo(ERROR_INVALID_ID, "Invalid strBucketId passed to GetBucketInfo (must have a value), strBucketId=" + strBucketId);
        return false;
    }

    var bucketInfo = objLMS.GetBucketInfo(strBucketId);

    //bucket size needs to be halfed to translate from octets to characters
    bucketInfo.TotalSpace = (bucketInfo.TotalSpace / 2);
    bucketInfo.UsedSpace = (bucketInfo.UsedSpace / 2);

    WriteToDebug("GetBucketInfo returning " + bucketInfo );

    return bucketInfo;
}


//Represents the size of an SSP Bucket
//Contains two values, a total space and used space, each of which is represented in characters
function SSPBucketSize(totalSpace, usedSpace){

    this.TotalSpace = totalSpace;
    this.UsedSpace = usedSpace;

    this.toString = function (){
        return "[SSPBucketSize " + this.TotalSpace + ", " + this.UsedSpace + "]";
    };
}
