/*--
Copyright © 1998, 2012, Oracle and/or its affiliates.  All rights reserved.
--*/

/* Constant for the different statuses that a coarse may have.  
   These ID corresponsed to the values used by both SCORM and 
   AICC. */ 
var LMS_PASSED = "passed";
var LMS_FAILED = "failed";
var LMS_COMPLETED = "completed";
var LMS_INCOMPLETE = "incomplete";
var LMS_NOT_ATTEMPTED = "not attempted";

/* Extend internal Date object to format for SCORM 2004 */
Date.prototype.toSCORM = function() {
    return this.getFullYear() + "-" + ("0"+(this.getMonth()+1)).slice(-2) + "-" + ("0"+this.getDate()).slice(-2)
        + "T" + ("0"+this.getHours()).slice(-2) + ":" + ("0"+this.getMinutes()).slice(-2) + ":" + ("0"+this.getSeconds()).slice(-2);
}

///////////////////////////////////////
//
// LmsCom object defintions
//
///////////////////////////////////////

///////////////////////////////////////
// Base LmsCom object defintion
///////////////////////////////////////
function LmsComBase(parent) {
    this.status_obj = null;
    this.ordinal_number = 0; 
    
    this.start_date = "";
    this.end_date = "";
    this.student_id = "";
    this.student_name = "";
    this.credit = "";
    this.lesson_status = LMS_NOT_ATTEMPTED;
    this.lesson_flag = "";
    this.lesson_location = "";
    this.lesson_data = "";
    this.original_lesson_data = "";
    this.core_vendor = "";
    this.score = "";
    this.time = "";
    this.idle_time = 0; // idle time only computed for lmscomkpath
    
    this.owner = null;  // owner window of this lmscom
    this.guid = null;   // guid of associated asset

    this.session_active = false;
    this.bigsco = false;    // executing in a big sco, so asset status stored differently

    if (parent != null) {
        this.Copy(parent);
        this.ParentLmsCom = parent; 
    } 

    this.object_type_name = "LmsComBase";
}

LmsComBase.prototype.GetOrdinalNumber = function() {
    return this.ordinal_number;
}

LmsComBase.prototype.SetOrdinalNumber = function(ordnum) {
    this.ordinal_number = ordnum;
}

/* Copies just that invarient information.  Allows child 
   pages to report on student info if they need to. */ 
LmsComBase.prototype.Copy = function(from) {

    this.student_id = from.student_id;
    this.student_name = from.student_name;
    this.credit = from.credit;
    this.lesson_status = from.lesson_status;
    this.bigsco = from.bigsco;
    this.lesson_data = from.lesson_data;
    this.lesson_location = from.lesson_location;
}

/* Starts a session.  For LmsComBase, all this does is
   records the start time. */ 
LmsComBase.prototype.Begin = function() {
    this.start_date = new Date().getTime();

    if (this.session_active)
        alert(R_lmscom_already_active);

    this.session_active = true; 
       
    return true;
}

/* Ends a session.  For LmsComBase, all this does is
   records the end time. */ 
LmsComBase.prototype.End = function() {

    if (!this.session_active)
        alert(R_lmscom_inactive);

    this.end_date = new Date().getTime();
    this.session_active = false;
 
    return true;
}

/* Load the SCORM API adapter if needed and save reference to it. */
LmsComBase.prototype.LoadAPIAdapter = function (callback) {
    //setTimeout(callback,1);
    // Above timeout caused mode launches to hang on ipad. Apparently it isn't cross-tab
    // execution that is blocked, but only the timer thread in inactive tabs.
    callback();
}

/* Sends completion status to the LMS. This is factored out of LmsCom.End() so
    that completion information may be sent immediately to allow the LMS to
    update any visual feedback. */
LmsComBase.prototype.SendCompletionInfo = function() {}

/* Notifies parent lmscom to roll up the status of this lmscom */
LmsComBase.prototype.NotifyParent = function() {
    var parentlmscom = this.ParentLmsCom;
    if (parentlmscom && !parentlmscom.owner.closed && parentlmscom.owner.ListenChildClose)
        parentlmscom.owner.ListenChildClose();
}

LmsComBase.prototype.getReportedLessonStatus = function() {
    if (this.lesson_status === LMS_PASSED)
        return LmsConfig.ReportOnPass;
    else if (this.lesson_status === LMS_FAILED)
        return LmsConfig.ReportOnFail;
    else
        return this.lesson_status;
}
/* Internal helper method that computes elapse time in 
   seconds. */
LmsComBase.prototype.computeTime = function(bdate, edate) {
    var elapsedSeconds = ((edate - bdate - this.idle_time) / 1000);
    return this.convertTotalSeconds(elapsedSeconds);
}

/* Internal helper method that converts date/time into
   seconds. */
LmsComBase.prototype.convertTotalSeconds = function(ts) {
    var sec = (ts % 60);
    ts -= sec;
    var tmp = (ts % 3600);  //# of seconds in the total # of minutes
    ts -= tmp;              //# of seconds in the total # of hours
    // convert seconds to conform to CMITimespan type (e.g. SS.00)
    sec = Math.round(sec * 100) / 100;
    var strSec = new String(sec);
    var strWholeSec = strSec;
    var strFractionSec = "";
    if (strSec.indexOf(".") != -1) {
        strWholeSec = strSec.substring(0, strSec.indexOf("."));
        strFractionSec = strSec.substring(strSec.indexOf(".") + 1, strSec.length);
    }
    if (strWholeSec.length < 2) {
        strWholeSec = "0" + strWholeSec;
    }
    strSec = strWholeSec;
    if ((ts % 3600) != 0)
        var hour = 0;
    else var hour = (ts / 3600);
    if ((tmp % 60) != 0)
        var min = 0;
    else var min = (tmp / 60);
    if ((new String(hour)).length < 2)
        hour = "0" + hour;
    if ((new String(min)).length < 2)
        min = "0" + min;
    var rtnVal = hour + ":" + min + ":" + strSec;
    return rtnVal;
}

// Get current bookmark. This is only needed for a TOC that is called via lmstart
// but not from an LMS. If you open a Big SCO's concept from kp.html and launch
// it, the Big SCO toc will get an lmscombase from the lmstart, but the lmstart
// page doesn't have the toc_hash, so it can't get the bookmark.
// Note that "view outline" from a normal concept does not need this. It replaces
// the lmstart page with the toc, so the toc will end up with an lmscomcookie of
// its own.
LmsComBase.prototype.GetBookmark = function(toc_hash) {
    if (this.ParentLmsCom)
        return this.ParentLmsCom.GetBookmark(toc_hash);
    return 0;
}

// Support for UPK specific functionality. Delegate to parent until an lmscomkpath is found
LmsComBase.prototype.doGetUPKValue = function(name) {
    if (this.ParentLmsCom)
        return this.ParentLmsCom.doGetUPKValue(name);
    return "";
}

LmsComBase.prototype.doSetUPKValue = function(name, value) {
    if (this.ParentLmsCom)
        this.ParentLmsCom.doSetUPKValue(name, value);
}

LmsComBase.prototype.doUPKCommit = function() {
    if (this.ParentLmsCom)
        this.ParentLmsCom.doUPKCommit();
}

/* Record an interaction completion. Only an lmscomkpath will actually do this, so
   a question in the outline will delegate to its parent lmscom (from lmstart). */
LmsComBase.prototype.setInteraction = function(index,qlmscom) {
    if (this.ParentLmsCom)
        return this.ParentLmsCom.setInteraction(index,qlmscom);
}
LmsComBase.prototype.getAnsweredQuestions = function() {
    if (this.ParentLmsCom)
        return this.ParentLmsCom.getAnsweredQuestions();
    return {};
}

LmsComBase.prototype.setObjective = function(index, objective) {
    if (this.ParentLmsCom)
        this.ParentLmsCom.setObjective(index, objective);
}

// sequencing at end of pre/post assessment
LmsComBase.prototype.requestContinue = function() {
    if (this.ParentLmsCom)
        this.ParentLmsCom.requestContinue();
}

/* Indicate if LMS connection is live */
LmsComBase.prototype.IsConnected = function() {
    if (this.ParentLmsCom)
        return this.ParentLmsCom.IsConnected();
    return false;
}

/* This method reads persisted data from LmsCom 
  (primary from lesson_data) and creates a LmsBasicStatus
  object from that. */ 
LmsComBase.prototype.OpenBasicStatus = function() {
    this.status_obj = new LmsBasicStatus();
    this.status_obj.status = this.lesson_status;

    return this.status_obj;
}

/* This method reads persisted data from LmsCom 
   (primary from lesson_data) and creates a LmsTopicStatus
   object from that. */ 
LmsComBase.prototype.OpenTopicStatus = function(lmsModes, playerModes) {
    var fname = "OpenTopicStatus";
    this.status_obj = new LmsTopicStatus();
    this.status_obj.setRequiredMode(lmsModes, playerModes);
    lmslog.addEntry(0,fname,"RequiredMode",this.status_obj.requiredMode);
    this.status_obj.isScored = !!lmsModes["K"];
    
    var availableModes;
    if (GetTopLevelLmsMode() == "LMS")
        availableModes = lmsModes;
    else
        availableModes = playerModes;
    this.status_obj.setModeavail("S", availableModes["S"]);
    this.status_obj.setModeavail("T", availableModes["T"]);
    this.status_obj.setModeavail("K", availableModes["K"]);
    this.status_obj.setModeavail("D", availableModes["D"]);
    this.status_obj.setModeavail("P", availableModes["P"]);
    this.status_obj.setModeavail("E", availableModes["E"]);
    lmslog.addEntry(0, fname, "SeeIt", this.status_obj.getModeavail("S"));
    lmslog.addEntry(0, fname, "TryIt", this.status_obj.getModeavail("T"));
    lmslog.addEntry(0, fname, "KnowIt", this.status_obj.getModeavail("K"));
    lmslog.addEntry(0, fname, "DoIt", this.status_obj.getModeavail("D"));
    lmslog.addEntry(0, fname, "PrintIt", this.status_obj.getModeavail("P"));
    lmslog.addEntry(0, fname, "TestIt", this.status_obj.getModeavail("E"));

    if (this.bigsco) {
        this.status_obj.savedStatus = this.lesson_data.charAt(0);
        this.lesson_data = this.lesson_data.substring(1);
    }
    this.status_obj.initTopicstat(this.lesson_data);
        
    return this.status_obj;
}

