// (C) Copyright 2015 Martin Dougiamas
//
// 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.

angular.module('mm.addons.mod_scorm')

/**
 * SCORM data model implementation for version 1.2.
 *
 * @module mm.addons.mod_scorm
 * @ngdoc service
 * @name $mmaModScormDataModel12
 */
.factory('$mmaModScormDataModel12', function($mmaModScorm, $mmEvents, $window, mmaModScormEventLaunchNextSco,
            mmaModScormEventLaunchPrevSco, mmaModScormEventUpdateToc, mmaModScormEventGoOffline) {
    var self = {};

    /**
     * Initialize the global SCORM API class.
     *
     * @param  {Object} scorm    The SCORM object.
     * @param  {Number} scoId    The SCO id.
     * @param  {Number} attempt  The attempt number.
     * @param  {Object} userData The user default data.
     * @param  {String} mode     Mode. One of $mmaModScorm#MODE constants.
     * @param  {Boolean} offline True if attempt is offline, false otherwise.
     */
    function SCORMAPI(scorm, scoId, attempt, userData, mode, offline) {

        // Contains all the current values for all the data model elements for each SCO.
        var currentUserData = {},
            self = this;

        // Current SCO Id.
        self.scoId = scoId;
        self.offline = offline;

        // Convenience function to trigger events.
        function triggerEvent(name) {
            $mmEvents.trigger(name, {
                scormid: scorm.id,
                scoid: self.scoId,
                attempt: attempt
            });
        }

        // Standard Data Type Definition.
        var CMIString256 = '^[\\u0000-\\uFFFF]{0,255}$';
        var CMIString4096 = '^[\\u0000-\\uFFFF]{0,4096}$';
        var CMITime = '^([0-2]{1}[0-9]{1}):([0-5]{1}[0-9]{1}):([0-5]{1}[0-9]{1})(\.[0-9]{1,2})?$';
        var CMITimespan = '^([0-9]{2,4}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,2})?$';
        var CMIInteger = '^\\d+$';
        var CMISInteger = '^-?([0-9]+)$';
        var CMIDecimal = '^-?([0-9]{0,3})(\.[0-9]*)?$';
        var CMIIdentifier = '^[\\u0021-\\u007E]{0,255}$';
        var CMIFeedback = CMIString256; // This must be redefined.
        var CMIIndex = '[._](\\d+).';

        // Vocabulary Data Type Definition.
        var CMIStatus = '^passed$|^completed$|^failed$|^incomplete$|^browsed$';
        var CMIStatus2 = '^passed$|^completed$|^failed$|^incomplete$|^browsed$|^not attempted$';
        var CMIExit = '^time-out$|^suspend$|^logout$|^$';
        var CMIType = '^true-false$|^choice$|^fill-in$|^matching$|^performance$|^sequencing$|^likert$|^numeric$';
        var CMIResult = '^correct$|^wrong$|^unanticipated$|^neutral$|^([0-9]{0,3})?(\.[0-9]*)?$';
        var NAVEvent = '^previous$|^continue$';

        // Children lists.
        var cmi_children = 'core,suspend_data,launch_data,comments,objectives,student_data,student_preference,interactions';
        var core_children = 'student_id,student_name,lesson_location,credit,lesson_status,entry,score,total_time,lesson_mode,exit,session_time';
        var score_children = 'raw,min,max';
        var comments_children = 'content,location,time';
        var objectives_children = 'id,score,status';
        var correct_responses_children = 'pattern';
        var student_data_children = 'mastery_score,max_time_allowed,time_limit_action';
        var student_preference_children = 'audio,language,speed,text';
        var interactions_children = 'id,objectives,time,type,correct_responses,weighting,student_response,result,latency';

        // Data ranges.
        var score_range = '0#100';
        var audio_range = '-1#100';
        var speed_range = '-100#100';
        var weighting_range = '-100#100';
        var text_range = '-1#1';

        // Prepare the definition array containing the default values.
        var def = {};
        // We need an extra object that will contain the objectives and interactions data (all the .n. elements).
        var defExtra = {};

        angular.forEach(userData, function(sco) {
            def[sco.scoid] = sco.defaultdata;
            defExtra[sco.scoid] = sco.userdata;
        });

        // The SCORM 1.2 data model.
        // Set up data model for each sco.
        var datamodel = {};
        for (var scoid in def) {
            datamodel[scoid] = {
                'cmi._children':{'defaultvalue':cmi_children, 'mod':'r', 'writeerror':'402'},
                'cmi._version':{'defaultvalue':'3.4', 'mod':'r', 'writeerror':'402'},
                'cmi.core._children':{'defaultvalue':core_children, 'mod':'r', 'writeerror':'402'},
                'cmi.core.student_id':{'defaultvalue':def[scoid]['cmi.core.student_id'], 'mod':'r', 'writeerror':'403'},
                'cmi.core.student_name':{'defaultvalue':def[scoid]['cmi.core.student_name'], 'mod':'r', 'writeerror':'403'},
                'cmi.core.lesson_location':{'defaultvalue':def[scoid]['cmi.core.lesson_location'], 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
                'cmi.core.credit':{'defaultvalue':def[scoid]['cmi.core.credit'], 'mod':'r', 'writeerror':'403'},
                'cmi.core.lesson_status':{'defaultvalue':def[scoid]['cmi.core.lesson_status'], 'format':CMIStatus, 'mod':'rw', 'writeerror':'405'},
                'cmi.core.entry':{'defaultvalue':def[scoid]['cmi.core.entry'], 'mod':'r', 'writeerror':'403'},
                'cmi.core.score._children':{'defaultvalue':score_children, 'mod':'r', 'writeerror':'402'},
                'cmi.core.score.raw':{'defaultvalue':def[scoid]['cmi.core.score.raw'], 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
                'cmi.core.score.max':{'defaultvalue':def[scoid]['cmi.core.score.max'], 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
                'cmi.core.score.min':{'defaultvalue':def[scoid]['cmi.core.score.min'], 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
                'cmi.core.total_time':{'defaultvalue':def[scoid]['cmi.core.total_time'], 'mod':'r', 'writeerror':'403'},
                'cmi.core.lesson_mode':{'defaultvalue':def[scoid]['cmi.core.lesson_mode'], 'mod':'r', 'writeerror':'403'},
                'cmi.core.exit':{'defaultvalue':def[scoid]['cmi.core.exit'], 'format':CMIExit, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
                'cmi.core.session_time':{'format':CMITimespan, 'mod':'w', 'defaultvalue':'00:00:00', 'readerror':'404', 'writeerror':'405'},
                'cmi.suspend_data':{'defaultvalue':def[scoid]['cmi.suspend_data'], 'format':CMIString4096, 'mod':'rw', 'writeerror':'405'},
                'cmi.launch_data':{'defaultvalue':def[scoid]['cmi.launch_data'], 'mod':'r', 'writeerror':'403'},
                'cmi.comments':{'defaultvalue':def[scoid]['cmi.comments'], 'format':CMIString4096, 'mod':'rw', 'writeerror':'405'},
                // Deprecated evaluation attributes.
                'cmi.evaluation.comments._count':{'defaultvalue':'0', 'mod':'r', 'writeerror':'402'},
                'cmi.evaluation.comments._children':{'defaultvalue':comments_children, 'mod':'r', 'writeerror':'402'},
                'cmi.evaluation.comments.n.content':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
                'cmi.evaluation.comments.n.location':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
                'cmi.evaluation.comments.n.time':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMITime, 'mod':'rw', 'writeerror':'405'},
                'cmi.comments_from_lms':{'mod':'r', 'writeerror':'403'},
                'cmi.objectives._children':{'defaultvalue':objectives_children, 'mod':'r', 'writeerror':'402'},
                'cmi.objectives._count':{'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
                'cmi.objectives.n.id':{'pattern':CMIIndex, 'format':CMIIdentifier, 'mod':'rw', 'writeerror':'405'},
                'cmi.objectives.n.score._children':{'pattern':CMIIndex, 'mod':'r', 'writeerror':'402'},
                'cmi.objectives.n.score.raw':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
                'cmi.objectives.n.score.min':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
                'cmi.objectives.n.score.max':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
                'cmi.objectives.n.status':{'pattern':CMIIndex, 'format':CMIStatus2, 'mod':'rw', 'writeerror':'405'},
                'cmi.student_data._children':{'defaultvalue':student_data_children, 'mod':'r', 'writeerror':'402'},
                'cmi.student_data.mastery_score':{'defaultvalue':def[scoid]['cmi.student_data.mastery_score'], 'mod':'r', 'writeerror':'403'},
                'cmi.student_data.max_time_allowed':{'defaultvalue':def[scoid]['cmi.student_data.max_time_allowed'], 'mod':'r', 'writeerror':'403'},
                'cmi.student_data.time_limit_action':{'defaultvalue':def[scoid]['cmi.student_data.time_limit_action'], 'mod':'r', 'writeerror':'403'},
                'cmi.student_preference._children':{'defaultvalue':student_preference_children, 'mod':'r', 'writeerror':'402'},
                'cmi.student_preference.audio':{'defaultvalue':def[scoid]['cmi.student_preference.audio'], 'format':CMISInteger, 'range':audio_range, 'mod':'rw', 'writeerror':'405'},
                'cmi.student_preference.language':{'defaultvalue':def[scoid]['cmi.student_preference.language'], 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
                'cmi.student_preference.speed':{'defaultvalue':def[scoid]['cmi.student_preference.speed'], 'format':CMISInteger, 'range':speed_range, 'mod':'rw', 'writeerror':'405'},
                'cmi.student_preference.text':{'defaultvalue':def[scoid]['cmi.student_preference.text'], 'format':CMISInteger, 'range':text_range, 'mod':'rw', 'writeerror':'405'},
                'cmi.interactions._children':{'defaultvalue':interactions_children, 'mod':'r', 'writeerror':'402'},
                'cmi.interactions._count':{'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
                'cmi.interactions.n.id':{'pattern':CMIIndex, 'format':CMIIdentifier, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
                'cmi.interactions.n.objectives._count':{'pattern':CMIIndex, 'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
                'cmi.interactions.n.objectives.n.id':{'pattern':CMIIndex, 'format':CMIIdentifier, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
                'cmi.interactions.n.time':{'pattern':CMIIndex, 'format':CMITime, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
                'cmi.interactions.n.type':{'pattern':CMIIndex, 'format':CMIType, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
                'cmi.interactions.n.correct_responses._count':{'pattern':CMIIndex, 'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
                'cmi.interactions.n.correct_responses.n.pattern':{'pattern':CMIIndex, 'format':CMIFeedback, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
                'cmi.interactions.n.weighting':{'pattern':CMIIndex, 'format':CMIDecimal, 'range':weighting_range, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
                'cmi.interactions.n.student_response':{'pattern':CMIIndex, 'format':CMIFeedback, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
                'cmi.interactions.n.result':{'pattern':CMIIndex, 'format':CMIResult, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
                'cmi.interactions.n.latency':{'pattern':CMIIndex, 'format':CMITimespan, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
                'nav.event':{'defaultvalue':'', 'format':NAVEvent, 'mod':'w', 'readerror':'404', 'writeerror':'405'}
            };

            currentUserData[scoid] = {};

            // Load default values.
            for (var element in datamodel[scoid]) {
                if (element.match(/\.n\./) === null) {
                    if (typeof datamodel[scoid][element].defaultvalue != 'undefined') {
                        currentUserData[scoid][element] = datamodel[scoid][element].defaultvalue;
                    }
                }
            }

            // Load initial user data for current SCO.
            for (element in def[scoid]) {
                if (element.match(/\.n\./) === null) {
                    if (typeof datamodel[scoid][element].defaultvalue != 'undefined') {
                        currentUserData[scoid][element] = datamodel[scoid][element].defaultvalue;
                    } else if (typeof defExtra[scoid][element] != 'undefined') {
                        // Check in user data values.
                        currentUserData[scoid][element] = defExtra[scoid][element];
                    } else {
                        currentUserData[scoid][element] = '';
                    }
                }
            }

            // Load interactions and objectives, and init the counters.
            var expression = new RegExp(CMIIndex,'g');
            var elementDotFormat, counterElement, currentCounterIndex, currentN;
            for (element in defExtra[scoid]) {
                counterElement = '';
                currentCounterIndex = 0;
                // This check for an indexed element. cmi.objectives.1.id or cmi.objectives_1.id.
                if (element.match(expression)) {
                    // Normalize to the expected value according the standard.
                    // Moodle stores this values using _n. instead .n.
                    elementDotFormat = element.replace(expression, ".$1.");
                    currentUserData[scoid][elementDotFormat] = defExtra[scoid][element];

                    // Get the correct counter and current index.
                    if (elementDotFormat.indexOf("cmi.evaluation.comments") === 0) {
                        counterElement = "cmi.evaluation.comments._count";
                        currentCounterIndex = elementDotFormat.match(/.(\d+)./)[1];
                    } else if (elementDotFormat.indexOf("cmi.objectives") === 0) {
                        counterElement = "cmi.objectives._count";
                        currentCounterIndex = elementDotFormat.match(/.(\d+)./)[1];
                    } else if (elementDotFormat.indexOf("cmi.interactions") === 0) {
                        if (elementDotFormat.indexOf(".objectives.") > 0) {
                            currentN = elementDotFormat.match(/cmi.interactions.(\d+)./)[1];
                            currentCounterIndex = elementDotFormat.match(/objectives.(\d+)./)[1];
                            counterElement = "cmi.interactions." + currentN + ".objectives._count";
                        } else if (elementDotFormat.indexOf(".correct_responses.") > 0) {
                            currentN = elementDotFormat.match(/cmi.interactions.(\d+)./)[1];
                            currentCounterIndex = elementDotFormat.match(/correct_responses.(\d+)./)[1];
                            counterElement = "cmi.interactions." + currentN + ".correct_responses._count";
                        } else {
                            counterElement = "cmi.interactions._count";
                            currentCounterIndex = elementDotFormat.match(/.(\d+)./)[1];
                        }
                    }

                    if (counterElement) {
                        if (typeof currentUserData[scoid][counterElement] == "undefined") {
                            currentUserData[scoid][counterElement] = 0;
                        }
                        // Check if we need to sum.
                        if (parseInt(currentCounterIndex) == parseInt(currentUserData[scoid][counterElement])) {
                            currentUserData[scoid][counterElement] = parseInt(currentUserData[scoid][counterElement]) + 1;
                        }
                        if (parseInt(currentCounterIndex) > parseInt(currentUserData[scoid][counterElement])) {
                            currentUserData[scoid][counterElement] = parseInt(currentCounterIndex) - 1;
                        }
                    }

                }
            }

            // Set default status.
            if (currentUserData[scoid]['cmi.core.lesson_status'] === '') {
                currentUserData[scoid]['cmi.core.lesson_status'] = 'not attempted';
            }

            // Define mode and credit.
            currentUserData[scoid]['cmi.core.credit'] = mode == $mmaModScorm.MODENORMAL ? 'credit' : 'no-credit';
            currentUserData[scoid]['cmi.core.lesson_mode'] = mode;
        }

        // API helper methods.

        /**
         * Get the value of the given element from the non-persistent (current) user data.
         *
         * @param  {String} el The element
         * @return {String}    The element value
         */
        function getEl(el) {
            if (typeof currentUserData[self.scoId] != 'undefined' && typeof currentUserData[self.scoId][el] != 'undefined') {
                return currentUserData[self.scoId][el];
            }
            return '';
        }

        /**
         * Set the value of the given element in the non-persistent (current) user data.
         *
         * @param  {String} el The element
         * @param  {String} value The value
         */
        function setEl(el, value) {
            if (typeof currentUserData[self.scoId] == 'undefined') {
                currentUserData[self.scoId] = {};
            }
            currentUserData[self.scoId][el] = value;
        }

        /**
         * Utility function for cloning an object
         *
         * @param {Object} obj The object to  be cloned
         * @return {Object} The object cloned
         */
        function CloneObj(obj){
            if(obj == null || typeof(obj) != 'object') {
                return obj;
            }

            var temp = new obj.constructor(); // Changed (twice).
            for(var key in obj) {
                temp[key] = CloneObj(obj[key]);
            }

            return temp;
        }

        /**
         * Utility function for adding two times in format hh:mm:ss.
         *
         * @param {String} first  First time.
         * @param {String} second Second time.
         * @return {String}       Total time.
         */
        function AddTime (first, second) {
            var sFirst = first.split(":");
            var sSecond = second.split(":");
            var cFirst = sFirst[2].split(".");
            var cSecond = sSecond[2].split(".");
            var change = 0;

            FirstCents = 0;  //Cents
            if (cFirst.length > 1) {
                FirstCents = parseInt(cFirst[1],10);
            }
            SecondCents = 0;
            if (cSecond.length > 1) {
                SecondCents = parseInt(cSecond[1],10);
            }
            var cents = FirstCents + SecondCents;
            change = Math.floor(cents / 100);
            cents = cents - (change * 100);
            if (Math.floor(cents) < 10) {
                cents = "0" + cents.toString();
            }

            var secs = parseInt(cFirst[0],10) + parseInt(cSecond[0],10) + change;  //Seconds
            change = Math.floor(secs / 60);
            secs = secs - (change * 60);
            if (Math.floor(secs) < 10) {
                secs = "0" + secs.toString();
            }

            mins = parseInt(sFirst[1],10) + parseInt(sSecond[1],10) + change;   //Minutes
            change = Math.floor(mins / 60);
            mins = mins - (change * 60);
            if (mins < 10) {
                mins = "0" + mins.toString();
            }

            hours = parseInt(sFirst[0],10) + parseInt(sSecond[0],10) + change;  //Hours
            if (hours < 10) {
                hours = "0" + hours.toString();
            }

            if (cents != '0') {
                return hours + ":" + mins + ":" + secs + '.' + cents;
            } else {
                return hours + ":" + mins + ":" + secs;
            }
        }

        /**
         * Utility function for calculating the total time spent in the SCO.
         */
        function TotalTime() {
            total_time = AddTime(getEl('cmi.core.total_time'), getEl('cmi.core.session_time'));
            return {'element': 'cmi.core.total_time', value: total_time};
        }

        /**
         * Persist the current user data (this is usually called by LMSCommit)
         *
         * @param {Bool} storetotaltime If true, we need to calculate the total time too
         * @return {Bool} [description]
         */
        function StoreData(storetotaltime) {
            if (storetotaltime) {
                if (getEl('cmi.core.lesson_status') == 'not attempted') {
                    setEl('cmi.core.lesson_status', 'completed');
                }
                if (getEl('cmi.core.lesson_mode') == $mmaModScorm.MODENORMAL) {
                    if (getEl('cmi.core.credit') == 'credit') {
                        if (getEl('cmi.student_data.mastery_score') !== '' && getEl('cmi.core.score.raw') !== '') {
                            if (parseFloat(getEl('cmi.core.score.raw')) >= parseFloat(getEl('cmi.student_data.mastery_score'))) {
                                setEl('cmi.core.lesson_status', 'passed');
                            } else {
                                setEl('cmi.core.lesson_status', 'failed');
                            }
                        }
                    }
                }
                if (getEl('cmi.core.lesson_mode') == $mmaModScorm.MODEBROWSE) {
                    if (datamodel[self.scoId]['cmi.core.lesson_status'].defaultvalue == '' && getEl('cmi.core.lesson_status') == 'not attempted') {
                        setEl('cmi.core.lesson_status', 'browsed');
                    }
                }
                tracks = CollectData();
                tracks.push(TotalTime());
            } else {
                tracks = CollectData();
            }
            var success = $mmaModScorm.saveTracksSync(self.scoId, attempt, tracks, self.offline, scorm, currentUserData);
            if (!self.offline && !success) {
                // Failure storing data in online. Go offline.
                self.offline = true;
                triggerEvent(mmaModScormEventGoOffline);
                return $mmaModScorm.saveTracksSync(self.scoId, attempt, tracks, self.offline, scorm, currentUserData);
            }
            return success;
        }

        /**
         * Collect all the user tracking data that must be persisted in the system, this is usually called by LMSCommit().
         *
         */
        function CollectData() {
            var data = [];
            for (var element in currentUserData[self.scoId]) {
                // Ommit for example the nav. elements.
                if (element.substr(0, 3) == 'cmi') {
                    expression = new RegExp(CMIIndex,'g');

                    // Get the generic name for this element (e.g. convert 'cmi.interactions.1.id' to 'cmi.interactions.n.id')
                    elementmodel = String(element).replace(expression,'.n.');

                    // Ignore the session time element.
                    if (element != "cmi.core.session_time") {

                        // Check if this specific element is not defined in the datamodel,
                        // but the generic element name is.
                        if (typeof datamodel[self.scoId][element] == "undefined" &&
                                typeof datamodel[self.scoId][elementmodel] != "undefined") {

                            // Add this specific element to the data model (by cloning
                            // the generic element) so we can track changes to it.
                            datamodel[self.scoId][element] = CloneObj(datamodel[self.scoId][elementmodel]);
                        }

                        // Check if the current element exists in the datamodel.
                        if (typeof datamodel[self.scoId][element] != "undefined") {

                            // Make sure this is not a read only element.
                            if (datamodel[self.scoId][element].mod != 'r') {

                                var el = {
                                    // Moodle stores the organizations and interactions using _n. instead .n.
                                    'element': element.replace(expression, "_$1."),
                                    'value': getEl(element)
                                };

                                // Check if the element has a default value.
                                if (typeof datamodel[self.scoId][element].defaultvalue != "undefined") {

                                    // Check if the default value is different from the current value.
                                    if (datamodel[self.scoId][element].defaultvalue != el['value'] ||
                                            typeof datamodel[self.scoId][element].defaultvalue != typeof(el['value'])) {

                                        data.push(el);

                                        // Update the element default to reflect the current committed value.
                                        datamodel[self.scoId][element].defaultvalue = el['value'];
                                    }
                                } else {
                                    data.push(el);
                                    // No default value for the element, so set it now.
                                    datamodel[self.scoId][element].defaultvalue = el['value'];
                                }
                            }
                        }
                    }
                }

            }
            return data;
        }

        // API methods now.
        var initialized = false;
        var errorCode;
        var timeout;

        self.LMSInitialize = function(param) {
            errorCode = "0";
            if (param == "") {
                if (!initialized) {
                    initialized = true;
                    errorCode = "0";
                    return "true";
                } else {
                    errorCode = "101";
                }
            } else {
                errorCode = "201";
            }

            return "false";
        };

        self.LMSFinish = function(param) {
            errorCode = "0";
            if (param == "") {
                if (initialized) {
                    initialized = false;
                    result = StoreData(true);
                    if (getEl('nav.event') != '') {
                        if (getEl('nav.event') == 'continue') {
                            triggerEvent(mmaModScormEventLaunchNextSco);
                        } else {
                            triggerEvent(mmaModScormEventLaunchPrevSco);
                        }
                    } else {
                        if (scorm.auto == '1') {
                            triggerEvent(mmaModScormEventLaunchNextSco);
                        }
                    }
                    errorCode = (result) ? '0' : '101';

                    // Trigger TOC update.
                    triggerEvent(mmaModScormEventUpdateToc);

                    // Conver to string representing a boolean.
                    result = result ? "true" : "false";
                    return result;
                } else {
                    errorCode = "301";
                }
            } else {
                errorCode = "201";
            }
            return "false";
        };

        self.LMSGetValue = function(element) {
            errorCode = "0";
            if (initialized) {
                if (element != "") {
                    expression = new RegExp(CMIIndex,'g');
                    elementmodel = String(element).replace(expression,'.n.');
                    if (typeof datamodel[self.scoId][elementmodel] != "undefined") {
                        if (datamodel[self.scoId][elementmodel].mod != 'w') {
                            errorCode = "0";
                            return getEl(element);
                        } else {
                            errorCode = datamodel[self.scoId][elementmodel].readerror;
                        }
                    } else {
                        childrenstr = '._children';
                        countstr = '._count';
                        if (elementmodel.substr(elementmodel.length - childrenstr.length,elementmodel.length) == childrenstr) {
                            parentmodel = elementmodel.substr(0,elementmodel.length - childrenstr.length);
                            if (typeof datamodel[self.scoId][parentmodel] != "undefined") {
                                errorCode = "202";
                            } else {
                                errorCode = "201";
                            }
                        } else if (elementmodel.substr(elementmodel.length - countstr.length,elementmodel.length) == countstr) {
                            parentmodel = elementmodel.substr(0,elementmodel.length - countstr.length);
                            if (typeof datamodel[self.scoId][parentmodel] != "undefined") {
                                errorCode = "203";
                            } else {
                                errorCode = "201";
                            }
                        } else {
                            errorCode = "201";
                        }
                    }
                } else {
                    errorCode = "201";
                }
            } else {
                errorCode = "301";
            }
            return "";
        };

        self.LMSSetValue = function(element, value) {
            errorCode = "0";
            if (initialized) {
                if (element != "") {
                    expression = new RegExp(CMIIndex,'g');
                    elementmodel = String(element).replace(expression,'.n.');
                    if (typeof datamodel[self.scoId][elementmodel] != "undefined") {
                        if (datamodel[self.scoId][elementmodel].mod != 'r') {
                            expression = new RegExp(datamodel[self.scoId][elementmodel].format);
                            value = value + '';
                            matches = value.match(expression);
                            if (matches != null) {
                                // Create dynamic data model element.
                                if (element != elementmodel) {

                                    // Init default counters and values.
                                    if (element.indexOf("cmi.objectives") === 0) {
                                        currentN = element.match(/cmi.objectives.(\d+)./)[1];
                                        counterElement = "cmi.objectives." + currentN + ".score";
                                        if (typeof currentUserData[self.scoId][counterElement + '._children'] == "undefined") {
                                            setEl(currentUserData[self.scoId][counterElement + '._children'], score_children);
                                            setEl(currentUserData[self.scoId][counterElement + '.raw'], '');
                                            setEl(currentUserData[self.scoId][counterElement + '.min'], '');
                                            setEl(currentUserData[self.scoId][counterElement + '.max'], '');
                                        }

                                    } else if (element.indexOf("cmi.interactions") === 0) {
                                        currentN = element.match(/cmi.interactions.(\d+)./)[1];

                                        counterElement = "cmi.interactions." + currentN + ".objectives._count";
                                        if (typeof currentUserData[self.scoId][counterElement] == "undefined") {
                                            setEl(counterElement, 0);
                                        }
                                        counterElement = "cmi.interactions." + currentN + ".correct_responses._count";
                                        if (typeof currentUserData[self.scoId][counterElement] == "undefined") {
                                            setEl(counterElement, 0);
                                        }
                                    }

                                    elementIndexes = element.split('.');
                                    subelement = 'cmi';
                                    for (i = 1; i < elementIndexes.length - 1; i++) {
                                        elementIndex = elementIndexes[i];
                                        if (elementIndexes[i + 1].match(/^\d+$/)) {
                                            if (typeof currentUserData[self.scoId][subelement + '.' + elementIndex + '._count'] == "undefined") {
                                                setEl(subelement + '.' + elementIndex + '._count', 0);
                                            }
                                            if (elementIndexes[i + 1] == getEl(subelement + '.' + elementIndex + '._count')) {
                                                var count = getEl(subelement + '.' + elementIndex + '._count');
                                                setEl(subelement + '.' + elementIndex + '._count', parseInt(count) + 1);
                                            }
                                            if (elementIndexes[i + 1] > getEl(subelement + '.' + elementIndex + '._count')) {
                                                errorCode = "201";
                                            }
                                            subelement = subelement.concat('.' + elementIndex + '.' + elementIndexes[i + 1]);
                                            i++;
                                        } else {
                                            subelement = subelement.concat('.' + elementIndex);
                                        }
                                    }
                                    element = subelement.concat('.' + elementIndexes[elementIndexes.length - 1]);
                                }
                                //Store data
                                if (errorCode == "0") {
                                    if (scorm.autocommit && !(timeout)) {
                                        timeout = setTimeout(self.LMSCommit, 60000, [""]);
                                    }
                                    if (typeof datamodel[self.scoId][elementmodel].range != "undefined") {
                                        range = datamodel[self.scoId][elementmodel].range;
                                        ranges = range.split('#');
                                        value = value * 1.0;
                                        if ((value >= ranges[0]) && (value <= ranges[1])) {
                                            setEl(element, value);
                                            errorCode = "0";
                                            return "true";
                                        } else {
                                            errorCode = datamodel[self.scoId][elementmodel].writeerror;
                                        }
                                    } else {
                                        if (element == 'cmi.comments') {
                                            setEl('cmi.comments', getEl('cmi.comments') + value);
                                        } else {
                                            setEl(element, value);
                                        }
                                        errorCode = "0";
                                        return "true";
                                    }
                                }
                            } else {
                                errorCode = datamodel[self.scoId][elementmodel].writeerror;
                            }
                        } else {
                            errorCode = datamodel[self.scoId][elementmodel].writeerror;
                        }
                    } else {
                        errorCode = "201";
                    }
                } else {
                    errorCode = "201";
                }
            } else {
                errorCode = "301";
            }
            return "false";
        };

        self.LMSCommit = function(param) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            errorCode = "0";
            if (param == "") {
                if (initialized) {
                    result = StoreData(false);
                    // Trigger TOC update.
                    triggerEvent(mmaModScormEventUpdateToc);

                    errorCode = result ? '0' : '101';

                    // Conver to string representing a boolean.
                    result = result ? "true" : "false";
                    return result;
                } else {
                    errorCode = "301";
                }
            } else {
                errorCode = "201";
            }
            return "false";
        };

        self.LMSGetLastError = function() {
            return errorCode;
        };

        var errorString = [];
        errorString["0"] = "No error";
        errorString["101"] = "General exception";
        errorString["201"] = "Invalid argument error";
        errorString["202"] = "Element cannot have children";
        errorString["203"] = "Element not an array - cannot have count";
        errorString["301"] = "Not initialized";
        errorString["401"] = "Not implemented error";
        errorString["402"] = "Invalid set value, element is a keyword";
        errorString["403"] = "Element is read only";
        errorString["404"] = "Element is write only";
        errorString["405"] = "Incorrect data type";

        self.LMSGetErrorString = function(param) {
            if (param != "") {
                return errorString[param];
            } else {
               return "";
            }
        };

        self.LMSGetDiagnostic = function(param) {
            if (param == "") {
                param = errorCode;
            }
            return param;
        };
    }


    /**
     * Prepare the datamodel for SCORM 1.2 populating all the required data.
     *
     * @module mm.addons.mod_scorm
     * @ngdoc method
     * @name $mmaModScormDataModel12#initAPI
     * @param  {Object} scorm    The SCORM object.
     * @param  {Number} scoId    The SCO id.
     * @param  {Number} attempt  The attempt number.
     * @param  {Object} userData The user default data.
     * @param  {String} [mode]   Mode. One of $mmaModScorm#MODE constants. By default, MODENORMAL.
     * @param  {Boolean} offline True if attempt is offline, false otherwise.
     */
    self.initAPI = function(scorm, scoId, attempt, userData, mode, offline) {
        mode = mode || $mmaModScorm.MODENORMAL;
        $window.API = new SCORMAPI(scorm, scoId, attempt, userData, mode, offline);
    };

    /**
     * Set a different SCO id for the current API object.
     * The scoId is like a pointer to be able to retrieve the SCO default values and set the new ones in the overall SCORM data structure
     *
     * @module mm.addons.mod_scorm
     * @ngdoc method
     * @name $mmaModScormDataModel12#loadSco
     * @param  {Number} scoId The new SCO id.
     */
    self.loadSco = function(scoId) {
        $window.API.scoId = scoId;
    };

    /**
     * Set offline mode to true or false.
     *
     * @module mm.addons.mod_scorm
     * @ngdoc method
     * @name $mmaModScormDataModel12#setOffline
     * @param  {Boolean} offline True if offline, false otherwise.
     */
    self.setOffline = function(offline) {
        $window.API.offline = offline;
    };

    return self;
});
