// For comments, see the client-side stub
var VLE = {
    apiversion : 1,
    pointversion : 21,
    serverversion : true,
    wwwroot : 'https://learn2.open.ac.uk',
    scriptbase : '/mod/oucontent/api',
    reported_visit : false,
    fixedparams : {},
    allgroups : null,
    allgroupspending : null,
    strings : {
        label_group: 'Group',
        option_choose: 'Choose...'
    },
    server_data_cache : {},
    ajax_call_count : {
        minute : 0,
        count : 0
    },

    fix_param : function(name, value) {
        this.fixedparams[name] = value;
    },

    get_param : function(name) {
        // Check valid param name
        this.check_key(name, 'Invalid param name: ' + name);

        // Check it's not predefined.
        if (this.fixedparams[name] !== undefined) {
            return this.fixedparams[name];
        }

        // Find in query
        var search = String(location.search);
        var matches = new RegExp('[?&]' + name + '=([^&]+)').exec(search);
        if (matches && matches[1]) {
            return decodeURIComponent(matches[1].replace(/\+/g, ' '));
        } else {
            return null;
        }
    },

    get_attachment : function(name, ok, error) {
        var url = this.get_param(name);
        if (!url) {
            error('Attachment not found: ' + name);
            return;
        }
        var xml = url.match(/\.xml$/);
        this.ajax_get(url, function(req) {
            ok(req.responseText, xml ? req.responseXML : null);
        }, error);
    },

    get_folder : function(name, ok, error, t) {
        // Specify this for callbacks.
        if (t === undefined) {
            t = this;
        }
        var path = this.get_param(name);
        if (!path) {
            error('Folder not found: ' + name);
            return;
        }
        // Build URL.
        var url = this.wwwroot + this.scriptbase + '/ajax.php?';
        var settings = this.get_base_settings();
        settings.x = 'folder';
        settings.filepath = path;
        var firstdone = false;
        for (var key in settings) {
            if (firstdone) {
                url += '&';
            }
            firstdone = true;
            url += key + '=' + settings[key];
        }
        this.ajax_get(url, function(req) {
            // Response is JSON format.
            ok.call(t, this.evaluate_json(req.responseText));
        }, function (msg) {error.call(t, msg)});
    },

    get_base_settings : function(activityid, itemid, courseid) {
        var settings = {};

        if (typeof activityid === 'undefined') {
            activityid = this.get_param('_a');
        }
        if (typeof itemid === 'undefined') {
            itemid = this.get_param('_i');
        }
        if (typeof courseid === 'undefined') {
            courseid = this.get_param('_c');
        }

        this.check_key(activityid, 'Missing or invalid _a parameter/activityid. ' +
                'Ensure you set an id for the MediaContent tag.');
        settings.a = activityid;
        if (courseid) {
            this.check_number(courseid, 'Invalid _c parameter/courseid');
            settings.c = courseid;
            this.check_key(itemid, 'Missing or invalid _i parameter/itemid. ' +
                    'Check document item id meets restrictions', 40);
            settings.i = itemid;
        } else {
            settings.p = this.get_param('_p');
            this.check_number(settings.p, 'Missing or invalid _p/_c parameter');
        }
        settings.s = this.get_param('_s');
        this.check_key(settings.s, 'Missing or invalid _s parameter');

        return settings;
    },

    /**
     * Gets the base key name for use in local storage.
     *
     * @param {object} settings Settings object
     * @returns {string} Base key name
     */
    get_local_storage_base_key: function(settings) {
        var baseKey = settings.a;
        if (settings.c) {
            baseKey += ':' + settings.c + ':' + settings.i;
        } else {
            baseKey += ':' + settings.p;
        }
        return baseKey;
    },

    get_server_data : function(userorgroup, names, ok, error, activityid, itemid, courseid) {
        var settings = this.get_base_settings(activityid, itemid, courseid);

        // Separate handling for guest users.
        if (!this.can_save()) {
            if(userorgroup !== true) {
                setTimeout(function() {
                    error('get_server_data only available for current user when using guest account');
                }, 0);
                return;
            }
            var baseKey = this.get_local_storage_base_key(settings);
            var result = {};
            for (var i = 0; i < names.length; i++) {
                this.check_key(names[i], 'Invalid name: ' + names[i]);
                var value = localStorage.getItem(baseKey + '/' + names[i]);
                if (value === null) {
                    value = '';
                }
                result[names[i]] = value;
            }
            setTimeout(function() {
                ok(result);
            }, 0);
            return;
        }

        // Build up list of parameters for request
        if (userorgroup === true) {
            settings.u = 'y';
        } else if (/^g[0-9]+$/.test(userorgroup)) {
            settings.u = userorgroup;
        }
        settings.n = '';
        for (var i = 0; i < names.length; i++) {
            this.check_key(names[i], 'Invalid name: ' + names[i]);
            if (i > 0) {
                settings.n += ',';
            }
            settings.n += names[i];
        }
        if (settings.n === '') {
            throw 'No names specified';
        }

        // Add parameters to URL
        var url = this.wwwroot + this.scriptbase + '/get.php';
        var first = true;
        for(var key in settings) {
            if (first) {
                url += '?';
                first = false;
            } else {
                url += '&';
            }
            // None of the parameters in this request can need URL-encoding
            url += key + '=' + settings[key];
        }

        // Do AJAX request
        this.ajax_get(url, function(req) {
            // Response is text format with URL-encoded name/value pairs
            var values = {};
            var params = String(req.responseText).split('\n');
            for (var i = 0; i < params.length; i++) {
                var line = params[i];
                if (line === '') {
                    continue;
                }
                var equals = line.indexOf('=');
                if (equals == -1) {
                    throw 'Unexpected result format';
                }
                var name = line.substring(0, equals);
                var value = line.substring(equals + 1);
                values[name] = decodeURIComponent(value.replace(/\+/g, ' '));
            }
            ok(values);
        }, error);
    },

    set_server_data : function(userorgroup, values, ok, error, previousvalues, retry,
            activityid, itemid, courseid) {
        var settings = this.get_base_settings(activityid, itemid, courseid);
        var gotprevious = !(typeof previousvalues === 'undefined');

        // Separate handling for guest users.
        if (!this.can_save()) {
            if(userorgroup !== true) {
                setTimeout(function() {
                    error('set_server_data only available for current user when using guest account');
                }, 0);
                return;
            }

            // If using retry parameter, check current data first.
            var names = Object.keys(values);
            if (gotprevious) {
                this.get_server_data(userorgroup, names, function(currentvalues) {
                    equal = true;
                    for (var i = 0; i < names.length; i++) {
                        if (currentvalues[i] != previousvalues[i]) {
                            equal = false;
                            break;
                        }
                    }
                    if (equal) {
                        // Use the version of this function without the previous values to really set it.
                        set_server_data(userorgroup, values, ok, error, undefined, undefined, activityid, itemid, courseid);
                    } else {
                        retry(currentvalues);
                    }
                }, error);
                return;
            }

            // Actually save data.
            var baseKey = this.get_local_storage_base_key(settings);
            for (var i = 0; i < names.length; i++) {
                var name = names[i];
                var value = values[name];
                if (value === '') {
                    localStorage.removeItem(key)
                } else {
                    localStorage.setItem(baseKey + '/' + name, value);
                }
            }
            setTimeout(function() {
                ok();
            }, 0);
            return;
        }

        // Build up list of parameters for request
        if (userorgroup === true) {
            settings.u = 'y';
        } else if (/^g[0-9]+$/.test(userorgroup)) {
            settings.u = userorgroup;
        }
        settings.n = '';
        settings.v = '';
        if (gotprevious) {
            settings.r = '';
        }
        var first = true;
        var anyChange = false;
        for(var key in values) {
            this.check_key(key, 'Invalid name: ' + key);
            if (first) {
                first = false;
            } else {
                settings.n += ',';
                settings.v += ',';
                if (gotprevious) {
                    settings.r += ',';
                }
            }
            settings.n += key;
            settings.v += encodeURIComponent(this.comma_escape(values[key]));
            if (gotprevious) {
                if (typeof previousvalues[key] === 'undefined') {
                    throw 'Undefined previous value for param: ' + key;
                }
                settings.r += encodeURIComponent(this.comma_escape(previousvalues[key]));
            }

            var cacheKey = (settings.u ? settings.u : 'n') + ':' + key;
            if (!(this.server_data_cache[cacheKey] === values[key])) {
                anyChange = true;
            }
            this.server_data_cache[cacheKey] = values[key];
        }
        if (settings.n === '') {
            throw 'No names specified';
        }

        // If there is no change to previously sent values, don't send it (we had to do this because
        // some developers made it send set requests every second regardless of change!)
        if (!anyChange) {
            if (window.console) {
                window.console.log('set_server_data: Not setting data because value unchanged');
                window.console.log(values);
            }
            ok();
            return;
        }

        // Add parameters to URL
        var url = this.wwwroot + this.scriptbase + '/set.php';
        var data = '';
        var first = true;
        for(var key in settings) {
            if (first) {
                first = false;
            } else {
                data += '&';
            }
            // Parameters are already URL-encoded if required
            data += key + '=' + settings[key];
        }

        // Do AJAX request
        this.ajax_post(url, function(req) {
            var result = String(req.responseText);
            // SPECIAL CASE: String 'RACE' followed by LF indicates that the
            // change was abandoned because previous values didn't match
            if (/^RACE\n/.test(result)) {
                // Use rest of string to do same as a get
                var values = {};
                var params = result.substring(5).split('\n');
                for (var i = 0; i < params.length; i++) {
                    var line = params[i];
                    if (line === '') {
                        continue;
                    }
                    var equals = line.indexOf('=');
                    if (equals == -1) {
                        throw 'Unexpected result format';
                    }
                    var name = line.substring(0, equals);
                    var value = line.substring(equals + 1);
                    values[name] = decodeURIComponent(value.replace(/\+/g, ' '));
                }
                retry(values);
            } else {
                ok();
            }
        }, error, data);
    },

    set_exported_response : function(html, ok, error, activityid, itemid, courseid) {
        var url = this.wwwroot + this.scriptbase + '/ajax.php';
        var settings = this.get_base_settings();
        settings.x = 'exported';
        settings.html = html;

        this.ajax_post(url, function(req) {
            var output = this.evaluate_json(req.responseText);
            if (!output.ok) {
                var msg = 'Error saving response for export';
                if (output.error) {
                    msg = output.error;
                }
                error(msg);
            } else {
                ok();
            }
        }, error, this.encode_settings(settings));
    },

    get_all_groups : function(ok, error, t) {
        // Use cached data if available.
        if (this.allgroups !== null) {
            ok(this.allgroups);
            return;
        }

        // Specify this for callbacks.
        if (t === undefined) {
            t = this;
        }

        // If there's already a pending request, wait for it.
        if (this.allgroupspending !== null) {
            this.allgroupspending.push({ok : ok, error : error});
            return;
        }

        // Request is now in progress.
        this.allgroupspending = [];

        // Build URL.
        var url = this.wwwroot + this.scriptbase + '/ajax.php';
        var settings = this.get_base_settings();
        settings.x = 'groups';
        var first = true;
        for(var key in settings) {
            if (first) {
                url += '?';
                first = false;
            } else {
                url += '&';
            }
            // None of the parameters in this request can need URL-encoding
            url += key + '=' + settings[key];
        }

        // Do AJAX request
        this.ajax_get(url, function(req) {
            // Response is JSON format.
            var rawgroups = this.evaluate_json(req.responseText);

            // Index groups by group id
            var groupindex = {};
            for (var i = 0; i < rawgroups.groups.length; i++) {
                var group = rawgroups.groups[i];
                groupindex['g' + group.id] = i;
            }

            // Build result from groupings.
            this.allgroups = [];
            for (var i = 0; i < rawgroups.groupings.length; i++) {
                var outgrouping = rawgroups.groupings[i];
                outgrouping.groups = [];
                for (var j = 0; j < outgrouping.groupids.length; j++) {
                    outgrouping.groups.push(rawgroups.groups[
                            groupindex['g' + outgrouping.groupids[j]]]);
                }
                delete outgrouping.groupids;
                this.allgroups.push(outgrouping);
            }

            // Add 'all groups' grouping.
            var allgrouping = { id: 0, name: '', groups: []};
            for (var i = 0; i < rawgroups.groups.length; i++) {
                allgrouping.groups.push(rawgroups.groups[i]);
            }
            this.allgroups.push(allgrouping);

            // Call continuation.
            ok.call(t, this.allgroups);
            for(var i = 0; i < this.allgroupspending.length; i++) {
                this.allgroupspending[i].ok(this.allgroups);
            }
            this.allgroupspending = null;
        }, function(msg) {
            error.call(t, msg);
            for(var i = 0; i < this.allgroupspending.length; i++) {
                this.allgroupspending[i].error(msg);
            }
            this.allgroupspending = null;
        });
    },

    get_tutor_groups : function(ok, error, t) {
        this.get_all_groups(function(groupings) {
            var allgroups = [];
            for (var i = 0; i < groupings.length; i++) {
                if (/^Tutor groups \(.*\)$/.test(groupings[i].name)) {
                    allgroups = allgroups.concat(groupings[i].groups);
                }
            }
            ok.call(t, allgroups);
        }, error, t);
    },

    get_regional_groups : function(ok, error, t) {
        this.get_all_groups(function(groupings) {
            var allgroups = [];
            for (var i = 0; i < groupings.length; i++) {
                if (/^Regional groups \(.*\)$/.test(groupings[i].name)) {
                    allgroups = allgroups.concat(groupings[i].groups);
                }
            }
            ok.call(t, allgroups);
        }, error, t);
    },

    make_group_chooser : function(groups, onchange, autoselect, error, t) {
        // Create empty, hidden div.
        var div = document.createElement('div');
        div.className = 'groupselector';
        div.style.display = 'none';

        // Specify this for callbacks.
        if (t === undefined) {
            t = this;
        }

        if (groups instanceof Array) {
            this.make_group_chooser_inner(groups, div, onchange, autoselect);
        } else {
            if (groups === 'tutor') {
                this.get_tutor_groups(function(realgroups) {
                    this.make_group_chooser_inner(realgroups, div, onchange, autoselect, t);
                }, error, this);
            } else if (groups === 'regional') {
                this.get_regional_groups(function(realgroups) {
                    this.make_group_chooser_inner(realgroups, div, onchange, autoselect, t);
                }, error, this);
            } else if (/^grouping:/.test(groups)) {
                this.get_all_groups(function(groupings) {
                    var re = new RegExp(groups);
                    var realgroups = [];
                    for (var i = 0; i < groupings.length; i++) {
                        if (re.test(groupings[i].name)) {
                            realgroups = realgroups.concat(groupings[i].groups);
                        }
                    }
                    this.make_group_chooser_inner(realgroups, div, onchange, autoselect, t);
                }, error, this);
            } else {
                throw 'Invalid groups parameter: [' + groups + ']';
            }
        }

        return div;
    },

    make_group_chooser_inner : function(groups, div, onchange, autoselect, t) {
        if (groups.length === 0) {
            onchange.call(t, null, false);
            return;
        }
        if (groups.length === 1) {
            onchange.call(t, groups[0], false);
            return;
        }
        // Field label is present for accessibility, but hidden.
        var html = '<label><span style="position:absolute; top:-10000px">' +
                this.strings.label_group + ' </span><select>';
        if (autoselect === false) {
            html += '<option value="">' + this.strings.option_choose + '</option>';
        }
        for (var i = 0; i < groups.length; i++) {
            html += '<option value="i' + i + '">' + groups[i].name +
                    '</option>';
        }
        html += '</select></label>';
        div.innerHTML = html;
        div.style.display = 'block';

        var select = div.firstChild.firstChild.nextSibling;
        select.onchange = function(e)
        {
          // Do nothing if they choose 'Choose'.
          if (select.value == "") {
              return;
          }

          // Call callback.
          onchange.call(t, groups[select.value.substring(1)], true);
        };

        if (autoselect !== false) {
            onchange.call(t, groups[0], false);
        }
    },

    evaluate_json : function(json) {
        if (window.JSON) {
            // Modern browsers use JSON parsing (more secure).
            return JSON.parse(json);
        } else {
            // Old browsers use eval.
            return eval('(' + json + ')');
        }
    },

    comma_escape: function(s) {
        var re1 = /~/g;
        var re2 = /,/g;
        return String(s).replace(re1, '~~').replace(re2, '~c');
    },

    fix_https_url : function(url) {
        // If the URL is https, but our page protocol is http, change it.
        // Needed for weird case in TOSL where we redirect to an http version
        // of the page so it can cope with insecure iframes.
        if (location.protocol === 'http:' && url.indexOf('https:') === 0) {
            return 'http:' + url.substring(6);
        } else {
            return url;
        }
    },

    count_ajax_call : function() {
        var minute = Math.floor(((new Date()).getTime()) / (1000 * 60));
        if (this.ajax_call_count.minute !== minute) {
            if (this.ajax_call_count.count > 50 && window.console) {
                window.console.log('Warning: Activity made many AJAX calls last minute: ' + this.ajax_call_count.count);
            }

            this.ajax_call_count.count = 0;
            this.ajax_call_count.minute = minute;
        }
        this.ajax_call_count.count++;
    },

    ajax_post : function(url, ok, error, data, synchronous) {
        // Default to asynch requests.
        if (typeof synchronous === 'undefined') {
            synchronous = false;
        }

        // AJAX request to load and exec it.
        var req;
        if (window.ActiveXObject) {
            // Use this version on (old) IE because there is an option that some
            // people have turned off which causes the standard one below to fail.
            req = new ActiveXObject("Microsoft.XMLHTTP");
        } else {
            req = new XMLHttpRequest();
        }
        url = this.fix_https_url(url);
        req.open('POST', url, !synchronous);
        req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        req.onreadystatechange = function(e) {
            if (req.readyState == 4) {
                if (req.status == 200) {
                    // Run loaded JS
                    ok.call(VLE, req);
                } else {
                    if (error) {
                        error('Error ' + req.status + ' loading ' + url);
                    }
                }
            }
        };
        req.send(data);
        this.count_ajax_call();
    },

    ajax_get : function(url, ok, error) {
        // Get the XMLHttpRequest object. On IE we prefer the ActiveX version
        // even though it now supports the standard way too, because the ActiveX
        // one can access files if run locally.
        var req;
        if (window.ActiveXObject) {
            req = new ActiveXObject("Microsoft.XMLHTTP");
        } else {
            req = new XMLHttpRequest();
        }
        url = this.fix_https_url(url);
        req.open('GET', url, true);
        req.onreadystatechange = function(e) {
            if (req.readyState == 4) {
                // Status 0 is for local files (testing use only).
                if (req.status == 200 || req.status == 0) {
                    ok.call(VLE, req);
                } else {
                    error('Error ' + req.status + ' loading ' + url);
                }
            }
        };
        req.send(null);
        this.count_ajax_call();
    },

    check_key : function(key, message, length) {
        if (length === undefined) {
            length = 20;
        }
        var re = new RegExp('^[A-Za-z0-9._-]{1,' + length + '}$')
        if (key === null || !key.match(re)) {
            throw message + ' (Required: between 1 and ' + length + ' characters, A-Z a-z 0-9 ._-)';
        }
    },

    check_number : function(key, message) {
        if (key === null || !key.match(/^[0-9]{1,10}$/)) {
            throw message;
        }
    },

    get_embed_context : function() {
        // Get iframe in parent.
        var el = null;
        var parentiframes = parent.document.getElementsByTagName('iframe');
        for (var i = 0; i < parentiframes.length; i++) {
            if (parentiframes[i].contentWindow == window) {
                el = parentiframes[i];
            }
        }
        if (el == null) {
            throw 'Unable to find iframe in parent document';
        }

        // Temporarily insert paragraph before iframe.
        var p = parent.document.createElement('p');
        p.appendChild(parent.document.createTextNode('\u00a0'));
        el.parentNode.insertBefore(p, el);

        // Recursively find background colour.
        var bg = null;
        for (var thing = p; thing != null; thing = thing.parentNode) {
            var style = this.get_computed_style(parent, thing);
            if (style.backgroundColor !== 'transparent' &&
                    style.backgroundColor !== 'rgba(0, 0, 0, 0)') {
                bg = style.backgroundColor;
                break;
            }
        }
        if (bg == null) {
            throw 'Unable to find background colour';
        }

        var parastyle = this.get_computed_style(parent, p);
        var result = {
            backgroundColor: bg,
            fontSize: parastyle.fontSize,
            color: parastyle.color,
            fontFamily: parastyle.fontFamily,
            lineHeight: parastyle.lineHeight,
            marginBottom: parastyle.marginBottom
        };

        // Change class name to use variant.
        p.className = 'theme-ou-variant';
        parastyle = this.get_computed_style(parent, p);
        result.variant = parastyle.color;

        // Delete paragraph again and return result.
        el.parentNode.removeChild(p);
        return result;
    },

    get_computed_style : function(win, el) {
        return win.getComputedStyle ? win.getComputedStyle(el, null) :
            el.currentStyle;
    },

    // Encodes settings into a URL string with &a=b format. Must be already
    // URL-encoded if required.
    encode_settings : function(settings) {
        var data = '';
        var first = true;
        for (var key in settings) {
            if (first) {
                first = false;
            } else {
                data += '&';
            }
            data += key + '=' + settings[key];
        }
        return data;
    },

    report_visit : function(error, activityid, itemid, courseid) {
        // Record that we've done the visit report.
        this.reported_visit = true;

        // Build up list of parameters for request.
        var settings = this.get_base_settings(activityid, itemid, courseid);
        settings.type = 'visit';

        // Add parameters to URL.
        var url = this.wwwroot + this.scriptbase + '/analytics.php';
        var data = this.encode_settings(settings);

        // Do AJAX request.
        this.ajax_post(url, function(req) {}, error, data);
    },

    report_count : function(shortname, displayname, error, activityid, itemid, courseid) {
        if (!this.reported_visit) {
            throw 'Call report_visit before using other report functions';
        }

        // Build up list of parameters for request.
        var settings = this.get_base_settings(activityid, itemid, courseid);
        settings.type = 'count';
        settings.shortname = shortname;
        settings.displayname = displayname;

        // Add parameters to URL.
        var url = this.wwwroot + this.scriptbase + '/analytics.php';
        var data = this.encode_settings(settings);

        // Do AJAX request.
        this.ajax_post(url, function(req) {}, error, data);
    },

    report_start_timer : function(shortname, displayname, error, activityid, itemid, courseid) {
        var t = this;
        if (!this.reported_visit) {
            throw 'Call report_visit before using other report functions';
        }

        // Track start time.
        var status = {
            start : (new Date()).getTime(),
            cancelled : false
        };

        // Function to report time periodically.
        var reporter = function(synch) {
            // If already cancelled, do nothing.
            if (status.cancelled) {
                return;
            }
            var now = (new Date()).getTime();
            var seconds = Math.round((now - status.start) / 1000);
            if (seconds > 0) {
                // Reset status object to use new start time (now).
                status.start = now;

                // Build up list of parameters for request.
                var settings = t.get_base_settings(activityid, itemid, courseid);
                settings.type = 'time';
                settings.shortname = shortname;
                settings.displayname = displayname;
                settings.value = seconds;

                // Add parameters to URL.
                var url = t.wwwroot + t.scriptbase + '/analytics.php';
                var data = t.encode_settings(settings);

                // Do AJAX request.
                t.ajax_post(url, function(req) {}, function(msg) {
                    // If an error occurs, cancel further reporting.
                    status.cancelled = true;
                    error(msg);
                }, data, synch);
            }
        }

        // Periodically report time.
        var timer = function() {
            reporter(false);
            if (!status.cancelled) {
                setTimeout(timer, 60000);
            }
        };
        setTimeout(timer, 60000);

        // Create a 'stop' function for use when stopping explicitly..
        var stopfunction = function() {
            reporter(false);
            status.cancelled = true;
        };

        // Automatically report time before unload (if browser supports it).
        if(window.addEventListener) {
            window.addEventListener('beforeunload', function() {
                reporter(true);
                status.cancelled = true;
            });
        }

        // Return stop function.
        return stopfunction;
    },

    reset_caches : function() {
        this.allgroups = null;
        this.allgroupspending = null;
    },

    sendingServiceRequestQueue : false,
    requestingCookie : false,
    serviceRequestQueue : [],
    send_service_request : function (service, input, ok, error, t) {
        if (t === undefined) {
            t = this;
        }

        var url = this.wwwroot + this.scriptbase + '/service.php';
        var settings = this.get_base_settings();
        settings.x = 'service';

        this.check_key(service, 'Invalid service name: ' + service);
        settings.service = service;

        settings.names = '';
        settings.values = '';
        var first = true;
        for (var key in input) {
            if (first) {
                first = false;
            } else {
                settings.names += ',';
                settings.values += ',';
            }
            this.check_key(key, 'Invalid input value name: ' + key);
            if (key.match(/^_/)) {
                throw 'Invalid input value name (starts with _): ' + key;
            }
            var value = input[key];
            if (typeof value === 'number') {
                value = String(value);
            } else if (typeof value !== 'string') {
                throw 'Invalid value (not number/string) for parameter: ' + key;
            }

            settings.names += key;
            settings.values += encodeURIComponent(this.comma_escape(input[key]));
        }

        t.serviceRequestQueue.push({ok: ok, error: error, url: url, settings: settings});
        // Check if the queue is being send or the cookie is being requested.
        if (t.sendingServiceRequestQueue || t.requestingCookie) {
            // If there are a loop working to send queue then the request only need to put into the queue.
            // If the app is requesting cookie, after get cookie the callback will call sendServiceRequestQueue().
            // so add request into the queue is enough.
            return;
        }

        // Check cookie here.
        var ip = typeof settings.p === "undefined" ? settings.i : settings.p;
        if (!t.checkCookie(settings.c, ip, settings.a, settings.service, settings.s)) {
            t.requestCookie(settings, error);
        } else {
            /*Send Service request to server.*/
            t.sendServiceRequestQueue(error);
        }
    },

    /**
     * Loop the queue and send request to server. When server return 'invalidcookie', request
     * the cookie again.
     *
     * @param error {function} Callback function when request cookie again and error occur.
     */
    sendServiceRequestQueue : function (error) {
        if (t === undefined) {
            t = this;
        }
        t.error = error;
        t.sendingServiceRequestQueue = true;
        while (t.serviceRequestQueue.length !== 0 && !t.requestingCookie) {
            (function () {
                var serviceRequest = t.serviceRequestQueue.shift();
                var ok = serviceRequest.ok;
                var error = serviceRequest.error;
                t.ajax_post(serviceRequest.url, function (req) {
                    var output = t.evaluate_json(req.responseText);
                    if (!output.ok) {
                        if (output.errorcode === 'invalidcookie') {
                            // If no cookie request, make one.
                            if (!t.requestingCookie) {
                                t.requestCookie(serviceRequest.settings, t.error);
                            }
                            // Put the request back to head of the queue.
                            t.serviceRequestQueue.unshift(serviceRequest);
                            return;
                        }
                        var msg = 'Unknown error';
                        if (output.error) {
                            msg = output.error;
                        }
                        if (error !== undefined) {
                            error.call(t, 'Server reported error: ' + msg);
                        }
                    } else {
                        ok.call(t, output.ok);
                    }
                }, function (msg) {
                    if (error !== undefined) {
                        error.call(t, msg);
                    }
                }, t.encode_settings(serviceRequest.settings));
            })();
        }
        t.sendingServiceRequestQueue = false;
    },

    get_olink_url : function(targetdoc, targetptr, ok, error, courseid, t) {
        // Specify this for callbacks.
        if (t === undefined) {
            t = this;
        }

        var settings = this.get_base_settings(undefined, undefined, courseid);

        // If this is a preview then it's not available.
        if (settings.p) {
            error.call(t, 'get_olink_url not available for preview documents');
            return;
        }

        // Build URL.
        settings.x = 'olink';
        settings.targetdoc = encodeURIComponent(targetdoc);
        settings.targetptr = encodeURIComponent(targetptr);
        var url = this.wwwroot + this.scriptbase + '/ajax.php?' + this.encode_settings(settings);

        // Do AJAX request
        this.ajax_get(url, function(req) {
            // Response is JSON format.
            var result = this.evaluate_json(req.responseText);
            if (result.url) {
                ok.call(t, result.url);
            } else {
                error.call(t, result.error);
            }
        }, function(msg) {
            error.call(t, msg);
        });
    },

    resize_iframe : function() {
        // Find iframe in parent window.
        var iframe = window.frameElement;
        // If we can't find it, put a message in the console and abort.
        if (!iframe) {
            if (window.console) {
                console.log('VLE.resize_iframe: Unable to find parent iframe');
            }
            return;
        }
        // Get current window scroll.
        var currentScroll = window.top.pageYOffset;

        // Set the iframe too small so that it scrolls, then set it to the
        // required scroll height. (You can't get scroll height when not
        // scrolling as it returns the current height.)
        iframe.height = 1;
        iframe.height = document.documentElement.scrollHeight;

        // Put scroll position back (it may have moved due to removing the iframe there).
        if (currentScroll !== undefined) {
            window.top.scrollTo(window.top.pageXOffset, currentScroll);
        }
    },

    /**
     * Validate cookie.
     *
     * @param c {string} Course code.
     * @param ip {number} i or p.
     * @param a {string} The value of the a parameter in the request (activity id).
     * @param service {string} The value of the service parameter in the request.
     * @param s {string} Session's key.
     * @returns {boolean} If valid return true otherwise return false;
     */
    checkCookie : function (c, ip, a, service, s) {
        if (typeof t === "undefined") {
            t = this;
        }
        // When using oucontent c is undefined so we assign it to empty to match with server.
        if (typeof c === "undefined") {
            c = '';
        }
        ip = ip.replace(/[.]/g, '_');
        var cookieName = 'oucontent_api_service_' + c + '_' + ip + '_' + a + '_' + service;
        var cookie = t.readCookie(cookieName);

        // Get cookie by provided parameter, if not exist, return false.
        if (!cookie) {
            return false;
        }

        // Split cookie body into array and check for cookie session id and MoodleSessionId.
        var cookieSession = cookie.split('\\n')[0];
        // Session in the cookie must be similar to passed session key if not return false.
        return cookieSession === s;
    },

    /**
     * Call to server and get cookie then set it into browser's cookie.
     *
     * @param settings {object} Parameter require to create a cookie must have {a, c, i or p, s, service}.
     * @param error {function} Callback function when error occur, require settings and error callback parameter.
     */
    requestCookie : function (settings, error) {
        // Only get necessary value to make cookie request and change x to servicecookie.
        requestSettings = {
            a: settings.a,
            c: settings.c,
            s: settings.s,
            service: settings.service,
            x: 'servicecookie'
        };

        // Only i or p value exist.
        if (settings.i) {
            requestSettings.i = settings.i;
        } else {
            requestSettings.p = settings.p;
        }

        if (typeof t === "undefined") {
            t = this;
        }
        // Url to get the cookie.
        var url = this.wwwroot + this.scriptbase + '/ajax.php';
        // Turn this flag on to prevent request for cookie again.
        t.requestingCookie = true;
        this.ajax_post(url, function (req) {
            // Success callback.
            var output = this.evaluate_json(req.responseText);
            t.createCookie(output.cookie.name, output.cookie.body, output.cookie.timeout);
            t.requestingCookie = false;
            t.sendServiceRequestQueue(error);
        }, function (msg) {
            // Error callback.
            if (typeof error !== "undefined") {
                error.call(t, msg);
            }
        }, this.encode_settings(requestSettings));
    },

    /**
     * Set browser's cookie.
     *
     * @param name {string} Name of the cookie.
     * @param value {string} Value of the cookie.
     * @param seconds {number} Time before expired (by second).
     */
    createCookie : function createCookie(name, value, seconds) {
        var expires = "";
        if (seconds) {
            var date = new Date();
            date.setTime(date.getTime() + (seconds * 1000));
            expires = "; expires=" + date.toGMTString();
        }
        else {
            expires = "";
        }
        document.cookie = name + "=" + value + expires + "; path=/";
    },

    /**
     * Get cookie from browser.
     *
     * @param name {string} Cookie's name.
     * @returns {*} If exist return cookie's value otherwise return null.
     */
    readCookie : function readCookie(name) {
        var nameEQ = name + "=";
        var ca = document.cookie.split(';');
        for (var i = 0; i < ca.length; i++) {
            var c = ca[i];
            while (c.charAt(0) == ' ') {
                c = c.substring(1, c.length);
            }
            if (c.indexOf(nameEQ) == 0) {
                return c.substring(nameEQ.length, c.length);
            }
        }
        return null;
    },

    can_save: function() {
        return this.get_param('_nosave') !== 'y';
    },

    /**
     * Get fullscreen link.
     * Will be called if webthumbnail is false or not set, and canusefullscreen is set.
     * For use with SC and not htmlactivity.
     *
     * @returns link url or null.
     */
    get_fullscreen_link : function() {
        var screenlink = parent.document.getElementById('oucontent-fullscreenlink');
        var url = screenlink.getAttribute('data-link');
        if (url != null) {
            // Need to replace & with its encoded version so that the a link is displayed and is clickable.
            // The incoming url is encodes/replaces the "&amp;" with &.
            url = url.replace(/&/g, "&amp;");
        }
        return url;
    }
};