// duplicates code for TopicStatus, except available modes are stored differently in
// topic.html than in lmstart.html
LmsComBase.prototype.OpenTopicLaunchStatus = function(mode, lmsModes, playerModes) {
    var fname = "OpenTopicLaunchStatus";

    this.status_obj = new LmsTopicLaunchStatus(mode);
    this.status_obj.setRequiredMode(lmsModes, playerModes);
    lmslog.addEntry(0,fname,"RequiredMode",this.status_obj.requiredMode);
    this.status_obj.isScored = lmsModes.indexOf("K")!=-1;

    var availableModes;
    if (GetTopLevelLmsMode() == "LMS")
        availableModes = lmsModes;
    else
        availableModes = playerModes;
    this.status_obj.setModeavail("S", availableModes.indexOf("S")!=-1);
    this.status_obj.setModeavail("T", availableModes.indexOf("T")!=-1);
    this.status_obj.setModeavail("K", availableModes.indexOf("K")!=-1);
    this.status_obj.setModeavail("D", availableModes.indexOf("D")!=-1);
    this.status_obj.setModeavail("P", availableModes.indexOf("P")!=-1);
    this.status_obj.setModeavail("E", availableModes.indexOf("E")!=-1);
    lmslog.addEntry(0, fname, "SeeIt", this.status_obj.getModeavail("S"));
    lmslog.addEntry(0, fname, "TryIt", this.status_obj.getModeavail("T"));
    lmslog.addEntry(0, fname, "KnowIt", this.status_obj.getModeavail("K"));
    lmslog.addEntry(0, fname, "DoIt", this.status_obj.getModeavail("D"));
    lmslog.addEntry(0, fname, "PrintIt", this.status_obj.getModeavail("P"));
    lmslog.addEntry(0, fname, "TestIt", this.status_obj.getModeavail("E"));

    this.status_obj.initTopicstat(this.lesson_data);
        
    return this.status_obj;
}

/* This method reads persisted data from LmsCom 
   (primary from lesson_data) and creates a 
   LmsQuestionStatus object from that. */ 
LmsComBase.prototype.OpenQuestionStatus = function() {
    this.status_obj = new LmsQuestionStatus();
    this.status_obj.status = LMS_NOT_ATTEMPTED;
    this.status_obj.savedStatus = this.lesson_data ? Stat2Lms(this.lesson_data) : LMS_NOT_ATTEMPTED;
    
    return this.status_obj;
}

/* This method reads persisted data from LmsCom 
   (primary from lesson_data) and creates a 
   LmsAssessmentStatus object from that. */
LmsComBase.prototype.OpenAssessmentStatus = function(itemCount, hash) {
    this.status_obj = new LmsAssessmentStatus();
    this.status_obj.hash = hash;    // save hash for when saving

    var str = this.lesson_data;
    var count;
    var savedhash = null;
    
    if (str) {
        if (!this.bigsco) {
            var index = str.indexOf("@");
            if (index > 0) {
                count = parseInt(str.substring(0, index));
                // if aicc lms (moodle) replaced "+" in hash with " ", put "+" back
                savedhash = str.substring(index+1, index+count+1).replace(/ /g,"+");
                str = str.substring(index+count+1);
            }
        } else
            savedhash = hash;   // to satisfy test below
    }
    
    if (hash === savedhash && str.length === itemCount+1) {         
        this.status_obj.savedStatus = str.charAt(0);
        this.status_obj.status = str.charAt(0) === "I" ? LMS_INCOMPLETE : LMS_NOT_ATTEMPTED;
        for (var i=0; i<itemCount; i++)
            this.status_obj.questionstatus[i] = str.charAt(i+1);
    } else {
        if (str && !this.bigsco) {  // there was data but we need to invalidate
            this.status_obj.stateDiscarded = true;
            this.status_obj.savedStatus = str.charAt(0);
        } else
            this.status_obj.savedStatus = "N";
        this.status_obj.status = LMS_NOT_ATTEMPTED;
        for (var i=0; i<itemCount; i++)
            this.status_obj.questionstatus[i] = "N";
    }    

    return this.status_obj;
}

/* This method reads persisted data from LmsCom 
  (primary from lesson_data) and creates a 
  LmsTOCStatus object from that. */
LmsComBase.prototype.OpenTOCStatus = function(nodecount, tocversion) {
    var tc = new LmsTOCStatus();
    this.status_obj = tc;

    var tocstate = new TOCState(nodecount, tocversion);
    tc.setTocState(tocstate);

    if (this.lesson_data != null && this.lesson_data.length > 0)
        tocstate.DeserializeTOC(this.lesson_data);
    return tc;
}

LmsComBase.prototype.SaveStatus = function() {
    if (this.status_obj)
        this.status_obj.Save(this);
}

// Add methods for communicating to UT (Kpath lite)
LmsComBase.prototype.utGetModeIndex = function() {
    if (this.ParentLmsCom)
        this.ParentLmsCom.utGetModeIndex();
}
LmsComBase.prototype.utPushModeIndex = function() {
    if (this.ParentLmsCom)
        this.ParentLmsCom.utPushModeIndex();
}
LmsComBase.prototype.utPopModeIndex = function() {
    if (this.ParentLmsCom)
        this.ParentLmsCom.utPopModeIndex();
}
LmsComBase.prototype.utRecordType = function(type) {
    if (this.ParentLmsCom)
        this.ParentLmsCom.utRecordType(type);
}
LmsComBase.prototype.utRecordStartTime = function(time) {
    if (this.ParentLmsCom)
        this.ParentLmsCom.utRecordStartTime(time);
}
LmsComBase.prototype.utRecordEndTime = function(time) {
    if (this.ParentLmsCom)
        this.ParentLmsCom.utRecordEndTime(time);
}
LmsComBase.prototype.utRecordCompletionStatus = function(status) {
    if (this.ParentLmsCom)
        this.ParentLmsCom.utRecordCompletionStatus(status);
}
LmsComBase.prototype.utRecordSuccessStatus = function(status) {
    if (this.ParentLmsCom)
        this.ParentLmsCom.utRecordSuccessStatus(status);
}
LmsComBase.prototype.utRecordScoreScaled = function(score) {
    if (this.ParentLmsCom)
        this.ParentLmsCom.utRecordScoreScaled(score);
}
LmsComBase.prototype.utRecordFrameViewed = function(frame) {
    if (this.ParentLmsCom)
        this.ParentLmsCom.utRecordFrameViewed(frame);
}

///////////////////////////////////////
// SCORM 1.2 LmsCom object defintion
///////////////////////////////////////
function LmsComScorm12() {
    this.allow_launch = false;

    this.object_type_name = "LmsComScorm12";
}

LmsComScorm12.prototype = new LmsComBase(); 
LmsComScorm12.prototype.constructor = LmsComScorm12;
LmsComScorm12.prototype.BaseClass = LmsComBase.prototype;

LmsComScorm12.prototype.Begin = function() {
    LmsComScorm12.prototype.BaseClass.Begin.call(this);

    //Initalizes communication with the LMS.
    var fname = "LmsComScorm12.Begin"
    lmslog.addEntry(0, fname, "", "Start");

    //SCORM - Gets all startup parameters from the LMS
    if (this.API == null) {
        this.allow_launch = true;
        return false;
    }
    
    if (this.API.LMSInitialize("") == "false"){
        var err = this.API.LMSGetLastError();
        var errDesc = this.API.LMSGetErrorString(err) + "\n" + this.API.LMSGetDiagnostic("");
        this.API = null;   // flag as disconnected
        lms_ConnectionLost(err + " " + errDesc);
        lmslog.addEntry(0, fname, "", errDesc);
        return false;        
    }

    this.lesson_status = this.original_lesson_status = this.doLMSGetValue("cmi.core.lesson_status");
    this.lesson_data = this.doLMSGetValue("cmi.suspend_data");
    this.lesson_location = this.doLMSGetValue("cmi.core.lesson_location");
    this.student_id = this.doLMSGetValue("cmi.core.student_id");
    this.student_name = this.doLMSGetValue("cmi.core.student_name");
    this.allow_launch = true;

    lmslog.addEntry(0, fname, "", "LMS session started");

    return true;
}

LmsComScorm12.prototype.End = function() {
    LmsComScorm12.prototype.BaseClass.End.call(this);

    //SCORM - Calls LMS to indicate that course session has ended
    var fname = "LmsComScorm12.End";
    lmslog.addEntry(0, fname, "", "Start");

    if (!this.allow_launch)
        return false;

    this.allow_launch = false;
    
    if (this.API == null) {
        return false;
    }

    if (this.lesson_status == LMS_INCOMPLETE)
        this.doLMSSetValue("cmi.core.exit", "suspend");
    else
        this.doLMSSetValue("cmi.core.exit", "");    // GS set this to suspend, too
        
    var sesstime = this.computeTime(this.start_date, this.end_date);
    this.doLMSSetValue("cmi.core.session_time", sesstime);
    // SCORM compliance suite dies if you send it an empty string (like, say, if you
    // wanted to clear out the value)
    if (this.lesson_data)
        this.doLMSSetValue("cmi.suspend_data", this.lesson_data);
    if (this.lesson_location)
        this.doLMSSetValue("cmi.core.lesson_location", this.lesson_location);

    this.SendCompletionInfo(true);
        
    if (this.API && this.API.LMSFinish("") == "false") {
        var err = this.API.LMSGetLastError();
        var errDesc = this.API.LMSGetErrorString(err) + "\n" + this.API.LMSGetDiagnostic("");
        this.API = null;   // flag as disconnected
        lms_ConnectionLost(err + " " + errDesc);
        lmslog.addEntry(0, fname, "", errDesc);
        return false;        
    }
    
    this.API = null;  // 13102364 - prevents errors from get/set if window close occurs before page callback
    
    lmslog.addEntry(0, fname, "", "LMS session terminated");

    return true;
}

/* Sends completion status to the LMS. This is factored out of LmsCom.End() so
    that completion information may be sent immediately to allow the LMS to
    update any visual feedback. "not attempted" is sent as "incomplete" because
    the SCORM1.2 spec can be interpreted to make "not attempted" an illegal value
    to set */
LmsComScorm12.prototype.SendCompletionInfo = function(isEnding) {
    if (this.lesson_status === LMS_NOT_ATTEMPTED) {
        if (this.original_lesson_status === LMS_NOT_ATTEMPTED)
            this.doLMSSetValue("cmi.core.lesson_status", LMS_INCOMPLETE);
        else
            this.doLMSSetValue("cmi.core.lesson_status", this.original_lesson_status);
    } else
        this.doLMSSetValue("cmi.core.lesson_status", this.getReportedLessonStatus());
    
    if (this.score !== "") {
        this.doLMSSetValue("cmi.core.score.raw", this.score);
        this.doLMSSetValue("cmi.core.score.min", "0");
        this.doLMSSetValue("cmi.core.score.max", "100");
    }
    if (!isEnding)
        this.doLMSCommit();
}

/* Load the SCORM API adapter if needed and save reference to it. */
LmsComScorm12.prototype.LoadAPIAdapter = function(callback) {
    this.API = null
    if (GetLmsComFactory())
        this.API = GetLmsComFactory().scorm_api_12;
    callback();
}

/* Indicate if LMS connection is live */
LmsComScorm12.prototype.IsConnected = function() {
    return this.API != null;
}

// SCORM adapter definitions

LmsComScorm12.prototype.ErrorCodes = {
        NoError : 0,
        GeneralException : 101,
        ElementCannotHaveChildren : 202,
        ElementNotAnArray : 203,
        NotInitialized : 301,
        NotImplemented : 401,
        InvalidSetValue : 402,
        ElementIsReadOnly : 403,
        ElementIsWriteOnly : 404,
        IncorrectDataType : 405
    };

LmsComScorm12.prototype.doLMSGetValue = function(name) {
    var fname = "doLMSGetValue";
    if (this.API == null) {
        return "";
    }
    
    var value = this.API.LMSGetValue(name);
    var err = this.API.LMSGetLastError();
    if (err != this.ErrorCodes.NoError) {
        var errDesc = this.API.LMSGetErrorString(err);
        lms_ConnectionLost(err + " " + errDesc + ", " + name);
        lmslog.addEntry(0,fname,"",R_scorm_getvalfail + name + " " + errDesc);
        return "";
    }
    
    lmslog.addEntry(1,fname,name,value);
    return value;
}

LmsComScorm12.prototype.doLMSSetValue = function(name, value) {
    var fname = "doLMSSetValue";
    if (this.API == null) {
        return "false";
    }
    
    if (this.API.LMSSetValue(name, value) == "false") {
        var err = this.API.LMSGetLastError();
        if (err != this.ErrorCodes.NoError && err != this.ErrorCodes.NotImplemented) {
            var errDesc = this.API.LMSGetErrorString(err);
            lms_ConnectionLost(err + " " + errDesc + ", " + name + ", " + value);
            lmslog.addEntry(0,fname,"",R_scorm_setvalfail + name + ":" + value + " " + errDesc);
            return "false";
        }
    }
    
    lmslog.addEntry(1,fname,name,value);
    return "true";
}

LmsComScorm12.prototype.doLMSCommit = function() {
    var fname = "doLMSCommit";
    if (this.API == null) {
        return "false";
    }
    
    if (this.API.LMSCommit("") == "false") {    // is commit failure a drop?
        var err = this.API.LMSGetLastError();
        var errDesc = this.API.LMSGetErrorString(err);
        lms_ConnectionLost(err + " " + errDesc);
        lmslog.addEntry(0, fname, "", errDesc);
        this.API = null;
        return "false";        
    }
    
    lmslog.addEntry(1,fname,"","");
    return "true";
}

///////////////////////////////////////
// SCORM 1.3 (SCORM2004) LmsCom object defintion
///////////////////////////////////////
function LmsComScorm2004() {
    this.allow_launch = false;
    this.object_type_name = "LmsComScorm2004";
    this.lesson_data_location = "cmi.suspend_data"; 
}

LmsComScorm2004.prototype = new LmsComBase();
LmsComScorm2004.prototype.constructor = LmsComScorm2004;
LmsComScorm2004.prototype.BaseClass = LmsComBase.prototype;

LmsComScorm2004.prototype.Begin = function() {
    LmsComScorm2004.prototype.BaseClass.Begin.call(this);

    //Initalizes communication with the LMS.
    var fname = "LmsComScorm2004.Begin"
    lmslog.addEntry(0, fname, "", "Start");

    //SCORM - Gets all startup parameters from the LMS
    if (this.API == null) {
        this.allow_launch = true;
        return false;
    }
    
    if (this.API.Initialize("") == "false"){
        var err = this.API.GetLastError();
        var errDesc = this.API.GetErrorString(err) + "\n" + this.API.GetDiagnostic("");
        this.API = null;   // flag as disconnected
        lms_ConnectionLost(err + " " + errDesc);
        lmslog.addEntry(0, fname, "", errDesc);
        return false;        
    }

    this.lesson_status = this.doGetValue("cmi.completion_status");
    if (this.lesson_status === "unknown") {
        this.lesson_status = LMS_NOT_ATTEMPTED;
    } else if (this.lesson_status === LMS_COMPLETED) {
        var success_status = this.doGetValue("cmi.success_status");
        if (success_status === LMS_PASSED || success_status === LMS_FAILED)
            this.lesson_status = success_status;
    }
    this.original_lesson_status = this.lesson_status;
    this.lesson_data = this.doGetValue(this.lesson_data_location);
    this.lesson_location = this.doGetValue("cmi.location");
    this.student_id = this.doGetValue("cmi.learner_id");
    this.student_name = this.doGetValue("cmi.learner_name");
    this.allow_launch = true;

    lmslog.addEntry(0, fname, "", "LMS session started");

    return true;
}

LmsComScorm2004.prototype.End = function() {
    LmsComScorm2004.prototype.BaseClass.End.call(this);

    //SCORM - Calls LMS to indicate that course session has ended
    var fname = "LmsComScorm2004.End";
    lmslog.addEntry(0, fname, "", "Start");

    if (!this.allow_launch)
        return false;

    this.allow_launch = false;

    if (this.API == null) {
        return false;
    }
    
    if (this.lesson_status == LMS_INCOMPLETE)
        this.doSetValue("cmi.exit", "suspend");
    else
        this.doSetValue("cmi.exit", "normal");

    var sesstime = this.computeTime(this.start_date, this.end_date);
    this.doSetValue("cmi.session_time", sesstime);
    // SCORM compliance suite dies if you send it an empty string (like, say, if you
    // wanted to clear out the value)
    if (this.lesson_data)
        this.doSetValue(this.lesson_data_location, this.lesson_data);
    if (this.lesson_location)
        this.doSetValue("cmi.location", this.lesson_location);

    this.SendCompletionInfo(true);
     
    if (this.API && this.API.Terminate("") == "false") {
        var err = this.API.GetLastError();
        var errDesc = this.API.GetErrorString(err) + "\n" + this.API.GetDiagnostic("");
        this.API = null;   // flag as disconnected
        lms_ConnectionLost(err + " " + errDesc);
        lmslog.addEntry(0, fname, "", errDesc);
        return false;        
    }
    
    this.API = null;  // 13102364 - prevents errors from get/set if window close occurs before page callback
    
    lmslog.addEntry(0, fname, "", "LMS session terminated");

    return true;
}

/* Load the SCORM API adapter if needed and save reference to it. */
LmsComScorm2004.prototype.LoadAPIAdapter = function(callback) {
    this.API = null
    if (GetLmsComFactory())
        this.API = GetLmsComFactory().scorm_api_2004;
    callback();
}

/* Sends completion status to the LMS. This is factored out of LmsCom.End() so
    that completion information may be sent immediately to allow the LMS to
    update any visual feedback. */
LmsComScorm2004.prototype.SendCompletionInfo = function(isEnding) {
    var lesson_status = this.getReportedLessonStatus();
    if (lesson_status === LMS_NOT_ATTEMPTED)
        lesson_status = this.original_lesson_status;
    // split scorm1.2/aicc completion into scorm2004 values
    // maybe it would be better to hold them in the scorm2004 manner and join
    // for aicc/scorm1.2
    if (lesson_status == LMS_PASSED || lesson_status == LMS_FAILED) {
        this.doSetValue("cmi.success_status", lesson_status);
        this.doSetValue("cmi.completion_status", LMS_COMPLETED);
    } else
        this.doSetValue("cmi.completion_status", lesson_status);

    if (this.score !== "") {
        this.doSetValue("cmi.score.raw", this.score);
        this.doSetValue("cmi.score.scaled", this.score / 100);
        this.doSetValue("cmi.score.min", "0");
        this.doSetValue("cmi.score.max", "100");
    }
    if (!isEnding)
        this.doCommit();
}

/* Internal helper method that converts date/time into
   seconds. This version generates a 1.3 compliant (second,10,2) result */
LmsComScorm2004.prototype.convertTotalSeconds = function(ts) {
    var sec = (ts % 60);
    ts -= sec;
    var tmp = (ts % 3600);  //# of seconds in the total # of minutes
    ts -= tmp;              //# of seconds in the total # of hours
    // convert seconds to conform to CMITimespan type (e.g. SS.00)
    sec = Math.round(sec * 100) / 100;
    var strSec = new String(sec);
    var strWholeSec = strSec;
    var strFractionSec = "";
    if (strSec.indexOf(".") != -1) {
        strWholeSec = strSec.substring(0, strSec.indexOf("."));
        strFractionSec = strSec.substring(strSec.indexOf(".") + 1, strSec.length);
    }
    if (strWholeSec.length < 2) {
        strWholeSec = "0" + strWholeSec;
    }
    strSec = strWholeSec;
    if ((ts % 3600) != 0)
        var hour = 0;
    else var hour = (ts / 3600);
    if ((tmp % 60) != 0)
        var min = 0;
    else var min = (tmp / 60);
    if ((new String(hour)).length < 2)
        hour = "0" + hour;
    if ((new String(min)).length < 2)
        min = "0" + min;
    var rtnVal = "PT" + hour + "H" + min + "M" + strSec + "S";
    return rtnVal;
}

/* Indicate if LMS connection is live */
LmsComScorm2004.prototype.IsConnected = function() {
    return this.API != null;
}

// SCORM adapter definitions

LmsComScorm2004.prototype.ErrorCodes = {
        NoError : 0,
        GeneralException : 101,
        GeneralInitializationFailure : 102,
        AlreadyInitialized : 103,
        ContentInstanceTerminated : 104,
        GeneralTerminationFailure : 111,
        TerminationBeforeInitialization : 112,
        TerminationAfterTermination : 113,
        RetrieveDataBeforeInitialization : 122,
        RetrieveDataAfterTermination : 123,
        StoreDataBeforeInitialization : 132,
        StoreDataAfterTermination : 133,
        CommitBeforeInitialization : 142,
        CommitAfterTermination : 143,
        GeneralArgumentError : 201,
        GeneralGetFailure : 301,
        GeneralSetFailure : 351,
        GeneralCommitFailure : 391,
        UndefinedDataModelElement : 401,
        UnimplementedDataModelElement : 402,
        DataModelElementValueNotInitialized : 403,
        DataModelElementIsReadOnly : 404,
        DataModelElementIsWriteOnly : 405,
        DataModelElementTypeMismatch : 406,
        DataModelElementValueOutOfRange : 407,
        DataModelDependencyNotEstablished : 408,
        
        KPathTimeout : 1501
    };

LmsComScorm2004.prototype.doGetValue = function(name) {
    var fname = "doGetValue";
    if (this.API == null) {
        return "";
    }
    
    var value = this.API.GetValue(name);
    var err = this.API.GetLastError();
    if (err != this.ErrorCodes.NoError && err != this.ErrorCodes.DataModelElementValueNotInitialized) {
        if (err == this.ErrorCodes.KPathTimeout && this.object_type_name == "LmsComKPath") {
            lmslog.addEntry(0,fname,"","KCenter timeout");
            var idle = this.API.GetValue("upk.idle_time");
            lmslog.addEntry(0,fname,"upk.idle_time",idle);
            this.idle_time += idle;
            lmslog.addEntry(0,fname,"idle_time",this.idle_time);
            return this.doGetValue(name);  // redo the call
        } else {            
            var errDesc = this.API.GetErrorString(err);
            lms_ConnectionLost(err + " " + errDesc + ", " + name);
            lmslog.addEntry(0,fname,"",R_scorm_getvalfail + name + " " + errDesc);
            return "";
        }
    }
    
    lmslog.addEntry(1,fname,name,value);
    return value;
}

LmsComScorm2004.prototype.doSetValue = function(name, value) {
    var fname = "doSetValue";
    if (this.API == null) {
        return "false";
    }
    
    if (this.API.SetValue(name, value) == "false") {
        var err = this.API.GetLastError();
        if (err != this.ErrorCodes.NoError && err != this.ErrorCodes.UnimplementedDataModelElement) {
            if (err == this.ErrorCodes.KPathTimeout && this.object_type_name == "LmsComKPath") {
                lmslog.addEntry(0,fname,"","KCenter timeout");
                var idle = this.API.GetValue("upk.idle_time");
                lmslog.addEntry(0,fname,"upk.idle_time",idle);
                this.idle_time += idle;
                lmslog.addEntry(0,fname,"idle_time",this.idle_time);
                return this.doSetValue(name, value);
            } else {          
                var errDesc = this.API.GetErrorString(err);
                lms_ConnectionLost(err + " " + errDesc + ", " + name + ", " + value);
                lmslog.addEntry(0,fname,"",R_scorm_setvalfail + name + ":" + value + " " + errDesc);
                return "false";
            }
        }
    }
//alert(fname+":"+name+":"+value);
    lmslog.addEntry(1,fname,name,value);
    return "true";
}

LmsComScorm2004.prototype.doCommit = function() {
    var fname = "doCommit";
    if (this.API == null) {
        return "false";
    }
    
    if (this.API.Commit("") == "false") {
        var err = this.API.GetLastError();
        var errDesc = this.API.GetErrorString(err);
        if (err == this.ErrorCodes.KPathTimeout && this.object_type_name == "LmsComKPath") {
            lmslog.addEntry(0,fname,"","KCenter timeout");
            var idle = this.API.GetValue("upk.idle_time");
            lmslog.addEntry(0,fname,"upk.idle_time",idle);
            this.idle_time += idle;
            lmslog.addEntry(0,fname,"idle_time",this.idle_time);
            return this.doCommit();
        } else {
            this.API = null;   // flag as disconnected
            lms_ConnectionLost(err + " " + errDesc);
            lmslog.addEntry(0, fname, "", errDesc);
            return "false";
        }        
    }
    
    lmslog.addEntry(1,fname,"","");
    return "true";
}


///////////////////////////////////////
// Knowledge Pathways LmsCom object defintion
///////////////////////////////////////
function LmsComKPath(parent) {
    this.allow_launch = false;

    if (parent != null) {
        this.Copy(parent);
        this.ParentLmsCom = parent; 
    } 

    this.object_type_name = "LmsComKPath";
    this.lesson_data_location = "upk.sco_data"; 
}

LmsComKPath.prototype = new LmsComScorm2004();
LmsComKPath.prototype.constructor = LmsComKPath;
LmsComKPath.prototype.BaseClass = LmsComScorm2004.prototype;

LmsComKPath.prototype.Begin = function() {
    var fname = "LmsComKPath.Begin"
    lmslog.addEntry(0, fname, "", "Start");
    var result = LmsComKPath.prototype.BaseClass.Begin.call(this);
    this.doSetValue("upk.launch_from", GetTopLevelLmsMode());
    lmslog.addEntry(0, fname, "", "LMS session started");
    return result;
}

/* Sends completion status to the LMS. This is factored out of LmsCom.End() so
    that completion information may be sent immediately to allow the LMS to
    update any visual feedback. Unlike the other lmscom types, we want to send
    LMS_NOT_ATTEMPTED to kpath if no new attempt is made */
LmsComKPath.prototype.SendCompletionInfo = function(isEnding) {
    var lesson_status = this.getReportedLessonStatus();
    if (lesson_status === LMS_NOT_ATTEMPTED) {
        this.doSetValue("cmi.completion_status", LMS_NOT_ATTEMPTED);
        return;
    }
    // split scorm1.2/aicc completion into scorm2004 values
    // maybe it would be better to hold them in the scorm2004 manner and join
    // for aicc/scorm1.2
    if (lesson_status == LMS_PASSED || lesson_status == LMS_FAILED) {
        this.doSetValue("cmi.success_status", lesson_status);
        this.doSetValue("cmi.completion_status", LMS_COMPLETED);
    } else
        this.doSetValue("cmi.completion_status", lesson_status);

    if (this.score !== "") {
        this.doSetValue("cmi.score.raw", this.score);
        this.doSetValue("cmi.score.scaled", this.score / 100);
        this.doSetValue("cmi.score.min", "0");
        this.doSetValue("cmi.score.max", "100");
    }
    if (!isEnding)
        this.doCommit();
}

/* Load the SCORM API adapter if needed and save reference to it. */
LmsComKPath.prototype.LoadAPIAdapter = function(callback) {
    if (!this.API) {    // don't overwrite previously set adapter
        var apiframe = findKPathAPIFrame(window);
        if (apiframe && apiframe.LoadRuntime && this.guid) {
            this.deferred_callback = callback;
            var that = this;
            apiframe.LoadRuntime(this.guid, 0, 0, 0, function(s,g,a,e){that.LoadAPIAdapterCallback(s,g,a,e)});
        } else {
            callback();
        }
    } else
        callback();
}

LmsComKPath.prototype.LoadAPIAdapterCallback = function(succeeded, guid, APIControl, ErrorMessage) {
    if (succeeded == "succeeded" && guid == this.guid) {
        this.API = APIControl.GetAPI();
        lmslog.addEntry(0, "LoadAPIAdapterCallback", "", "KPath adapter bound");
        try {
            this.deferred_callback();
        } catch (e) { } // catch case where we cycle quickly through the toc & lmscom page closed before API callback runs
    } else if (succeeded == "no SCO") {
        this.API = null;    // fall back to lmscombase functionality
        lmslog.addEntry(0,"LoadAPIAdapterCallback","",ErrorMessage);
        this.deferred_callback();
    } else {
        alert(R_scorm_noapi + " " + ErrorMessage);
        lmslog.addEntry(0,"LoadAPIAdapterCallback","",ErrorMessage);
        this.deferred_callback();
    }
}

LmsComKPath.prototype.doGetUPKValue = function(name) {
    var ret = this.doGetValue(name);
    if (ret === "")
        ret = -1;
    return ret;
}

LmsComKPath.prototype.doSetUPKValue = function(name, value) {
    this.doSetValue(name, value);
}

LmsComKPath.prototype.doUPKCommit = function() {
    this.doCommit();
}

// Record an interaction completion.
LmsComKPath.prototype.setInteraction = function(index,qlmscom) {
    if (!this.API)  // if running in "lmscombase mode"
        return;
        
    var statobj = qlmscom.status_obj;
    var count = this.doGetValue("cmi.interactions._count");
    if (index == -1) index = count;
    if (index >= count)
        this.doSetValue("cmi.interactions."+index+".id",qlmscom.guid);
    this.doSetValue("cmi.interactions."+index+".type",statobj.getQuestionType());
    this.doSetValue("cmi.interactions."+index+".timestamp",(new Date()).toSCORM());
    this.doSetValue("cmi.interactions."+index+".weighting","1");
    if (statobj.getResult()) {
        this.doSetValue("cmi.interactions."+index+".learner_response",statobj.getLearnerResponse());
        this.doSetValue("cmi.interactions."+index+".result",statobj.getResult());
    }
    this.doCommit();
    return index;
}

LmsComKPath.prototype.getAnsweredQuestions = function() {
    var answeredQuestions = {};
    if (!this.API)
        return answeredQuestions;
        
    var count = this.doGetValue("cmi.interactions._count");
    for (var i=0; i<count; i++) {
        var id = this.doGetValue("cmi.interactions." + i + ".id");
        answeredQuestions[id] = { index: i };
        answeredQuestions[id].weighting = this.doGetValue("cmi.interactions." + i + ".weighting");
        answeredQuestions[id].result = this.doGetValue("cmi.interactions." + i + ".result");
    }
    return answeredQuestions;
}

LmsComKPath.prototype.setObjective = function(index, objective) {
    if (this.API) {
        var objectiveID = objective.objectiveGuid == "PRIMARYOBJ" ? "PRIMARYOBJ" : "OBJ_" + objective.objectiveGuid; 
        this.doSetValue("cmi.objectives."+index+".id", objectiveID);
        this.doSetValue("cmi.objectives."+index+".score.scaled", objective.score/100);
        this.doSetValue("cmi.objectives."+index+".completion_status", objective.completed ? "completed" : "incomplete");
        this.doSetValue("cmi.objectives."+index+".success_status", objective.completed ? "passed" : "failed");
    }
}

// sequencing at end of pre/post assessment
LmsComKPath.prototype.requestContinue = function() {
    this.doSetValue("adl.nav.request","continue");
}

// Add methods for communicating to UT (Kpath lite)
LmsComKPath.prototype.utGetModeIndex = function() {
    this.mode_index = this.doGetValue("upk.modes._count");
}
// single entry stack for ut tracking seeit/tryit/printit from doit
LmsComKPath.prototype.utPushModeIndex = function() {
    this.saved_mode_index = this.mode_index;
    this.utGetModeIndex();
}
LmsComKPath.prototype.utPopModeIndex = function() {
    this.mode_index = this.saved_mode_index;
    delete this.saved_mode_index;
}
LmsComKPath.prototype.utRecordType = function(type) {
    this.doSetValue("upk.modes." + this.mode_index + ".type", type);
}
LmsComKPath.prototype.utRecordStartTime = function(time) {
    this.doSetValue("upk.modes." + this.mode_index + ".start_time",time.toSCORM());
}
LmsComKPath.prototype.utRecordEndTime = function(time) {
    this.doSetValue("upk.modes." + this.mode_index + ".end_time",time.toSCORM());
}
LmsComKPath.prototype.utRecordCompletionStatus = function(status) {
    this.doSetValue("upk.modes." + this.mode_index + ".completion_status",status);
}
LmsComKPath.prototype.utRecordSuccessStatus = function(status) {
    this.doSetValue("upk.modes." + this.mode_index + ".success_status",status);
}
LmsComKPath.prototype.utRecordScoreScaled = function(score) {
    this.doSetValue("upk.modes." + this.mode_index + ".score.scaled",score);
}
LmsComKPath.prototype.utRecordFrameViewed = function(frame) {
    this.doSetValue("upk.modes." + this.mode_index + ".frame_viewed",frame);
    var sesstime = this.computeTime(this.start_date, new Date().getTime());
    this.doSetValue("cmi.session_time", sesstime);
    this.doCommit();
}

///////////////////////////////////////
// AICC LmsCom object defintion
///////////////////////////////////////
function LmsComAICC(aicc_url,aicc_sid) {
    this.allow_launch = false;

    this.aicc_url = aicc_url;
    this.aicc_sid = aicc_sid;
    this.app = document.applets.GetHTTPPostData;
    
    this.object_type_name = "LmsComAICC";
}

LmsComAICC.prototype = new LmsComBase(); 
LmsComAICC.prototype.constructor = LmsComScorm12;
LmsComAICC.prototype.BaseClass = LmsComBase.prototype;

LmsComAICC.prototype.Begin = function() {
    LmsComAICC.prototype.BaseClass.Begin.call(this);

    //Initalizes communication with the LMS.
    var fname = "LmsComAICC.Begin"
    lmslog.addEntry(0, fname, "", "Start");

    this.hacpGetparam();
    
    lmslog.addEntry(0, fname, "", "about to log paramvalues");
}

LmsComAICC.prototype.End = function() {
    LmsComAICC.prototype.BaseClass.End.call(this);

    var fname = "LmsComAICC.End";
    lmslog.addEntry(0, fname, "", "Start");

    if (!this.allow_launch)
        return false;

    this.allow_launch = false;
    
	var NewAiccData = this.hacpGetpostdata();
	var params = "command=putparam&version=" + LmsConfig.AiccVersion + "&session_id=" + this.aicc_sid + "&aicc_data=" + escape(NewAiccData) + "&";
	lmslog.addEntry(0,fname,"params",params);
	var strData = this.app.GetData(unescape(this.aicc_url),params)
	lmslog.addEntry(0,fname,"strData",strData);
	params = "version=" + LmsConfig.AiccVersion + "&command=ExitAU&session_id=" + this.aicc_sid + "&";
	lmslog.addEntry(0,fname,"params",params);
	strData = this.app.GetData(unescape(this.aicc_url),params)
	lmslog.addEntry(0,fname,"strData",strData);
}

// stolen from lmsfunctions.js
var closingAmp = true;  // huh?

LmsComAICC.prototype.hacpGetparam = function() {
	//AICC - Gets startup parameters from the LMS
	var fname="hacpGetparam";
	lmslog.addEntry(0,fname,"","Start");
	var strData="";
	var params;
	params = "version=" + LmsConfig.AiccVersion + "&command=GetParam&session_id=" + this.aicc_sid;
	if (closingAmp)
		params = params + "&";
	lmslog.addEntry(0,fname,"params",params);
	strData = this.app.GetData(unescape(this.aicc_url),params)
	lmslog.addEntry(0,fname,"strData",strData);
	var lessonStatus;
	var lessonFlag;
	var ErrorNum;
	var ErrorText;
	var IsValid = false;
	strData += " ";
	var strArgs = strData.split("\r");
	for (var i=0; i<strArgs.length; i++) {
		var strArg = strArgs[i];
		var tag = trimValue(strArg.substring( 0, strArg.indexOf( "=" ) ));
        var val = trimValue(strArg.substring( strArg.indexOf( "=" ) + 1 ));
		if (tag.toLowerCase() == "error") {
			ErrorNum = val;
			IsValid = true;
		}
		if (tag.toLowerCase() == "error_text") {
			ErrorText = val;
		}
		if (tag.toLowerCase() == "aicc_data") {
			this.aicc_data = strData.substring(strData.indexOf(strArg.substring( 0, strArg.indexOf( "=" ) )) + strArg.substring( 0, strArg.indexOf( "=" ) ).length + 1);
			IsValid = true;
		}
	}
	if (IsValid) {
		if (ErrorNum == 0) {
			// successful
			var CoreData;
			CoreData = get_group_content("core", this.aicc_data);
			var strArgs = CoreData.split("\r");
			for (var i=0; i<strArgs.length; i++) {
				var strArg = strArgs[i];
				var tag = trimValue(strArg.substring( 0, strArg.indexOf( "=" ) ));
	            var val = trimValue(strArg.substring( strArg.indexOf( "=" ) + 1 ));
				if (tag.toLowerCase() == "student_id") {
					this.student_id = val;
				}
				if (tag.toLowerCase() == "student_name") {
					this.student_name = val;
//					this.splitStudentname(val);				
				}
				if (tag.toLowerCase() == "lesson_status") {
					this.lesson_status = val.toLowerCase().charAt(0);
					if (val.indexOf(",")!=-1)
						this.lesson_flag = trimValue(val.substring( val.indexOf( "," ) + 1 )).toLowerCase();
					//map abreviated flags
					if (this.lesson_status == "p")
						this.lesson_status = LMS_PASSED;
					if (this.lesson_status == "c")
						this.lesson_status = LMS_COMPLETED;
					if (this.lesson_status == "f")
						this.lesson_status = LMS_FAILED;
					if (this.lesson_status == "i")
						this.lesson_status = LMS_INCOMPLETE;
					if (this.lesson_status == "n")
						this.lesson_status = LMS_NOT_ATTEMPTED;
					this.original_lesson_status = this.lesson_status;
				}
				if (tag.toLowerCase() == "lesson_location") {
					this.lesson_location = val;
				}
				if (tag.toLowerCase() == "credit") {
					this.credit = val;
				}
				if (tag.toLowerCase() == "score") {
					this.score = val;
				}
				if (tag.toLowerCase() == "time") {
					this.time = val;
				}
			}
			if (this.lesson_status == LMS_INCOMPLETE || 
			    this.lesson_status == LMS_COMPLETED || 
			    this.lesson_status == LMS_PASSED || 
			    this.lesson_status == LMS_FAILED
			    ) {
				this.lesson_data = trimValue(get_group_content("core_lesson", this.aicc_data));
			}
			else {
				this.lesson_data = "";
			}
			this.core_vendor = trimValue(get_group_content("core_vendor", this.aicc_data));
			//startTimer();
			this.allow_launch = true;
		}
		else {
			// unsuccessful, display error and do not launch
			alert(R_error_unexp + ErrorText);
			lmslog.addEntry(0,fname,"","Unable to continue because of the following error: " + ErrorText);
		}
	}
	else {
		alert(R_error_svr + unescape(this.aicc_url) + "\r\r - " + strData);
		lmslog.addEntry(0,fname,"","Unable to post to server " + unescape(this.aicc_url) + " - " + strData);
	}
}

LmsComAICC.prototype.hacpGetpostdata = function() {
	//AICC - Builds the AICC message string for passing to the LMS
	var fname = "hacpGetpostdata";
	lmslog.addEntry(0,fname,"","Start");
	var NewAiccData="";
	NewAiccData = "[CORE]\r\n";
	NewAiccData += "Lesson_Location=" + "\r\n";
	var lesson_status = this.getReportedLessonStatus();
	if (lesson_status === LMS_NOT_ATTEMPTED)
        lesson_status = this.original_lesson_status;
	if (this.lesson_status === LMS_INCOMPLETE) {
		NewAiccData += "Lesson_Status=" + lesson_status + ", suspend\r\n";
	}
	else {
		NewAiccData += "Lesson_Status=" + lesson_status + "\r\n";
	}
	lmslog.addEntry(0,fname,"is_assess",this.is_assess);
	if (this.score !== "") {
		NewAiccData += "Score=" + this.score + "\r\n";
	} else {
		NewAiccData += "Score=\r\n";
	}
	NewAiccData += "Time=" + this.computeTime(this.start_date, this.end_date) + "\r\n";
	NewAiccData += "[CORE_LESSON]\r\n";
	NewAiccData += this.lesson_data + "\r\n";
	lmslog.addEntry(0,fname,"NewAiccData",NewAiccData);
	return NewAiccData;
}

function get_group_content(section, buffer) {
	var contents = "";
	var strArgs = buffer.split("\r");
	var foundSection = false;
	for (var i=0; i<strArgs.length; i++) {
		var line = trimValue(strArgs[i]);
		if (line.substr(0,1) == "[") {
			if (line.toLowerCase().indexOf("[" + section.toLowerCase() + "]") >= 0) {
				foundSection = true;
			}
			else {
				foundSection = false;
			}
		}
		else {
			if (foundSection) {
				contents += line + "\r\n";
			}
		}
	}
	return contents;
}

function trimValue(Val) {
	var strval;
	var startPos = 0;
	var endPos = Val.length;
    for (var i=0; i<Val.length; i++) {
		// space, tab, lf, cr
	   if ((Val.substr(i,1) != " ") && (Val.substr(i,1) != "\u0009") && (Val.substr(i,1) != "\u000A") && (Val.substr(i,1) != "\u000D")) {
	   	startPos = i;
		break;
	   }
	}
	if (i == Val.length)
	   return "";
    for (var i=Val.length-1; i>0; i--) {
		// space, tab, lf, cr
	   if ((Val.substr(i,1) != " ") && (Val.substr(i,1) != "\u0009") && (Val.substr(i,1) != "\u000A") && (Val.substr(i,1) != "\u000D")) {
	   	endPos = i;
		break;
	   }
	}
	if (i == 0)
	   endPos = 0;
	return Val.substring(startPos, endPos + 1);
}

/* Indicate if LMS connection is live */
LmsComAICC.prototype.IsConnected = function() {
    return true;    // what does it mean to be disconnected from an AICC LMS?
}

///////////////////////////////////////
// Cookie based LmsCom object defintion
///////////////////////////////////////
function LmsComCookie() {
    this.allow_launch = false;

    this.object_type_name = "LmsComCookie";
}

LmsComCookie.prototype = new LmsComBase();
LmsComCookie.prototype.constructor = LmsComCookie;
LmsComCookie.prototype.BaseClass = LmsComBase.prototype;

LmsComCookie.prototype.Begin = function() {
    LmsComCookie.prototype.BaseClass.Begin.call(this);
    if (this.toc_hash = window.toc_hash) {  // assignment intended
    	var c = new Cookie(document, "Bookmark_" + this.toc_hash, 30);
    	c.Load();
    	this.lesson_location = c.Location;
	}
}

LmsComCookie.prototype.End = function() {
    LmsComCookie.prototype.BaseClass.End.call(this);
    if (this.toc_hash) {
    	var c = new Cookie(document, "Bookmark_" + this.toc_hash, 30);
    	c.Load();
    	c.Location = this.lesson_location;
    	c.Store();
	}
}

// Get current bookmark. This is only needed for a TOC that is called via lmstart
// but not from an LMS. If you open a Big SCO's concept from kp.html and launch
// it, the Big SCO toc will get an lmscombase from the lmstart, but the lmstart
// page doesn't have the toc_hash, so it can't get the bookmark.
// Note that "view outline" from a normal concept does not need this. It replaces
// the lmstart page with the toc, so the toc will end up with an lmscomcookie of
// its own.
LmsComCookie.prototype.GetBookmark = function(toc_hash) { 
    if (this.toc_hash = toc_hash) { // assignment intended
    	var c = new Cookie(document, "Bookmark_" + this.toc_hash, 30);
    	c.Load();
    	this.lesson_location = c.Location;
	}
    return parseInt(this.lesson_location, 10);
}

///////////////////////////////////////
//
// LmsStatus object defintions
//
///////////////////////////////////////

/* The following are a seriers of objects 
   used to communicate the learning
   asset runtime state with the peristence
   structure used by the LmsCom object */

///////////////////////////////////////
// Basic status
///////////////////////////////////////

/* Really basic status - use by documents
   like concepts in the TOC */ 
function LmsBasicStatus() {
    this.status = LMS_NOT_ATTEMPTED;  
}

LmsBasicStatus.prototype.getStatus = function() {
    return this.status;
}

LmsBasicStatus.prototype.setStatus = function(status) {
    this.status = status;
}

LmsBasicStatus.prototype.Save = function(lmscom) {
    lmscom.lesson_status = this.status;
}

///////////////////////////////////////
// Topic status
///////////////////////////////////////
LmsTopicStatus.prototype = new LmsBasicStatus();
LmsTopicStatus.prototype.constructor = LmsTopicStatus;
LmsTopicStatus.prototype.BaseClass = LmsBasicStatus.prototype;

function LmsTopicStatus() {
    this.score = "";
    this.passed = false;
    this.modes = new Array();
    this.modes["S"] = new Mode("S");
    this.modes["T"] = new Mode("T");
    this.modes["K"] = new Mode("K");
    this.modes["D"] = new Mode("D");
    this.modes["P"] = new Mode("P");
    this.modes["E"] = new Mode("E");
    
    this.anylaunched = false;  // any mode launched this attempt?
}

LmsTopicStatus.prototype.Save = function(lmscom) {
    var fname = "SaveTopicStatus";

    lmscom.lesson_data = lmscom.bigsco ? "" : this.getStatusstring();
    lmscom.lesson_status = this.getLMSStatus();
    lmscom.score = this.getScore();
}

LmsTopicStatus.prototype.setModeavail = function(m, a) {
    this.modes[m].avail = a;
}

LmsTopicStatus.prototype.getModeavail = function(m) {
    return this.modes[m].avail;
}

LmsTopicStatus.prototype.setModestatus = function(m, s) {
    if (this.modes[m].avail)
        this.modes[m].complete = s;
    else
        lmslog.addEntry(0, "setModestatus", "", "Invalid call of setModestatus");
}

LmsTopicStatus.prototype.getModestatus = function(m) {
    return this.modes[m].complete;
}

LmsTopicStatus.prototype.setRequiredMode = function(lmsModes, playerModes) {
    if (GetTopLevelLmsMode() == "LMS")
        this.requiredMode = this._getRequiredMode(lmsModes);
    else
        this.requiredMode = this._getRequiredMode(playerModes);
}

LmsTopicStatus.prototype._getRequiredMode = function(modes) {
    var modeorder = "EKTSPD";
    for (var i=0; i<modeorder.length; i++) {
        if (modes[modeorder.charAt(i)])
            return modeorder.charAt(i);
    }
    return "None"
}

LmsTopicStatus.prototype.GetRequiredMode = function() {
    return this.requiredMode;
}

LmsTopicStatus.prototype.initTopicstat = function(s) {
    var loopVar;
    var fname = "initTopicstat";
    lmslog.addEntry(0, fname, "", "Start");
    lmslog.addEntry(0, fname, "s string", s);

    lmslog.addEntry(0, fname, "s.length", s.length);
    if (s.length != 0) {
        //split the tracking string
        var f = s.split(",");
        for (loopVar = 0; loopVar <= f.length - 1; loopVar++) {
            lmslog.addEntry(0, fname, "f[" + loopVar + "]", f[loopVar]);
            if (f[loopVar].charAt(1) == "=") {
                this.modes[f[loopVar].charAt(0)].complete = this.modeIscomplete(f[loopVar].substr(2, 1));
            }
            else if (f[loopVar].substr(0, 5) == "PASS=") {
                var flag = f[loopVar].substr(5, 1);
                if (flag == "0")
                    this.passed = false;
                else
                    this.passed = true;
            }
            else if (f[loopVar].substr(0, 6) == "SCORE=") {
                this.score = f[loopVar].substr(6);
            }
        }
    }
}

LmsTopicStatus.prototype.clearTopicstat = function() {
    this.score = "";
    this.passed = false;
    for (var m in this.modes)
        this.modes[m].complete = false;
}

LmsTopicStatus.prototype.getStatusstring = function() {
    var fname = "get_statusstring";
    lmslog.addEntry(0, fname, "", "Start");
    var dataString = "";
    for (x in this.modes) {
        if (dataString.length != 0)
            dataString += ",";
        dataString += this.modes[x].id + "=" + (this.modes[x].complete ? "C" : "I");
    }
    /*if (this.modes["E"].complete) {
        dataString += ",PASS=" + (this.passed ? "1" : "0");
    } else*/ if (this.modes["K"].complete && this.isScored) {
        dataString += ",PASS=" + (this.passed ? "1" : "0") + ",SCORE=" + this.score;
    }
    lmslog.addEntry(0, fname, "dataString", dataString);
    return dataString;
}

LmsTopicStatus.prototype.getStatus = function() {
    var itemstatus = "";
    if (this.isComplete()) {
        //topic is complete
        // NB: Test for avail is still correct here. We want to return "C" to 
        // lmsui if knowit unavailable, even if topic was passed when launched in
        // UT mode.
        if (/*this.modes["E"].avail ||*/ (this.modes["K"].avail && this.isScored)) {
            if (this.passed) {
                itemstatus = LMS_PASSED;
            }
            else {
                itemstatus = LMS_FAILED;
            }
        } else {
            itemstatus = LMS_COMPLETED;
            if (this.savedStatus == "P" || this.savedStatus == "F")
                itemstatus = this.savedStatus;
        }
    } else {
        //topic is not complete
        itemstatus = LMS_INCOMPLETE;
        if (this.savedStatus == "P" || this.savedStatus == "F" || this.savedStatus == "C")
            itemstatus = Stat2Lms(this.savedStatus);
    }
    return itemstatus;
}

// Reports Not Attempted for a completed topic where no mode has been launched.
// This keep us from reporting an attempt if the user is just browsing
// the tree and doesn't do anything.
LmsTopicStatus.prototype.getLMSStatus = function() {
    if (this.isComplete() && !this.anylaunched)
        return LMS_NOT_ATTEMPTED;
    return this.getStatus();
}

LmsTopicStatus.prototype.getScore = function() {
    return this.score;
}

// Is considered complete if either the required
// mode is taken or a mode that allows the users
// to demonstrate more knowledge than the required mode
// has been taken.
// From "Play Mode Filtering in LMS"
LmsTopicStatus.prototype.isComplete = function() {
    var fname = "is_complete";
    lmslog.addEntry(0, fname, "", "Start");
    var iscomp = false;
    
    // Topic has no modes hance no required mode,
    // is considered complete just by visiting it.
    if (this.requiredMode === "None") {
        iscomp = true;
    } else {
        var modeorder = ["E", "K", "T", "S", "P", "D"];
        for (i=0; i<modeorder.length; i++) {
            if (this.modes[modeorder[i]].complete) iscomp = true;
            if (modeorder[i] === this.requiredMode)
                break
        }
    }
    lmslog.addEntry(0, fname, "iscomp", iscomp);
    return iscomp;
}

LmsTopicStatus.prototype.modeIscomplete = function(f) {
    var ret = false;
    var fname = "modeIscomplete";
    lmslog.addEntry(0, fname, "", "Start");
    if (f == "C")
        ret = true;
    else if (f == "I")
        ret = false;
    lmslog.addEntry(0, fname, "ret", ret);
    return ret;
}

LmsTopicStatus.prototype.isRequired = function(m) {
    return this.requiredMode === m;
}
                
// holds the status of a single mode launch of a topic
// pretty much duplicates the code in TopicStatus so if topic mode launched
// standalone (UT mode) it can write back to kpath coordinated with a kpath
// launch of the topic
LmsTopicLaunchStatus.prototype = new LmsTopicStatus();
LmsTopicLaunchStatus.prototype.constructor = LmsTopicStatus;
LmsTopicLaunchStatus.prototype.BaseClass = LmsTopicStatus.prototype;

function LmsTopicLaunchStatus(mode) {
    this.mode = this.modes[mode];
    this.score = "";
    this.passed = false;
    
    this.anylaunched = true;
}

LmsTopicLaunchStatus.prototype.Save = function(lmscom) {
    var fname = "SaveTopicLaunchStatus";

    // serialize state to report back to kpath in UT mode; LMS mode will ignore
    // this and use the mode completion flag for roll-up    
    this.BaseClass.Save.call(this,lmscom);
}

LmsTopicLaunchStatus.prototype._getRequiredMode = function(modes) {
    var modeorder = "EKTSPD";
    for (var i=0; i<modeorder.length; i++) {
        if (modes.indexOf(modeorder.charAt(i)) != -1)
            return modeorder.charAt(i);
    }
    return "None"
}

// returns result when topic is used in an assessment
LmsTopicLaunchStatus.prototype.getResult = function() {
    return this.passed ? "correct" : "incorrect";
}
LmsTopicLaunchStatus.prototype.getQuestionType = function() {
    return "other";
}
LmsTopicLaunchStatus.prototype.getLearnerResponse = function() {
    return "";
}

//////////////////////////////////////////
// Mode object definition
function Mode(mid) {
    this.avail = false;
    this.complete = false;
    this.id = mid;
}

///////////////////////////////////////
// Question status
///////////////////////////////////////
LmsQuestionStatus.prototype = new LmsBasicStatus();
LmsQuestionStatus.prototype.constructor = LmsQuestionStatus;
LmsQuestionStatus.prototype.BaseClass = LmsBasicStatus.prototype;

function LmsQuestionStatus() {
    this.question_type = null;
    this.learner_response = null;
    this.result = null;
}

LmsQuestionStatus.prototype.Save = function(lmscom) {
    lmscom.lesson_status = this.status;
    lmscom.lesson_data = Lms2Stat(this.getStatus());
}

LmsQuestionStatus.prototype.getStatus = function() {
    return this.status === LMS_NOT_ATTEMPTED ? this.savedStatus : this.status;
}

/* get/set question type */
LmsQuestionStatus.prototype.getQuestionType = function() {
    return this.question_type;
}

LmsQuestionStatus.prototype.setQuestionType = function(qtype) {
    return this.question_type = qtype;
}

/* get/set Learner Response */
LmsQuestionStatus.prototype.getLearnerResponse = function() {
    return this.learner_response;
}

LmsQuestionStatus.prototype.setLearnerResponse = function(lresp) {
    return this.learner_response = lresp;
}

LmsQuestionStatus.prototype.getResult = function() {
    return this.result;
}

LmsQuestionStatus.prototype.setResult = function(result) {
    return this.result = result;
}

///////////////////////////////////////
// Assessment status
///////////////////////////////////////
LmsAssessmentStatus.prototype = new LmsBasicStatus();
LmsAssessmentStatus.prototype.constructor = LmsAssessmentStatus;
LmsAssessmentStatus.prototype.BaseClass = LmsBasicStatus.prototype;

function LmsAssessmentStatus() {
    this.score = "";
    this.questionstatus = new Array();
    this.stateDiscarded = false;    // saved state was discarded due to change in assessment
}

LmsAssessmentStatus.prototype.Save = function(lmscom) {
    lmscom.lesson_status = this.status;

    if (this.status != LMS_INCOMPLETE &&
        this.status != LMS_NOT_ATTEMPTED)
        lmscom.score = this.score;
    else
        lmscom.score = "";

    var str = "";
    if (!lmscom.bigsco)
        str = this.hash.length + "@" + this.hash;
    // if a new attempt and we didn't do anything, keep old saved status    
    str += this.status == LMS_NOT_ATTEMPTED ? this.savedStatus : Lms2Stat(this.status);
    for (var i=0; i<this.questionstatus.length; i++)
        str += this.questionstatus[i];
    lmscom.lesson_data = str;
}

LmsAssessmentStatus.prototype.getStatus = function() {
    return this.status === LMS_NOT_ATTEMPTED ? Stat2Lms(this.savedStatus) : this.status;
}

LmsAssessmentStatus.prototype.getLMSStatus = function() {
    return this.status;
}
/* get/set Learner Response */
LmsAssessmentStatus.prototype.getScore = function() {
    return this.score;
}

LmsAssessmentStatus.prototype.setScore = function(score) {
    return this.score = score;
}

///////////////////////////////////////
// TOC status
///////////////////////////////////////
LmsTOCStatus.prototype = new LmsBasicStatus();
LmsTOCStatus.prototype.constructor = LmsTOCStatus;
LmsTOCStatus.prototype.BaseClass = LmsBasicStatus.prototype;

function LmsTOCStatus() {
}

LmsTOCStatus.prototype.getStatus = function() {
    return Stat2Lms(this.getTocState().GetStatus(0));
}

LmsTOCStatus.prototype.Save = function(lmscom) {
    lmscom.lesson_status = this.getStatus();

    var tocstat = this.getTocState();
    lmscom.lesson_data = tocstat.SerializeTOC();
}

/* get/set the state structure for the entire
   TOC */
LmsTOCStatus.prototype.getTocState = function() {
    return this.tocstate;
}

LmsTOCStatus.prototype.setTocState = function(tocstate) {
    return this.tocstate = tocstate;
}

function NodeState() {
    this.Status = 'N';
    this.Score = null;
    this.ChildrenStatus = ""; 
}

/* class that holds the state of the tree */
function TOCState(count, tocversion)
{
    this.NodeCounter = count;
    this.TocVersion = tocversion;
    
    this.InitInLookupTable();
}

/* sets the status of the outline element based on its index */
TOCState.prototype.SetStatus = function(index, status) {
    if (!this.LookUpTable[index])
        this.LookUpTable[index] = new NodeState();
    this.LookUpTable[index].Status = status;
}

/* gets the status of the outline element based on its index */ 
TOCState.prototype.GetStatus = function(index) {
    return this.LookUpTable[index].Status;
}

/* sets the score of the outline element based on its index */
TOCState.prototype.SetScore = function(index, score) {
if (!this.LookUpTable[index])
    this.LookUpTable[index] = new NodeState();
    
    this.LookUpTable[index].Score = score;
}

/* gets the score of the outline element based on its index */ 
TOCState.prototype.GetScore = function(index) {
    return this.LookUpTable[index].Score;
}

/* sets the score of the outline element based on its index */
TOCState.prototype.SetChildren = function(index, ChildrenStatus) {
if (!this.LookUpTable[index])
    this.LookUpTable[index] = new NodeState();
    
    this.LookUpTable[index].ChildrenStatus = ChildrenStatus;
}

/* gets the score of the outline element based on its index */ 
TOCState.prototype.GetChildren = function(index) {
    return this.LookUpTable[index].ChildrenStatus;
}

/* Deserializes the persisted state (str) into the TOC XML
   outline structure */
TOCState.prototype.DeserializeTOC = function(str) {
    var count = 0;
    var version;

    var index = str.indexOf("@");
    var count;

    if (index > 0) {
        count = parseInt(str.substring(0, index));
        version = str.substring(index + 1, index + count + 1);
    }
    else
        return;

    /* if the TOC has changed since last 
    time data was persisted, throw away the 
    persisted data */
    if (version != this.TocVersion)
        return;

    var statusstr = str.substring(index + count + 2);

    index = statusstr.indexOf("@");
    if (index > 0) {
        count = parseInt(statusstr.substring(0, index));
        statusstr = statusstr.substring(index + 1, index + count + 1);

        /* if the status string isn't equal to the 
        count we stored for it,then the string got 
        truncated by the LMS because it was too big
        so throw away the data */
        if (statusstr.length != count)
            return;
    }
    else
        return;

    statusstr = this.RLESDecode(statusstr);
    str = this.DeserializeLookUpTable(statusstr);
}

/* Deserializes the persisted state (str) of just the tree portion 
   of the outline into the XML outline structure */
TOCState.prototype.DeserializeLookUpTable = function(str) {
    var count = 0;
    var score = "";

    for (var i = 0; i < str.length; i++) {
        var status = str.charAt(i);
        this.SetStatus(count, status); 
        
        score = "";
        while (i < str.length - 1 && str.charAt(i+1) >= "0" && str.charAt(i+1) <= "9") {
            i++;
            score += str.charAt(i);      
        }

        if (score.length > 0)
           this.SetScore(count, parseInt(score));
        
        if (i < str.length - 1 && str.charAt(i+1) == "{") {
            var j = str.indexOf("}",i+2);
            this.SetChildren(count, str.substring(i+2,j));
            i = j;
        }
        
        count++;
    }
}

//this.TocVersion.length
TOCState.prototype.SerializeTOC = function() {
    var statusstr = this.RLESEncode(this.SerializeLookupTable());
    return this.TocVersion.length + "@" + this.TocVersion + " " + statusstr.length + "@" + statusstr;
}

TOCState.prototype.SerializeLookupTable = function() {
    var str = '';

    for (var i = 0; i < this.NodeCounter; i++) {

        var status = this.LookUpTable[i].Status;
        var score = this.LookUpTable[i].Score;
        var childrenstatus = this.LookUpTable[i].ChildrenStatus;

        if (!status) status = "N";

        str += status;
//        if (score !== null)
//            str += score;
            
        if (childrenstatus)
            str += "{" + childrenstatus + "}";            
    }

    return str;
}

TOCState.prototype.InitInLookupTable = function() {
    this.LookUpTable = new Array();

    for (var i = 0; i < this.NodeCounter; i++) {
        this.LookUpTable[i] = new NodeState();
    }
    this.SetStatus(0, "I");
}

/* Run lenght encode (RLE) the string to compress the storage */
TOCState.prototype.RLESEncode = function(str) {
    var last = '';
    var run = 0;
    var outstr = '';

    last = str.charAt(0);
    run = 1;

    for (var i = 1; i < str.length; i++) {
        if (str.charAt(i) == last) {
            run++;
        }
        else {
            if (run > 2) {
                outstr += "#";
                outstr += run;
                outstr += last;
            }
            else {
                outstr += str.substring(i - run, i)
            }

            last = str.charAt(i);
            run = 1;
        }
    }

    if (run > 2) {
        outstr += "#";
        outstr += run;
        outstr += last;
    }
    else {
        outstr += str.substring(i - run, i)
    }

    return outstr;
}

/* Run lenght decode (RLE) the string */
TOCState.prototype.RLESDecode = function(str) {
    var outstr = '';
    var runcountstr = '';
    var runcount;

    for (var i = 0; i < str.length; i++) {
        if (str.charAt(i) == '#') {
            runcountstr = '';
            i++;
            while (str.charAt(i) >= "0" && str.charAt(i) <= "9") {
                runcountstr += str.charAt(i);
                i++;
            }

            runcount = parseInt(runcountstr);
            for (var j = 0; j < runcount; j++)
                outstr += str.charAt(i);
        }
        else
            outstr += str.charAt(i);
    }

    return outstr;
}

function Lms2Stat(lmsStatus) {
    return lmsStatus.charAt(0).toUpperCase();
}

function Stat2Lms(status) {
    return [LMS_PASSED, LMS_FAILED, LMS_COMPLETED, LMS_INCOMPLETE, LMS_NOT_ATTEMPTED]["PFCIN".indexOf(status)];
}
/////////////////////
// LogEntry object //
/////////////////////

function LogEntry(startdate, ltype, funcname, varname, msgval) {
    this.logType = ltype;
    this.funcName = funcname;
    this.varName = varname;
    this.msgValue = msgval;
    this.dateTime = new Date().getTime() - startdate;   //Store the entry time in millisecs after the log started
    //alert(this.toString());
}

LogEntry.prototype.GetTableRow = function(i) {
    var typeMsg = "Info";
    var color = "#006600";

    if (this.logType === 1) {
        typeMsg = "LMS Comm";
        color = "#000033";
    }

    var varName = this.varName;
    if (varName === "") { varName = "&nbsp;"; }

    var msgValue = this.msgValue;
    if (msgValue === "") { msgValue = "&nbsp;"; }

    var html = "<tr>";
    html += "<td align='right'>" + (this.dateTime / 1000) + "</td>";
    html += "<td style='color:" + color + ";'>" + this.funcName + "</td>";
    html += "<td>" + varName + "</td>";
    html += "<td>" + msgValue + "</td>";
    html += "</tr>";

    return html;
}


////////////////
// Log object //
////////////////
function Log(active) {
    this.traceWindow = null;
    this.logArray = new Array();
    this.enabled = active;
    this.startDateTime = new Date();
    this.changed = true;    // Flag for letting external parties know when the log has changed
    this.windowReady = false;
    this.startLog();
}

Log.prototype.startLog = function() {
    if (this.enabled) {
        //this.log_string += "<table border='1'><tr><td>Type</td><td>Function</td><td>Variable</td><td>Message</td></tr>";
        this.addEntry(0, "initLog", "", "Log Intialized");
        this.openTraceWindow();
    }
}

Log.prototype.addEntry = function(ltype, funcname, varname, msgval) {
    if (this.enabled) {
        this.logArray.push(new LogEntry(this.startDateTime.getTime(), ltype, funcname, varname, msgval));
        this.changed = true;
        if (this.traceWindow && this.windowReady)
            this.traceWindow.UpdateTraceStack();
    }
}

Log.prototype.writeLog = function(sortOrder, traceLength) {
    var t = traceLength;
    if (t > this.logArray.length || t == 0) {
        t = this.logArray.length;
    }

    var html = "<table width='600'>";
    html += "<tr>";
    html += "<th colspan=4>Session start: " + this.startDateTime + "</th>";
    html += "</tr>";

    html += "<tr>";
    html += "<th>time</th>";
    html += "<th>function</th>";
    html += "<th>variable</th>";
    html += "<th>message</th>";
    html += "</tr>";

    var logEntry;
    if (sortOrder == 0) {
        for (var i = this.logArray.length - t; i < this.logArray.length; i++) {
            logEntry = this.logArray[i];
            html += logEntry.GetTableRow(i);
        }
    } else {
        for (var i = this.logArray.length - 1; i >= this.logArray.length - t; i--) {
            logEntry = this.logArray[i];
            html += logEntry.GetTableRow(i);
        }
    }
    html += "</table>";

    this.changed = false;   // Reset the changed flag
    this.windowReady = true;    // window initiates first call to writelog, so we know we're up
    return html;
}

Log.prototype.writeDetails = function(i) {
    return this.logArray[i].GetDetails();
}

Log.prototype.openTraceWindow = function() {
    if (this.traceWindow == null) {
        this.changed = true;
        this.traceWindow = window.open(lms_path + "lmsdebug.html", "_blank");
    }
}
