var rjs;(function () { if (!rjs || !rjs.requirejs) {
if (!rjs) { rjs = {}; } else { require = rjs; }
/** vim: et:ts=4:sw=4:sts=4
 * @license RequireJS 2.3.3 Copyright jQuery Foundation and other contributors.
 * Released under MIT license, https://github.com/requirejs/requirejs/blob/master/LICENSE
 */
//Not using strict: uneven strict support in browsers, #392, and causes
//problems with requirejs.exec()/transpiler plugins that may not be strict.
/*jslint regexp: true, nomen: true, sloppy: true */
/*global window, navigator, document, importScripts, setTimeout, opera */

var requirejs, require, define;
(function (global, setTimeout) {
    var req, s, head, baseElement, dataMain, src,
        interactiveScript, currentlyAddingScript, mainScript, subPath,
        version = '2.3.3',
        commentRegExp = /\/\*[\s\S]*?\*\/|([^:"'=]|^)\/\/.*$/mg,
        cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,
        jsSuffixRegExp = /\.js$/,
        currDirRegExp = /^\.\//,
        op = Object.prototype,
        ostring = op.toString,
        hasOwn = op.hasOwnProperty,
        isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document),
        isWebWorker = !isBrowser && typeof importScripts !== 'undefined',
        //PS3 indicates loaded and complete, but need to wait for complete
        //specifically. Sequence is 'loading', 'loaded', execution,
        // then 'complete'. The UA check is unfortunate, but not sure how
        //to feature test w/o causing perf issues.
        readyRegExp = isBrowser && navigator.platform === 'PLAYSTATION 3' ?
                      /^complete$/ : /^(complete|loaded)$/,
        defContextName = '_',
        //Oh the tragedy, detecting opera. See the usage of isOpera for reason.
        isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]',
        contexts = {},
        cfg = {},
        globalDefQueue = [],
        useInteractive = false;

    //Could match something like ')//comment', do not lose the prefix to comment.
    function commentReplace(match, singlePrefix) {
        return singlePrefix || '';
    }

    function isFunction(it) {
        return ostring.call(it) === '[object Function]';
    }

    function isArray(it) {
        return ostring.call(it) === '[object Array]';
    }

    /**
     * Helper function for iterating over an array. If the func returns
     * a true value, it will break out of the loop.
     */
    function each(ary, func) {
        if (ary) {
            var i;
            for (i = 0; i < ary.length; i += 1) {
                if (ary[i] && func(ary[i], i, ary)) {
                    break;
                }
            }
        }
    }

    /**
     * Helper function for iterating over an array backwards. If the func
     * returns a true value, it will break out of the loop.
     */
    function eachReverse(ary, func) {
        if (ary) {
            var i;
            for (i = ary.length - 1; i > -1; i -= 1) {
                if (ary[i] && func(ary[i], i, ary)) {
                    break;
                }
            }
        }
    }

    function hasProp(obj, prop) {
        return hasOwn.call(obj, prop);
    }

    function getOwn(obj, prop) {
        return hasProp(obj, prop) && obj[prop];
    }

    /**
     * Cycles over properties in an object and calls a function for each
     * property value. If the function returns a truthy value, then the
     * iteration is stopped.
     */
    function eachProp(obj, func) {
        var prop;
        for (prop in obj) {
            if (hasProp(obj, prop)) {
                if (func(obj[prop], prop)) {
                    break;
                }
            }
        }
    }

    /**
     * Simple function to mix in properties from source into target,
     * but only if target does not already have a property of the same name.
     */
    function mixin(target, source, force, deepStringMixin) {
        if (source) {
            eachProp(source, function (value, prop) {
                if (force || !hasProp(target, prop)) {
                    if (deepStringMixin && typeof value === 'object' && value &&
                        !isArray(value) && !isFunction(value) &&
                        !(value instanceof RegExp)) {

                        if (!target[prop]) {
                            target[prop] = {};
                        }
                        mixin(target[prop], value, force, deepStringMixin);
                    } else {
                        target[prop] = value;
                    }
                }
            });
        }
        return target;
    }

    //Similar to Function.prototype.bind, but the 'this' object is specified
    //first, since it is easier to read/figure out what 'this' will be.
    function bind(obj, fn) {
        return function () {
            return fn.apply(obj, arguments);
        };
    }

    function scripts() {
        return document.getElementsByTagName('script');
    }

    function defaultOnError(err) {
        throw err;
    }

    //Allow getting a global that is expressed in
    //dot notation, like 'a.b.c'.
    function getGlobal(value) {
        if (!value) {
            return value;
        }
        var g = global;
        each(value.split('.'), function (part) {
            g = g[part];
        });
        return g;
    }

    /**
     * Constructs an error with a pointer to an URL with more information.
     * @param {String} id the error ID that maps to an ID on a web page.
     * @param {String} message human readable error.
     * @param {Error} [err] the original error, if there is one.
     *
     * @returns {Error}
     */
    function makeError(id, msg, err, requireModules) {
        var e = new Error(msg + '\nhttp://requirejs.org/docs/errors.html#' + id);
        e.requireType = id;
        e.requireModules = requireModules;
        if (err) {
            e.originalError = err;
        }
        return e;
    }

    if (typeof define !== 'undefined') {
        //If a define is already in play via another AMD loader,
        //do not overwrite.
        return;
    }

    if (typeof requirejs !== 'undefined') {
        if (isFunction(requirejs)) {
            //Do not overwrite an existing requirejs instance.
            return;
        }
        cfg = requirejs;
        requirejs = undefined;
    }

    //Allow for a require config object
    if (typeof require !== 'undefined' && !isFunction(require)) {
        //assume it is a config object.
        cfg = require;
        require = undefined;
    }

    function newContext(contextName) {
        var inCheckLoaded, Module, context, handlers,
            checkLoadedTimeoutId,
            config = {
                //Defaults. Do not set a default for map
                //config to speed up normalize(), which
                //will run faster if there is no default.
                waitSeconds: 7,
                baseUrl: './',
                paths: {},
                bundles: {},
                pkgs: {},
                shim: {},
                config: {}
            },
            registry = {},
            //registry of just enabled modules, to speed
            //cycle breaking code when lots of modules
            //are registered, but not activated.
            enabledRegistry = {},
            undefEvents = {},
            defQueue = [],
            defined = {},
            urlFetched = {},
            bundlesMap = {},
            requireCounter = 1,
            unnormalizedCounter = 1;

        /**
         * Trims the . and .. from an array of path segments.
         * It will keep a leading path segment if a .. will become
         * the first path segment, to help with module name lookups,
         * which act like paths, but can be remapped. But the end result,
         * all paths that use this function should look normalized.
         * NOTE: this method MODIFIES the input array.
         * @param {Array} ary the array of path segments.
         */
        function trimDots(ary) {
            var i, part;
            for (i = 0; i < ary.length; i++) {
                part = ary[i];
                if (part === '.') {
                    ary.splice(i, 1);
                    i -= 1;
                } else if (part === '..') {
                    // If at the start, or previous value is still ..,
                    // keep them so that when converted to a path it may
                    // still work when converted to a path, even though
                    // as an ID it is less than ideal. In larger point
                    // releases, may be better to just kick out an error.
                    if (i === 0 || (i === 1 && ary[2] === '..') || ary[i - 1] === '..') {
                        continue;
                    } else if (i > 0) {
                        ary.splice(i - 1, 2);
                        i -= 2;
                    }
                }
            }
        }

        /**
         * Given a relative module name, like ./something, normalize it to
         * a real name that can be mapped to a path.
         * @param {String} name the relative name
         * @param {String} baseName a real name that the name arg is relative
         * to.
         * @param {Boolean} applyMap apply the map config to the value. Should
         * only be done if this normalization is for a dependency ID.
         * @returns {String} normalized name
         */
        function normalize(name, baseName, applyMap) {
            var pkgMain, mapValue, nameParts, i, j, nameSegment, lastIndex,
                foundMap, foundI, foundStarMap, starI, normalizedBaseParts,
                baseParts = (baseName && baseName.split('/')),
                map = config.map,
                starMap = map && map['*'];

            //Adjust any relative paths.
            if (name) {
                name = name.split('/');
                lastIndex = name.length - 1;

                // If wanting node ID compatibility, strip .js from end
                // of IDs. Have to do this here, and not in nameToUrl
                // because node allows either .js or non .js to map
                // to same file.
                if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) {
                    name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, '');
                }

                // Starts with a '.' so need the baseName
                if (name[0].charAt(0) === '.' && baseParts) {
                    //Convert baseName to array, and lop off the last part,
                    //so that . matches that 'directory' and not name of the baseName's
                    //module. For instance, baseName of 'one/two/three', maps to
                    //'one/two/three.js', but we want the directory, 'one/two' for
                    //this normalization.
                    normalizedBaseParts = baseParts.slice(0, baseParts.length - 1);
                    name = normalizedBaseParts.concat(name);
                }

                trimDots(name);
                name = name.join('/');
            }

            //Apply map config if available.
            if (applyMap && map && (baseParts || starMap)) {
                nameParts = name.split('/');

                outerLoop: for (i = nameParts.length; i > 0; i -= 1) {
                    nameSegment = nameParts.slice(0, i).join('/');

                    if (baseParts) {
                        //Find the longest baseName segment match in the config.
                        //So, do joins on the biggest to smallest lengths of baseParts.
                        for (j = baseParts.length; j > 0; j -= 1) {
                            mapValue = getOwn(map, baseParts.slice(0, j).join('/'));

                            //baseName segment has config, find if it has one for
                            //this name.
                            if (mapValue) {
                                mapValue = getOwn(mapValue, nameSegment);
                                if (mapValue) {
                                    //Match, update name to the new value.
                                    foundMap = mapValue;
                                    foundI = i;
                                    break outerLoop;
                                }
                            }
                        }
                    }

                    //Check for a star map match, but just hold on to it,
                    //if there is a shorter segment match later in a matching
                    //config, then favor over this star map.
                    if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) {
                        foundStarMap = getOwn(starMap, nameSegment);
                        starI = i;
                    }
                }

                if (!foundMap && foundStarMap) {
                    foundMap = foundStarMap;
                    foundI = starI;
                }

                if (foundMap) {
                    nameParts.splice(0, foundI, foundMap);
                    name = nameParts.join('/');
                }
            }

            // If the name points to a package's name, use
            // the package main instead.
            pkgMain = getOwn(config.pkgs, name);

            return pkgMain ? pkgMain : name;
        }

        function removeScript(name) {
            if (isBrowser) {
                each(scripts(), function (scriptNode) {
                    if (scriptNode.getAttribute('data-requiremodule') === name &&
                            scriptNode.getAttribute('data-requirecontext') === context.contextName) {
                        scriptNode.parentNode.removeChild(scriptNode);
                        return true;
                    }
                });
            }
        }

        function hasPathFallback(id) {
            var pathConfig = getOwn(config.paths, id);
            if (pathConfig && isArray(pathConfig) && pathConfig.length > 1) {
                //Pop off the first array value, since it failed, and
                //retry
                pathConfig.shift();
                context.require.undef(id);

                //Custom require that does not do map translation, since
                //ID is "absolute", already mapped/resolved.
                context.makeRequire(null, {
                    skipMap: true
                })([id]);

                return true;
            }
        }

        //Turns a plugin!resource to [plugin, resource]
        //with the plugin being undefined if the name
        //did not have a plugin prefix.
        function splitPrefix(name) {
            var prefix,
                index = name ? name.indexOf('!') : -1;
            if (index > -1) {
                prefix = name.substring(0, index);
                name = name.substring(index + 1, name.length);
            }
            return [prefix, name];
        }

        /**
         * Creates a module mapping that includes plugin prefix, module
         * name, and path. If parentModuleMap is provided it will
         * also normalize the name via require.normalize()
         *
         * @param {String} name the module name
         * @param {String} [parentModuleMap] parent module map
         * for the module name, used to resolve relative names.
         * @param {Boolean} isNormalized: is the ID already normalized.
         * This is true if this call is done for a define() module ID.
         * @param {Boolean} applyMap: apply the map config to the ID.
         * Should only be true if this map is for a dependency.
         *
         * @returns {Object}
         */
        function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) {
            var url, pluginModule, suffix, nameParts,
                prefix = null,
                parentName = parentModuleMap ? parentModuleMap.name : null,
                originalName = name,
                isDefine = true,
                normalizedName = '';

            //If no name, then it means it is a require call, generate an
            //internal name.
            if (!name) {
                isDefine = false;
                name = '_@r' + (requireCounter += 1);
            }

            nameParts = splitPrefix(name);
            prefix = nameParts[0];
            name = nameParts[1];

            if (prefix) {
                prefix = normalize(prefix, parentName, applyMap);
                pluginModule = getOwn(defined, prefix);
            }

            //Account for relative paths if there is a base name.
            if (name) {
                if (prefix) {
                    if (isNormalized) {
                        normalizedName = name;
                    } else if (pluginModule && pluginModule.normalize) {
                        //Plugin is loaded, use its normalize method.
                        normalizedName = pluginModule.normalize(name, function (name) {
                            return normalize(name, parentName, applyMap);
                        });
                    } else {
                        // If nested plugin references, then do not try to
                        // normalize, as it will not normalize correctly. This
                        // places a restriction on resourceIds, and the longer
                        // term solution is not to normalize until plugins are
                        // loaded and all normalizations to allow for async
                        // loading of a loader plugin. But for now, fixes the
                        // common uses. Details in #1131
                        normalizedName = name.indexOf('!') === -1 ?
                                         normalize(name, parentName, applyMap) :
                                         name;
                    }
                } else {
                    //A regular module.
                    normalizedName = normalize(name, parentName, applyMap);

                    //Normalized name may be a plugin ID due to map config
                    //application in normalize. The map config values must
                    //already be normalized, so do not need to redo that part.
                    nameParts = splitPrefix(normalizedName);
                    prefix = nameParts[0];
                    normalizedName = nameParts[1];
                    isNormalized = true;

                    url = context.nameToUrl(normalizedName);
                }
            }

            //If the id is a plugin id that cannot be determined if it needs
            //normalization, stamp it with a unique ID so two matching relative
            //ids that may conflict can be separate.
            suffix = prefix && !pluginModule && !isNormalized ?
                     '_unnormalized' + (unnormalizedCounter += 1) :
                     '';

            return {
                prefix: prefix,
                name: normalizedName,
                parentMap: parentModuleMap,
                unnormalized: !!suffix,
                url: url,
                originalName: originalName,
                isDefine: isDefine,
                id: (prefix ?
                        prefix + '!' + normalizedName :
                        normalizedName) + suffix
            };
        }

        function getModule(depMap) {
            var id = depMap.id,
                mod = getOwn(registry, id);

            if (!mod) {
                mod = registry[id] = new context.Module(depMap);
            }

            return mod;
        }

        function on(depMap, name, fn) {
            var id = depMap.id,
                mod = getOwn(registry, id);

            if (hasProp(defined, id) &&
                    (!mod || mod.defineEmitComplete)) {
                if (name === 'defined') {
                    fn(defined[id]);
                }
            } else {
                mod = getModule(depMap);
                if (mod.error && name === 'error') {
                    fn(mod.error);
                } else {
                    mod.on(name, fn);
                }
            }
        }

        function onError(err, errback) {
            var ids = err.requireModules,
                notified = false;

            if (errback) {
                errback(err);
            } else {
                each(ids, function (id) {
                    var mod = getOwn(registry, id);
                    if (mod) {
                        //Set error on module, so it skips timeout checks.
                        mod.error = err;
                        if (mod.events.error) {
                            notified = true;
                            mod.emit('error', err);
                        }
                    }
                });

                if (!notified) {
                    req.onError(err);
                }
            }
        }

        /**
         * Internal method to transfer globalQueue items to this context's
         * defQueue.
         */
        function takeGlobalQueue() {
            //Push all the globalDefQueue items into the context's defQueue
            if (globalDefQueue.length) {
                each(globalDefQueue, function(queueItem) {
                    var id = queueItem[0];
                    if (typeof id === 'string') {
                        context.defQueueMap[id] = true;
                    }
                    defQueue.push(queueItem);
                });
                globalDefQueue = [];
            }
        }

        handlers = {
            'require': function (mod) {
                if (mod.require) {
                    return mod.require;
                } else {
                    return (mod.require = context.makeRequire(mod.map));
                }
            },
            'exports': function (mod) {
                mod.usingExports = true;
                if (mod.map.isDefine) {
                    if (mod.exports) {
                        return (defined[mod.map.id] = mod.exports);
                    } else {
                        return (mod.exports = defined[mod.map.id] = {});
                    }
                }
            },
            'module': function (mod) {
                if (mod.module) {
                    return mod.module;
                } else {
                    return (mod.module = {
                        id: mod.map.id,
                        uri: mod.map.url,
                        config: function () {
                            return getOwn(config.config, mod.map.id) || {};
                        },
                        exports: mod.exports || (mod.exports = {})
                    });
                }
            }
        };

        function cleanRegistry(id) {
            //Clean up machinery used for waiting modules.
            delete registry[id];
            delete enabledRegistry[id];
        }

        function breakCycle(mod, traced, processed) {
            var id = mod.map.id;

            if (mod.error) {
                mod.emit('error', mod.error);
            } else {
                traced[id] = true;
                each(mod.depMaps, function (depMap, i) {
                    var depId = depMap.id,
                        dep = getOwn(registry, depId);

                    //Only force things that have not completed
                    //being defined, so still in the registry,
                    //and only if it has not been matched up
                    //in the module already.
                    if (dep && !mod.depMatched[i] && !processed[depId]) {
                        if (getOwn(traced, depId)) {
                            mod.defineDep(i, defined[depId]);
                            mod.check(); //pass false?
                        } else {
                            breakCycle(dep, traced, processed);
                        }
                    }
                });
                processed[id] = true;
            }
        }

        function checkLoaded() {
            var err, usingPathFallback,
                waitInterval = config.waitSeconds * 1000,
                //It is possible to disable the wait interval by using waitSeconds of 0.
                expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(),
                noLoads = [],
                reqCalls = [],
                stillLoading = false,
                needCycleCheck = true;

            //Do not bother if this call was a result of a cycle break.
            if (inCheckLoaded) {
                return;
            }

            inCheckLoaded = true;

            //Figure out the state of all the modules.
            eachProp(enabledRegistry, function (mod) {
                var map = mod.map,
                    modId = map.id;

                //Skip things that are not enabled or in error state.
                if (!mod.enabled) {
                    return;
                }

                if (!map.isDefine) {
                    reqCalls.push(mod);
                }

                if (!mod.error) {
                    //If the module should be executed, and it has not
                    //been inited and time is up, remember it.
                    if (!mod.inited && expired) {
                        if (hasPathFallback(modId)) {
                            usingPathFallback = true;
                            stillLoading = true;
                        } else {
                            noLoads.push(modId);
                            removeScript(modId);
                        }
                    } else if (!mod.inited && mod.fetched && map.isDefine) {
                        stillLoading = true;
                        if (!map.prefix) {
                            //No reason to keep looking for unfinished
                            //loading. If the only stillLoading is a
                            //plugin resource though, keep going,
                            //because it may be that a plugin resource
                            //is waiting on a non-plugin cycle.
                            return (needCycleCheck = false);
                        }
                    }
                }
            });

            if (expired && noLoads.length) {
                //If wait time expired, throw error of unloaded modules.
                err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads);
                err.contextName = context.contextName;
                return onError(err);
            }

            //Not expired, check for a cycle.
            if (needCycleCheck) {
                each(reqCalls, function (mod) {
                    breakCycle(mod, {}, {});
                });
            }

            //If still waiting on loads, and the waiting load is something
            //other than a plugin resource, or there are still outstanding
            //scripts, then just try back later.
            if ((!expired || usingPathFallback) && stillLoading) {
                //Something is still waiting to load. Wait for it, but only
                //if a timeout is not already in effect.
                if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) {
                    checkLoadedTimeoutId = setTimeout(function () {
                        checkLoadedTimeoutId = 0;
                        checkLoaded();
                    }, 50);
                }
            }

            inCheckLoaded = false;
        }

        Module = function (map) {
            this.events = getOwn(undefEvents, map.id) || {};
            this.map = map;
            this.shim = getOwn(config.shim, map.id);
            this.depExports = [];
            this.depMaps = [];
            this.depMatched = [];
            this.pluginMaps = {};
            this.depCount = 0;

            /* this.exports this.factory
               this.depMaps = [],
               this.enabled, this.fetched
            */
        };

        Module.prototype = {
            init: function (depMaps, factory, errback, options) {
                options = options || {};

                //Do not do more inits if already done. Can happen if there
                //are multiple define calls for the same module. That is not
                //a normal, common case, but it is also not unexpected.
                if (this.inited) {
                    return;
                }

                this.factory = factory;

                if (errback) {
                    //Register for errors on this module.
                    this.on('error', errback);
                } else if (this.events.error) {
                    //If no errback already, but there are error listeners
                    //on this module, set up an errback to pass to the deps.
                    errback = bind(this, function (err) {
                        this.emit('error', err);
                    });
                }

                //Do a copy of the dependency array, so that
                //source inputs are not modified. For example
                //"shim" deps are passed in here directly, and
                //doing a direct modification of the depMaps array
                //would affect that config.
                this.depMaps = depMaps && depMaps.slice(0);

                this.errback = errback;

                //Indicate this module has be initialized
                this.inited = true;

                this.ignore = options.ignore;

                //Could have option to init this module in enabled mode,
                //or could have been previously marked as enabled. However,
                //the dependencies are not known until init is called. So
                //if enabled previously, now trigger dependencies as enabled.
                if (options.enabled || this.enabled) {
                    //Enable this module and dependencies.
                    //Will call this.check()
                    this.enable();
                } else {
                    this.check();
                }
            },

            defineDep: function (i, depExports) {
                //Because of cycles, defined callback for a given
                //export can be called more than once.
                if (!this.depMatched[i]) {
                    this.depMatched[i] = true;
                    this.depCount -= 1;
                    this.depExports[i] = depExports;
                }
            },

            fetch: function () {
                if (this.fetched) {
                    return;
                }
                this.fetched = true;

                context.startTime = (new Date()).getTime();

                var map = this.map;

                //If the manager is for a plugin managed resource,
                //ask the plugin to load it now.
                if (this.shim) {
                    context.makeRequire(this.map, {
                        enableBuildCallback: true
                    })(this.shim.deps || [], bind(this, function () {
                        return map.prefix ? this.callPlugin() : this.load();
                    }));
                } else {
                    //Regular dependency.
                    return map.prefix ? this.callPlugin() : this.load();
                }
            },

            load: function () {
                var url = this.map.url;

                //Regular dependency.
                if (!urlFetched[url]) {
                    urlFetched[url] = true;
                    context.load(this.map.id, url);
                }
            },

            /**
             * Checks if the module is ready to define itself, and if so,
             * define it.
             */
            check: function () {
                if (!this.enabled || this.enabling) {
                    return;
                }

                var err, cjsModule,
                    id = this.map.id,
                    depExports = this.depExports,
                    exports = this.exports,
                    factory = this.factory;

                if (!this.inited) {
                    // Only fetch if not already in the defQueue.
                    if (!hasProp(context.defQueueMap, id)) {
                        this.fetch();
                    }
                } else if (this.error) {
                    this.emit('error', this.error);
                } else if (!this.defining) {
                    //The factory could trigger another require call
                    //that would result in checking this module to
                    //define itself again. If already in the process
                    //of doing that, skip this work.
                    this.defining = true;

                    if (this.depCount < 1 && !this.defined) {
                        if (isFunction(factory)) {
                            //If there is an error listener, favor passing
                            //to that instead of throwing an error. However,
                            //only do it for define()'d  modules. require
                            //errbacks should not be called for failures in
                            //their callbacks (#699). However if a global
                            //onError is set, use that.
                            if ((this.events.error && this.map.isDefine) ||
                                req.onError !== defaultOnError) {
                                try {
                                    exports = context.execCb(id, factory, depExports, exports);
                                } catch (e) {
                                    err = e;
                                }
                            } else {
                                exports = context.execCb(id, factory, depExports, exports);
                            }

                            // Favor return value over exports. If node/cjs in play,
                            // then will not have a return value anyway. Favor
                            // module.exports assignment over exports object.
                            if (this.map.isDefine && exports === undefined) {
                                cjsModule = this.module;
                                if (cjsModule) {
                                    exports = cjsModule.exports;
                                } else if (this.usingExports) {
                                    //exports already set the defined value.
                                    exports = this.exports;
                                }
                            }

                            if (err) {
                                err.requireMap = this.map;
                                err.requireModules = this.map.isDefine ? [this.map.id] : null;
                                err.requireType = this.map.isDefine ? 'define' : 'require';
                                return onError((this.error = err));
                            }

                        } else {
                            //Just a literal value
                            exports = factory;
                        }

                        this.exports = exports;

                        if (this.map.isDefine && !this.ignore) {
                            defined[id] = exports;

                            if (req.onResourceLoad) {
                                var resLoadMaps = [];
                                each(this.depMaps, function (depMap) {
                                    resLoadMaps.push(depMap.normalizedMap || depMap);
                                });
                                req.onResourceLoad(context, this.map, resLoadMaps);
                            }
                        }

                        //Clean up
                        cleanRegistry(id);

                        this.defined = true;
                    }

                    //Finished the define stage. Allow calling check again
                    //to allow define notifications below in the case of a
                    //cycle.
                    this.defining = false;

                    if (this.defined && !this.defineEmitted) {
                        this.defineEmitted = true;
                        this.emit('defined', this.exports);
                        this.defineEmitComplete = true;
                    }

                }
            },

            callPlugin: function () {
                var map = this.map,
                    id = map.id,
                    //Map already normalized the prefix.
                    pluginMap = makeModuleMap(map.prefix);

                //Mark this as a dependency for this plugin, so it
                //can be traced for cycles.
                this.depMaps.push(pluginMap);

                on(pluginMap, 'defined', bind(this, function (plugin) {
                    var load, normalizedMap, normalizedMod,
                        bundleId = getOwn(bundlesMap, this.map.id),
                        name = this.map.name,
                        parentName = this.map.parentMap ? this.map.parentMap.name : null,
                        localRequire = context.makeRequire(map.parentMap, {
                            enableBuildCallback: true
                        });

                    //If current map is not normalized, wait for that
                    //normalized name to load instead of continuing.
                    if (this.map.unnormalized) {
                        //Normalize the ID if the plugin allows it.
                        if (plugin.normalize) {
                            name = plugin.normalize(name, function (name) {
                                return normalize(name, parentName, true);
                            }) || '';
                        }

                        //prefix and name should already be normalized, no need
                        //for applying map config again either.
                        normalizedMap = makeModuleMap(map.prefix + '!' + name,
                                                      this.map.parentMap,
                                                      true);
                        on(normalizedMap,
                            'defined', bind(this, function (value) {
                                this.map.normalizedMap = normalizedMap;
                                this.init([], function () { return value; }, null, {
                                    enabled: true,
                                    ignore: true
                                });
                            }));

                        normalizedMod = getOwn(registry, normalizedMap.id);
                        if (normalizedMod) {
                            //Mark this as a dependency for this plugin, so it
                            //can be traced for cycles.
                            this.depMaps.push(normalizedMap);

                            if (this.events.error) {
                                normalizedMod.on('error', bind(this, function (err) {
                                    this.emit('error', err);
                                }));
                            }
                            normalizedMod.enable();
                        }

                        return;
                    }

                    //If a paths config, then just load that file instead to
                    //resolve the plugin, as it is built into that paths layer.
                    if (bundleId) {
                        this.map.url = context.nameToUrl(bundleId);
                        this.load();
                        return;
                    }

                    load = bind(this, function (value) {
                        this.init([], function () { return value; }, null, {
                            enabled: true
                        });
                    });

                    load.error = bind(this, function (err) {
                        this.inited = true;
                        this.error = err;
                        err.requireModules = [id];

                        //Remove temp unnormalized modules for this module,
                        //since they will never be resolved otherwise now.
                        eachProp(registry, function (mod) {
                            if (mod.map.id.indexOf(id + '_unnormalized') === 0) {
                                cleanRegistry(mod.map.id);
                            }
                        });

                        onError(err);
                    });

                    //Allow plugins to load other code without having to know the
                    //context or how to 'complete' the load.
                    load.fromText = bind(this, function (text, textAlt) {
                        /*jslint evil: true */
                        var moduleName = map.name,
                            moduleMap = makeModuleMap(moduleName),
                            hasInteractive = useInteractive;

                        //As of 2.1.0, support just passing the text, to reinforce
                        //fromText only being called once per resource. Still
                        //support old style of passing moduleName but discard
                        //that moduleName in favor of the internal ref.
                        if (textAlt) {
                            text = textAlt;
                        }

                        //Turn off interactive script matching for IE for any define
                        //calls in the text, then turn it back on at the end.
                        if (hasInteractive) {
                            useInteractive = false;
                        }

                        //Prime the system by creating a module instance for
                        //it.
                        getModule(moduleMap);

                        //Transfer any config to this other module.
                        if (hasProp(config.config, id)) {
                            config.config[moduleName] = config.config[id];
                        }

                        try {
                            req.exec(text);
                        } catch (e) {
                            return onError(makeError('fromtexteval',
                                             'fromText eval for ' + id +
                                            ' failed: ' + e,
                                             e,
                                             [id]));
                        }

                        if (hasInteractive) {
                            useInteractive = true;
                        }

                        //Mark this as a dependency for the plugin
                        //resource
                        this.depMaps.push(moduleMap);

                        //Support anonymous modules.
                        context.completeLoad(moduleName);

                        //Bind the value of that module to the value for this
                        //resource ID.
                        localRequire([moduleName], load);
                    });

                    //Use parentName here since the plugin's name is not reliable,
                    //could be some weird string with no path that actually wants to
                    //reference the parentName's path.
                    plugin.load(map.name, localRequire, load, config);
                }));

                context.enable(pluginMap, this);
                this.pluginMaps[pluginMap.id] = pluginMap;
            },

            enable: function () {
                enabledRegistry[this.map.id] = this;
                this.enabled = true;

                //Set flag mentioning that the module is enabling,
                //so that immediate calls to the defined callbacks
                //for dependencies do not trigger inadvertent load
                //with the depCount still being zero.
                this.enabling = true;

                //Enable each dependency
                each(this.depMaps, bind(this, function (depMap, i) {
                    var id, mod, handler;

                    if (typeof depMap === 'string') {
                        //Dependency needs to be converted to a depMap
                        //and wired up to this module.
                        depMap = makeModuleMap(depMap,
                                               (this.map.isDefine ? this.map : this.map.parentMap),
                                               false,
                                               !this.skipMap);
                        this.depMaps[i] = depMap;

                        handler = getOwn(handlers, depMap.id);

                        if (handler) {
                            this.depExports[i] = handler(this);
                            return;
                        }

                        this.depCount += 1;

                        on(depMap, 'defined', bind(this, function (depExports) {
                            if (this.undefed) {
                                return;
                            }
                            this.defineDep(i, depExports);
                            this.check();
                        }));

                        if (this.errback) {
                            on(depMap, 'error', bind(this, this.errback));
                        } else if (this.events.error) {
                            // No direct errback on this module, but something
                            // else is listening for errors, so be sure to
                            // propagate the error correctly.
                            on(depMap, 'error', bind(this, function(err) {
                                this.emit('error', err);
                            }));
                        }
                    }

                    id = depMap.id;
                    mod = registry[id];

                    //Skip special modules like 'require', 'exports', 'module'
                    //Also, don't call enable if it is already enabled,
                    //important in circular dependency cases.
                    if (!hasProp(handlers, id) && mod && !mod.enabled) {
                        context.enable(depMap, this);
                    }
                }));

                //Enable each plugin that is used in
                //a dependency
                eachProp(this.pluginMaps, bind(this, function (pluginMap) {
                    var mod = getOwn(registry, pluginMap.id);
                    if (mod && !mod.enabled) {
                        context.enable(pluginMap, this);
                    }
                }));

                this.enabling = false;

                this.check();
            },

            on: function (name, cb) {
                var cbs = this.events[name];
                if (!cbs) {
                    cbs = this.events[name] = [];
                }
                cbs.push(cb);
            },

            emit: function (name, evt) {
                each(this.events[name], function (cb) {
                    cb(evt);
                });
                if (name === 'error') {
                    //Now that the error handler was triggered, remove
                    //the listeners, since this broken Module instance
                    //can stay around for a while in the registry.
                    delete this.events[name];
                }
            }
        };

        function callGetModule(args) {
            //Skip modules already defined.
            if (!hasProp(defined, args[0])) {
                getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
            }
        }

        function removeListener(node, func, name, ieName) {
            //Favor detachEvent because of IE9
            //issue, see attachEvent/addEventListener comment elsewhere
            //in this file.
            if (node.detachEvent && !isOpera) {
                //Probably IE. If not it will throw an error, which will be
                //useful to know.
                if (ieName) {
                    node.detachEvent(ieName, func);
                }
            } else {
                node.removeEventListener(name, func, false);
            }
        }

        /**
         * Given an event from a script node, get the requirejs info from it,
         * and then removes the event listeners on the node.
         * @param {Event} evt
         * @returns {Object}
         */
        function getScriptData(evt) {
            //Using currentTarget instead of target for Firefox 2.0's sake. Not
            //all old browsers will be supported, but this one was easy enough
            //to support and still makes sense.
            var node = evt.currentTarget || evt.srcElement;

            //Remove the listeners once here.
            removeListener(node, context.onScriptLoad, 'load', 'onreadystatechange');
            removeListener(node, context.onScriptError, 'error');

            return {
                node: node,
                id: node && node.getAttribute('data-requiremodule')
            };
        }

        function intakeDefines() {
            var args;

            //Any defined modules in the global queue, intake them now.
            takeGlobalQueue();

            //Make sure any remaining defQueue items get properly processed.
            while (defQueue.length) {
                args = defQueue.shift();
                if (args[0] === null) {
                    return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' +
                        args[args.length - 1]));
                } else {
                    //args are id, deps, factory. Should be normalized by the
                    //define() function.
                    callGetModule(args);
                }
            }
            context.defQueueMap = {};
        }

        context = {
            config: config,
            contextName: contextName,
            registry: registry,
            defined: defined,
            urlFetched: urlFetched,
            defQueue: defQueue,
            defQueueMap: {},
            Module: Module,
            makeModuleMap: makeModuleMap,
            nextTick: req.nextTick,
            onError: onError,

            /**
             * Set a configuration for the context.
             * @param {Object} cfg config object to integrate.
             */
            configure: function (cfg) {
                //Make sure the baseUrl ends in a slash.
                if (cfg.baseUrl) {
                    if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') {
                        cfg.baseUrl += '/';
                    }
                }

                // Convert old style urlArgs string to a function.
                if (typeof cfg.urlArgs === 'string') {
                    var urlArgs = cfg.urlArgs;
                    cfg.urlArgs = function(id, url) {
                        return (url.indexOf('?') === -1 ? '?' : '&') + urlArgs;
                    };
                }

                //Save off the paths since they require special processing,
                //they are additive.
                var shim = config.shim,
                    objs = {
                        paths: true,
                        bundles: true,
                        config: true,
                        map: true
                    };

                eachProp(cfg, function (value, prop) {
                    if (objs[prop]) {
                        if (!config[prop]) {
                            config[prop] = {};
                        }
                        mixin(config[prop], value, true, true);
                    } else {
                        config[prop] = value;
                    }
                });

                //Reverse map the bundles
                if (cfg.bundles) {
                    eachProp(cfg.bundles, function (value, prop) {
                        each(value, function (v) {
                            if (v !== prop) {
                                bundlesMap[v] = prop;
                            }
                        });
                    });
                }

                //Merge shim
                if (cfg.shim) {
                    eachProp(cfg.shim, function (value, id) {
                        //Normalize the structure
                        if (isArray(value)) {
                            value = {
                                deps: value
                            };
                        }
                        if ((value.exports || value.init) && !value.exportsFn) {
                            value.exportsFn = context.makeShimExports(value);
                        }
                        shim[id] = value;
                    });
                    config.shim = shim;
                }

                //Adjust packages if necessary.
                if (cfg.packages) {
                    each(cfg.packages, function (pkgObj) {
                        var location, name;

                        pkgObj = typeof pkgObj === 'string' ? {name: pkgObj} : pkgObj;

                        name = pkgObj.name;
                        location = pkgObj.location;
                        if (location) {
                            config.paths[name] = pkgObj.location;
                        }

                        //Save pointer to main module ID for pkg name.
                        //Remove leading dot in main, so main paths are normalized,
                        //and remove any trailing .js, since different package
                        //envs have different conventions: some use a module name,
                        //some use a file name.
                        config.pkgs[name] = pkgObj.name + '/' + (pkgObj.main || 'main')
                                     .replace(currDirRegExp, '')
                                     .replace(jsSuffixRegExp, '');
                    });
                }

                //If there are any "waiting to execute" modules in the registry,
                //update the maps for them, since their info, like URLs to load,
                //may have changed.
                eachProp(registry, function (mod, id) {
                    //If module already has init called, since it is too
                    //late to modify them, and ignore unnormalized ones
                    //since they are transient.
                    if (!mod.inited && !mod.map.unnormalized) {
                        mod.map = makeModuleMap(id, null, true);
                    }
                });

                //If a deps array or a config callback is specified, then call
                //require with those args. This is useful when require is defined as a
                //config object before require.js is loaded.
                if (cfg.deps || cfg.callback) {
                    context.require(cfg.deps || [], cfg.callback);
                }
            },

            makeShimExports: function (value) {
                function fn() {
                    var ret;
                    if (value.init) {
                        ret = value.init.apply(global, arguments);
                    }
                    return ret || (value.exports && getGlobal(value.exports));
                }
                return fn;
            },

            makeRequire: function (relMap, options) {
                options = options || {};

                function localRequire(deps, callback, errback) {
                    var id, map, requireMod;

                    if (options.enableBuildCallback && callback && isFunction(callback)) {
                        callback.__requireJsBuild = true;
                    }

                    if (typeof deps === 'string') {
                        if (isFunction(callback)) {
                            //Invalid call
                            return onError(makeError('requireargs', 'Invalid require call'), errback);
                        }

                        //If require|exports|module are requested, get the
                        //value for them from the special handlers. Caveat:
                        //this only works while module is being defined.
                        if (relMap && hasProp(handlers, deps)) {
                            return handlers[deps](registry[relMap.id]);
                        }

                        //Synchronous access to one module. If require.get is
                        //available (as in the Node adapter), prefer that.
                        if (req.get) {
                            return req.get(context, deps, relMap, localRequire);
                        }

                        //Normalize module name, if it contains . or ..
                        map = makeModuleMap(deps, relMap, false, true);
                        id = map.id;

                        if (!hasProp(defined, id)) {
                            return onError(makeError('notloaded', 'Module name "' +
                                        id +
                                        '" has not been loaded yet for context: ' +
                                        contextName +
                                        (relMap ? '' : '. Use require([])')));
                        }
                        return defined[id];
                    }

                    //Grab defines waiting in the global queue.
                    intakeDefines();

                    //Mark all the dependencies as needing to be loaded.
                    context.nextTick(function () {
                        //Some defines could have been added since the
                        //require call, collect them.
                        intakeDefines();

                        requireMod = getModule(makeModuleMap(null, relMap));

                        //Store if map config should be applied to this require
                        //call for dependencies.
                        requireMod.skipMap = options.skipMap;

                        requireMod.init(deps, callback, errback, {
                            enabled: true
                        });

                        checkLoaded();
                    });

                    return localRequire;
                }

                mixin(localRequire, {
                    isBrowser: isBrowser,

                    /**
                     * Converts a module name + .extension into an URL path.
                     * *Requires* the use of a module name. It does not support using
                     * plain URLs like nameToUrl.
                     */
                    toUrl: function (moduleNamePlusExt) {
                        var ext,
                            index = moduleNamePlusExt.lastIndexOf('.'),
                            segment = moduleNamePlusExt.split('/')[0],
                            isRelative = segment === '.' || segment === '..';

                        //Have a file extension alias, and it is not the
                        //dots from a relative path.
                        if (index !== -1 && (!isRelative || index > 1)) {
                            ext = moduleNamePlusExt.substring(index, moduleNamePlusExt.length);
                            moduleNamePlusExt = moduleNamePlusExt.substring(0, index);
                        }

                        return context.nameToUrl(normalize(moduleNamePlusExt,
                                                relMap && relMap.id, true), ext,  true);
                    },

                    defined: function (id) {
                        return hasProp(defined, makeModuleMap(id, relMap, false, true).id);
                    },

                    specified: function (id) {
                        id = makeModuleMap(id, relMap, false, true).id;
                        return hasProp(defined, id) || hasProp(registry, id);
                    }
                });

                //Only allow undef on top level require calls
                if (!relMap) {
                    localRequire.undef = function (id) {
                        //Bind any waiting define() calls to this context,
                        //fix for #408
                        takeGlobalQueue();

                        var map = makeModuleMap(id, relMap, true),
                            mod = getOwn(registry, id);

                        mod.undefed = true;
                        removeScript(id);

                        delete defined[id];
                        delete urlFetched[map.url];
                        delete undefEvents[id];

                        //Clean queued defines too. Go backwards
                        //in array so that the splices do not
                        //mess up the iteration.
                        eachReverse(defQueue, function(args, i) {
                            if (args[0] === id) {
                                defQueue.splice(i, 1);
                            }
                        });
                        delete context.defQueueMap[id];

                        if (mod) {
                            //Hold on to listeners in case the
                            //module will be attempted to be reloaded
                            //using a different config.
                            if (mod.events.defined) {
                                undefEvents[id] = mod.events;
                            }

                            cleanRegistry(id);
                        }
                    };
                }

                return localRequire;
            },

            /**
             * Called to enable a module if it is still in the registry
             * awaiting enablement. A second arg, parent, the parent module,
             * is passed in for context, when this method is overridden by
             * the optimizer. Not shown here to keep code compact.
             */
            enable: function (depMap) {
                var mod = getOwn(registry, depMap.id);
                if (mod) {
                    getModule(depMap).enable();
                }
            },

            /**
             * Internal method used by environment adapters to complete a load event.
             * A load event could be a script load or just a load pass from a synchronous
             * load call.
             * @param {String} moduleName the name of the module to potentially complete.
             */
            completeLoad: function (moduleName) {
                var found, args, mod,
                    shim = getOwn(config.shim, moduleName) || {},
                    shExports = shim.exports;

                takeGlobalQueue();

                while (defQueue.length) {
                    args = defQueue.shift();
                    if (args[0] === null) {
                        args[0] = moduleName;
                        //If already found an anonymous module and bound it
                        //to this name, then this is some other anon module
                        //waiting for its completeLoad to fire.
                        if (found) {
                            break;
                        }
                        found = true;
                    } else if (args[0] === moduleName) {
                        //Found matching define call for this script!
                        found = true;
                    }

                    callGetModule(args);
                }
                context.defQueueMap = {};

                //Do this after the cycle of callGetModule in case the result
                //of those calls/init calls changes the registry.
                mod = getOwn(registry, moduleName);

                if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) {
                    if (config.enforceDefine && (!shExports || !getGlobal(shExports))) {
                        if (hasPathFallback(moduleName)) {
                            return;
                        } else {
                            return onError(makeError('nodefine',
                                             'No define call for ' + moduleName,
                                             null,
                                             [moduleName]));
                        }
                    } else {
                        //A script that does not call define(), so just simulate
                        //the call for it.
                        callGetModule([moduleName, (shim.deps || []), shim.exportsFn]);
                    }
                }

                checkLoaded();
            },

            /**
             * Converts a module name to a file path. Supports cases where
             * moduleName may actually be just an URL.
             * Note that it **does not** call normalize on the moduleName,
             * it is assumed to have already been normalized. This is an
             * internal API, not a public one. Use toUrl for the public API.
             */
            nameToUrl: function (moduleName, ext, skipExt) {
                var paths, syms, i, parentModule, url,
                    parentPath, bundleId,
                    pkgMain = getOwn(config.pkgs, moduleName);

                if (pkgMain) {
                    moduleName = pkgMain;
                }

                bundleId = getOwn(bundlesMap, moduleName);

                if (bundleId) {
                    return context.nameToUrl(bundleId, ext, skipExt);
                }

                //If a colon is in the URL, it indicates a protocol is used and it is just
                //an URL to a file, or if it starts with a slash, contains a query arg (i.e. ?)
                //or ends with .js, then assume the user meant to use an url and not a module id.
                //The slash is important for protocol-less URLs as well as full paths.
                if (req.jsExtRegExp.test(moduleName)) {
                    //Just a plain path, not module name lookup, so just return it.
                    //Add extension if it is included. This is a bit wonky, only non-.js things pass
                    //an extension, this method probably needs to be reworked.
                    url = moduleName + (ext || '');
                } else {
                    //A module that needs to be converted to a path.
                    paths = config.paths;

                    syms = moduleName.split('/');
                    //For each module name segment, see if there is a path
                    //registered for it. Start with most specific name
                    //and work up from it.
                    for (i = syms.length; i > 0; i -= 1) {
                        parentModule = syms.slice(0, i).join('/');

                        parentPath = getOwn(paths, parentModule);
                        if (parentPath) {
                            //If an array, it means there are a few choices,
                            //Choose the one that is desired
                            if (isArray(parentPath)) {
                                parentPath = parentPath[0];
                            }
                            syms.splice(0, i, parentPath);
                            break;
                        }
                    }

                    //Join the path parts together, then figure out if baseUrl is needed.
                    url = syms.join('/');
                    url += (ext || (/^data\:|^blob\:|\?/.test(url) || skipExt ? '' : '.js'));
                    url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? '' : config.baseUrl) + url;
                }

                return config.urlArgs && !/^blob\:/.test(url) ?
                       url + config.urlArgs(moduleName, url) : url;
            },

            //Delegates to req.load. Broken out as a separate function to
            //allow overriding in the optimizer.
            load: function (id, url) {
                req.load(context, id, url);
            },

            /**
             * Executes a module callback function. Broken out as a separate function
             * solely to allow the build system to sequence the files in the built
             * layer in the right sequence.
             *
             * @private
             */
            execCb: function (name, callback, args, exports) {
                return callback.apply(exports, args);
            },

            /**
             * callback for script loads, used to check status of loading.
             *
             * @param {Event} evt the event from the browser for the script
             * that was loaded.
             */
            onScriptLoad: function (evt) {
                //Using currentTarget instead of target for Firefox 2.0's sake. Not
                //all old browsers will be supported, but this one was easy enough
                //to support and still makes sense.
                if (evt.type === 'load' ||
                        (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
                    //Reset interactive script so a script node is not held onto for
                    //to long.
                    interactiveScript = null;

                    //Pull out the name of the module and the context.
                    var data = getScriptData(evt);
                    context.completeLoad(data.id);
                }
            },

            /**
             * Callback for script errors.
             */
            onScriptError: function (evt) {
                var data = getScriptData(evt);
                if (!hasPathFallback(data.id)) {
                    var parents = [];
                    eachProp(registry, function(value, key) {
                        if (key.indexOf('_@r') !== 0) {
                            each(value.depMaps, function(depMap) {
                                if (depMap.id === data.id) {
                                    parents.push(key);
                                    return true;
                                }
                            });
                        }
                    });
                    return onError(makeError('scripterror', 'Script error for "' + data.id +
                                             (parents.length ?
                                             '", needed by: ' + parents.join(', ') :
                                             '"'), evt, [data.id]));
                }
            }
        };

        context.require = context.makeRequire();
        return context;
    }

    /**
     * Main entry point.
     *
     * If the only argument to require is a string, then the module that
     * is represented by that string is fetched for the appropriate context.
     *
     * If the first argument is an array, then it will be treated as an array
     * of dependency string names to fetch. An optional function callback can
     * be specified to execute when all of those dependencies are available.
     *
     * Make a local req variable to help Caja compliance (it assumes things
     * on a require that are not standardized), and to give a short
     * name for minification/local scope use.
     */
    req = requirejs = function (deps, callback, errback, optional) {

        //Find the right context, use default
        var context, config,
            contextName = defContextName;

        // Determine if have config object in the call.
        if (!isArray(deps) && typeof deps !== 'string') {
            // deps is a config object
            config = deps;
            if (isArray(callback)) {
                // Adjust args if there are dependencies
                deps = callback;
                callback = errback;
                errback = optional;
            } else {
                deps = [];
            }
        }

        if (config && config.context) {
            contextName = config.context;
        }

        context = getOwn(contexts, contextName);
        if (!context) {
            context = contexts[contextName] = req.s.newContext(contextName);
        }

        if (config) {
            context.configure(config);
        }

        return context.require(deps, callback, errback);
    };

    /**
     * Support rjs.require.config() to make it easier to cooperate with other
     * AMD loaders on globally agreed names.
     */
    req.config = function (config) {
        return req(config);
    };

    /**
     * Execute something after the current tick
     * of the event loop. Override for other envs
     * that have a better solution than setTimeout.
     * @param  {Function} fn function to execute later.
     */
    req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) {
        setTimeout(fn, 4);
    } : function (fn) { fn(); };

    /**
     * Export require as a global, but only if it does not already exist.
     */
    if (!require) {
        require = req;
    }

    req.version = version;

    //Used to filter out dependencies that are already paths.
    req.jsExtRegExp = /^\/|:|\?|\.js$/;
    req.isBrowser = isBrowser;
    s = req.s = {
        contexts: contexts,
        newContext: newContext
    };

    //Create default context.
    req({});

    //Exports some context-sensitive methods on global require.
    each([
        'toUrl',
        'undef',
        'defined',
        'specified'
    ], function (prop) {
        //Reference from contexts instead of early binding to default context,
        //so that during builds, the latest instance of the default context
        //with its config gets used.
        req[prop] = function () {
            var ctx = contexts[defContextName];
            return ctx.require[prop].apply(ctx, arguments);
        };
    });

    if (isBrowser) {
        head = s.head = document.getElementsByTagName('head')[0];
        //If BASE tag is in play, using appendChild is a problem for IE6.
        //When that browser dies, this can be removed. Details in this jQuery bug:
        //http://dev.jquery.com/ticket/2709
        baseElement = document.getElementsByTagName('base')[0];
        if (baseElement) {
            head = s.head = baseElement.parentNode;
        }
    }

    /**
     * Any errors that require explicitly generates will be passed to this
     * function. Intercept/override it if you want custom error handling.
     * @param {Error} err the error object.
     */
    req.onError = defaultOnError;

    /**
     * Creates the node for the load command. Only used in browser envs.
     */
    req.createNode = function (config, moduleName, url) {
        var node = config.xhtml ?
                document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
                document.createElement('script');
        node.type = config.scriptType || 'text/javascript';
        node.charset = 'utf-8';
        node.async = true;
        return node;
    };

    /**
     * Does the request to load a module for the browser case.
     * Make this a separate function to allow other environments
     * to override it.
     *
     * @param {Object} context the require context to find state.
     * @param {String} moduleName the name of the module.
     * @param {Object} url the URL to the module.
     */
    req.load = function (context, moduleName, url) {
        var config = (context && context.config) || {},
            node;
        if (isBrowser) {
            //In the browser so use a script tag
            node = req.createNode(config, moduleName, url);

            node.setAttribute('data-requirecontext', context.contextName);
            node.setAttribute('data-requiremodule', moduleName);

            //Set up load listener. Test attachEvent first because IE9 has
            //a subtle issue in its addEventListener and script onload firings
            //that do not match the behavior of all other browsers with
            //addEventListener support, which fire the onload event for a
            //script right after the script execution. See:
            //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution
            //UNFORTUNATELY Opera implements attachEvent but does not follow the script
            //script execution mode.
            if (node.attachEvent &&
                    //Check if node.attachEvent is artificially added by custom script or
                    //natively supported by browser
                    //read https://github.com/requirejs/requirejs/issues/187
                    //if we can NOT find [native code] then it must NOT natively supported.
                    //in IE8, node.attachEvent does not have toString()
                    //Note the test for "[native code" with no closing brace, see:
                    //https://github.com/requirejs/requirejs/issues/273
                    !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
                    !isOpera) {
                //Probably IE. IE (at least 6-8) do not fire
                //script onload right after executing the script, so
                //we cannot tie the anonymous define call to a name.
                //However, IE reports the script as being in 'interactive'
                //readyState at the time of the define call.
                useInteractive = true;

                node.attachEvent('onreadystatechange', context.onScriptLoad);
                //It would be great to add an error handler here to catch
                //404s in IE9+. However, onreadystatechange will fire before
                //the error handler, so that does not help. If addEventListener
                //is used, then IE will fire error before load, but we cannot
                //use that pathway given the connect.microsoft.com issue
                //mentioned above about not doing the 'script execute,
                //then fire the script load event listener before execute
                //next script' that other browsers do.
                //Best hope: IE10 fixes the issues,
                //and then destroys all installs of IE 6-9.
                //node.attachEvent('onerror', context.onScriptError);
            } else {
                node.addEventListener('load', context.onScriptLoad, false);
                node.addEventListener('error', context.onScriptError, false);
            }
            node.src = url;

            //Calling onNodeCreated after all properties on the node have been
            //set, but before it is placed in the DOM.
            if (config.onNodeCreated) {
                config.onNodeCreated(node, config, moduleName, url);
            }

            //For some cache cases in IE 6-8, the script executes before the end
            //of the appendChild execution, so to tie an anonymous define
            //call to the module name (which is stored on the node), hold on
            //to a reference to this node, but clear after the DOM insertion.
            currentlyAddingScript = node;
            if (baseElement) {
                head.insertBefore(node, baseElement);
            } else {
                head.appendChild(node);
            }
            currentlyAddingScript = null;

            return node;
        } else if (isWebWorker) {
            try {
                //In a web worker, use importScripts. This is not a very
                //efficient use of importScripts, importScripts will block until
                //its script is downloaded and evaluated. However, if web workers
                //are in play, the expectation is that a build has been done so
                //that only one script needs to be loaded anyway. This may need
                //to be reevaluated if other use cases become common.

                // Post a task to the event loop to work around a bug in WebKit
                // where the worker gets garbage-collected after calling
                // importScripts(): https://webkit.org/b/153317
                setTimeout(function() {}, 0);
                importScripts(url);

                //Account for anonymous modules
                context.completeLoad(moduleName);
            } catch (e) {
                context.onError(makeError('importscripts',
                                'importScripts failed for ' +
                                    moduleName + ' at ' + url,
                                e,
                                [moduleName]));
            }
        }
    };

    function getInteractiveScript() {
        if (interactiveScript && interactiveScript.readyState === 'interactive') {
            return interactiveScript;
        }

        eachReverse(scripts(), function (script) {
            if (script.readyState === 'interactive') {
                return (interactiveScript = script);
            }
        });
        return interactiveScript;
    }

    //Look for a data-main script attribute, which could also adjust the baseUrl.
    if (isBrowser && !cfg.skipDataMain) {
        //Figure out baseUrl. Get it from the script tag with require.js in it.
        eachReverse(scripts(), function (script) {
            //Set the 'head' where we can append children by
            //using the script's parent.
            if (!head) {
                head = script.parentNode;
            }

            //Look for a data-main attribute to set main script for the page
            //to load. If it is there, the path to data main becomes the
            //baseUrl, if it is not already set.
            dataMain = script.getAttribute('data-main');
            if (dataMain) {
                //Preserve dataMain in case it is a path (i.e. contains '?')
                mainScript = dataMain;

                //Set final baseUrl if there is not already an explicit one,
                //but only do so if the data-main value is not a loader plugin
                //module ID.
                if (!cfg.baseUrl && mainScript.indexOf('!') === -1) {
                    //Pull off the directory of data-main for use as the
                    //baseUrl.
                    src = mainScript.split('/');
                    mainScript = src.pop();
                    subPath = src.length ? src.join('/')  + '/' : './';

                    cfg.baseUrl = subPath;
                }

                //Strip off any trailing .js since mainScript is now
                //like a module name.
                mainScript = mainScript.replace(jsSuffixRegExp, '');

                //If mainScript is still a path, fall back to dataMain
                if (req.jsExtRegExp.test(mainScript)) {
                    mainScript = dataMain;
                }

                //Put the data-main script in the files to load.
                cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];

                return true;
            }
        });
    }

    /**
     * The function that handles definitions of modules. Differs from
     * require() in that a string for the module should be the first argument,
     * and the function to execute after dependencies are loaded should
     * return a value to define the module corresponding to the first argument's
     * name.
     */
    define = function (name, deps, callback) {
        var node, context;

        //Allow for anonymous modules
        if (typeof name !== 'string') {
            //Adjust args appropriately
            callback = deps;
            deps = name;
            name = null;
        }

        //This module may not have dependencies
        if (!isArray(deps)) {
            callback = deps;
            deps = null;
        }

        //If no name, and callback is a function, then figure out if it a
        //CommonJS thing with dependencies.
        if (!deps && isFunction(callback)) {
            deps = [];
            //Remove comments from the callback string,
            //look for require calls, and pull them into the dependencies,
            //but only if there are function args.
            if (callback.length) {
                callback
                    .toString()
                    .replace(commentRegExp, commentReplace)
                    .replace(cjsRequireRegExp, function (match, dep) {
                        deps.push(dep);
                    });

                //May be a CommonJS thing even without require calls, but still
                //could use exports, and module. Avoid doing exports and module
                //work though if it just needs require.
                //REQUIRES the function to expect the CommonJS variables in the
                //order listed below.
                deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps);
            }
        }

        //If in IE 6-8 and hit an anonymous define() call, do the interactive
        //work.
        if (useInteractive) {
            node = currentlyAddingScript || getInteractiveScript();
            if (node) {
                if (!name) {
                    name = node.getAttribute('data-requiremodule');
                }
                context = contexts[node.getAttribute('data-requirecontext')];
            }
        }

        //Always save off evaluating the def call until the script onload handler.
        //This allows multiple modules to be in a file without prematurely
        //tracing dependencies, and allows for anonymous module support,
        //where the module name is not known until the script onload event
        //occurs. If no context, use the global queue, and get it processed
        //in the onscript load callback.
        if (context) {
            context.defQueue.push([name, deps, callback]);
            context.defQueueMap[name] = true;
        } else {
            globalDefQueue.push([name, deps, callback]);
        }
    };

    define.amd = {
        jQuery: true
    };

    /**
     * Executes the text. Normally just uses eval, but can be modified
     * to use a better, environment-specific call. Only used for transpiling
     * loader plugins, not for plain JS modules.
     * @param {String} text the text to execute/evaluate.
     */
    req.exec = function (text) {
        /*jslint evil: true */
        return eval(text);
    };

    //Set up with config info.
    req(cfg);
}(this, (typeof setTimeout === 'undefined' ? undefined : setTimeout)));

rjs.requirejs = requirejs;rjs.require = require;rjs.define = define;
}
}());
rjs.define("requireLib", function(){});

/*!
 * jQuery JavaScript Library v3.2.0
 * https://jquery.com/
 *
 * Includes Sizzle.js
 * https://sizzlejs.com/
 *
 * Copyright JS Foundation and other contributors
 * Released under the MIT license
 * https://jquery.org/license
 *
 * Date: 2017-03-16T21:26Z
 */
( function( global, factory ) {

	"use strict";

	if ( typeof module === "object" && typeof module.exports === "object" ) {

		// For CommonJS and CommonJS-like environments where a proper `window`
		// is present, execute the factory and get jQuery.
		// For environments that do not have a `window` with a `document`
		// (such as Node.js), expose a factory as module.exports.
		// This accentuates the need for the creation of a real `window`.
		// e.g. var jQuery = require("jquery")(window);
		// See ticket #14549 for more info.
		module.exports = global.document ?
			factory( global, true ) :
			function( w ) {
				if ( !w.document ) {
					throw new Error( "jQuery requires a window with a document" );
				}
				return factory( w );
			};
	} else {
		factory( global );
	}

// Pass this if window is not defined yet
} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {

// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1
// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode
// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common
// enough that all such attempts are guarded in a try block.


var arr = [];

var document = window.document;

var getProto = Object.getPrototypeOf;

var slice = arr.slice;

var concat = arr.concat;

var push = arr.push;

var indexOf = arr.indexOf;

var class2type = {};

var toString = class2type.toString;

var hasOwn = class2type.hasOwnProperty;

var fnToString = hasOwn.toString;

var ObjectFunctionString = fnToString.call( Object );

var support = {};



	function DOMEval( code, doc ) {
		doc = doc || document;

		var script = doc.createElement( "script" );

		script.text = code;
		doc.head.appendChild( script ).parentNode.removeChild( script );
	}
/* global Symbol */
// Defining this global in .eslintrc.json would create a danger of using the global
// unguarded in another place, it seems safer to define global only for this module



var
	version = "3.2.0",

	// Define a local copy of jQuery
	jQuery = function( selector, context ) {

		// The jQuery object is actually just the init constructor 'enhanced'
		// Need init if jQuery is called (just allow error to be thrown if not included)
		return new jQuery.fn.init( selector, context );
	},

	// Support: Android <=4.0 only
	// Make sure we trim BOM and NBSP
	rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,

	// Matches dashed string for camelizing
	rmsPrefix = /^-ms-/,
	rdashAlpha = /-([a-z])/g,

	// Used by jQuery.camelCase as callback to replace()
	fcamelCase = function( all, letter ) {
		return letter.toUpperCase();
	};

jQuery.fn = jQuery.prototype = {

	// The current version of jQuery being used
	jquery: version,

	constructor: jQuery,

	// The default length of a jQuery object is 0
	length: 0,

	toArray: function() {
		return slice.call( this );
	},

	// Get the Nth element in the matched element set OR
	// Get the whole matched element set as a clean array
	get: function( num ) {

		// Return all the elements in a clean array
		if ( num == null ) {
			return slice.call( this );
		}

		// Return just the one element from the set
		return num < 0 ? this[ num + this.length ] : this[ num ];
	},

	// Take an array of elements and push it onto the stack
	// (returning the new matched element set)
	pushStack: function( elems ) {

		// Build a new jQuery matched element set
		var ret = jQuery.merge( this.constructor(), elems );

		// Add the old object onto the stack (as a reference)
		ret.prevObject = this;

		// Return the newly-formed element set
		return ret;
	},

	// Execute a callback for every element in the matched set.
	each: function( callback ) {
		return jQuery.each( this, callback );
	},

	map: function( callback ) {
		return this.pushStack( jQuery.map( this, function( elem, i ) {
			return callback.call( elem, i, elem );
		} ) );
	},

	slice: function() {
		return this.pushStack( slice.apply( this, arguments ) );
	},

	first: function() {
		return this.eq( 0 );
	},

	last: function() {
		return this.eq( -1 );
	},

	eq: function( i ) {
		var len = this.length,
			j = +i + ( i < 0 ? len : 0 );
		return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] );
	},

	end: function() {
		return this.prevObject || this.constructor();
	},

	// For internal use only.
	// Behaves like an Array's method, not like a jQuery method.
	push: push,
	sort: arr.sort,
	splice: arr.splice
};

jQuery.extend = jQuery.fn.extend = function() {
	var options, name, src, copy, copyIsArray, clone,
		target = arguments[ 0 ] || {},
		i = 1,
		length = arguments.length,
		deep = false;

	// Handle a deep copy situation
	if ( typeof target === "boolean" ) {
		deep = target;

		// Skip the boolean and the target
		target = arguments[ i ] || {};
		i++;
	}

	// Handle case when target is a string or something (possible in deep copy)
	if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
		target = {};
	}

	// Extend jQuery itself if only one argument is passed
	if ( i === length ) {
		target = this;
		i--;
	}

	for ( ; i < length; i++ ) {

		// Only deal with non-null/undefined values
		if ( ( options = arguments[ i ] ) != null ) {

			// Extend the base object
			for ( name in options ) {
				src = target[ name ];
				copy = options[ name ];

				// Prevent never-ending loop
				if ( target === copy ) {
					continue;
				}

				// Recurse if we're merging plain objects or arrays
				if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
					( copyIsArray = Array.isArray( copy ) ) ) ) {

					if ( copyIsArray ) {
						copyIsArray = false;
						clone = src && Array.isArray( src ) ? src : [];

					} else {
						clone = src && jQuery.isPlainObject( src ) ? src : {};
					}

					// Never move original objects, clone them
					target[ name ] = jQuery.extend( deep, clone, copy );

				// Don't bring in undefined values
				} else if ( copy !== undefined ) {
					target[ name ] = copy;
				}
			}
		}
	}

	// Return the modified object
	return target;
};

jQuery.extend( {

	// Unique for each copy of jQuery on the page
	expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),

	// Assume jQuery is ready without the ready module
	isReady: true,

	error: function( msg ) {
		throw new Error( msg );
	},

	noop: function() {},

	isFunction: function( obj ) {
		return jQuery.type( obj ) === "function";
	},

	isWindow: function( obj ) {
		return obj != null && obj === obj.window;
	},

	isNumeric: function( obj ) {

		// As of jQuery 3.0, isNumeric is limited to
		// strings and numbers (primitives or objects)
		// that can be coerced to finite numbers (gh-2662)
		var type = jQuery.type( obj );
		return ( type === "number" || type === "string" ) &&

			// parseFloat NaNs numeric-cast false positives ("")
			// ...but misinterprets leading-number strings, particularly hex literals ("0x...")
			// subtraction forces infinities to NaN
			!isNaN( obj - parseFloat( obj ) );
	},

	isPlainObject: function( obj ) {
		var proto, Ctor;

		// Detect obvious negatives
		// Use toString instead of jQuery.type to catch host objects
		if ( !obj || toString.call( obj ) !== "[object Object]" ) {
			return false;
		}

		proto = getProto( obj );

		// Objects with no prototype (e.g., `Object.create( null )`) are plain
		if ( !proto ) {
			return true;
		}

		// Objects with prototype are plain iff they were constructed by a global Object function
		Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
		return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
	},

	isEmptyObject: function( obj ) {

		/* eslint-disable no-unused-vars */
		// See https://github.com/eslint/eslint/issues/6125
		var name;

		for ( name in obj ) {
			return false;
		}
		return true;
	},

	type: function( obj ) {
		if ( obj == null ) {
			return obj + "";
		}

		// Support: Android <=2.3 only (functionish RegExp)
		return typeof obj === "object" || typeof obj === "function" ?
			class2type[ toString.call( obj ) ] || "object" :
			typeof obj;
	},

	// Evaluates a script in a global context
	globalEval: function( code ) {
		DOMEval( code );
	},

	// Convert dashed to camelCase; used by the css and data modules
	// Support: IE <=9 - 11, Edge 12 - 13
	// Microsoft forgot to hump their vendor prefix (#9572)
	camelCase: function( string ) {
		return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
	},

	each: function( obj, callback ) {
		var length, i = 0;

		if ( isArrayLike( obj ) ) {
			length = obj.length;
			for ( ; i < length; i++ ) {
				if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
					break;
				}
			}
		} else {
			for ( i in obj ) {
				if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
					break;
				}
			}
		}

		return obj;
	},

	// Support: Android <=4.0 only
	trim: function( text ) {
		return text == null ?
			"" :
			( text + "" ).replace( rtrim, "" );
	},

	// results is for internal usage only
	makeArray: function( arr, results ) {
		var ret = results || [];

		if ( arr != null ) {
			if ( isArrayLike( Object( arr ) ) ) {
				jQuery.merge( ret,
					typeof arr === "string" ?
					[ arr ] : arr
				);
			} else {
				push.call( ret, arr );
			}
		}

		return ret;
	},

	inArray: function( elem, arr, i ) {
		return arr == null ? -1 : indexOf.call( arr, elem, i );
	},

	// Support: Android <=4.0 only, PhantomJS 1 only
	// push.apply(_, arraylike) throws on ancient WebKit
	merge: function( first, second ) {
		var len = +second.length,
			j = 0,
			i = first.length;

		for ( ; j < len; j++ ) {
			first[ i++ ] = second[ j ];
		}

		first.length = i;

		return first;
	},

	grep: function( elems, callback, invert ) {
		var callbackInverse,
			matches = [],
			i = 0,
			length = elems.length,
			callbackExpect = !invert;

		// Go through the array, only saving the items
		// that pass the validator function
		for ( ; i < length; i++ ) {
			callbackInverse = !callback( elems[ i ], i );
			if ( callbackInverse !== callbackExpect ) {
				matches.push( elems[ i ] );
			}
		}

		return matches;
	},

	// arg is for internal usage only
	map: function( elems, callback, arg ) {
		var length, value,
			i = 0,
			ret = [];

		// Go through the array, translating each of the items to their new values
		if ( isArrayLike( elems ) ) {
			length = elems.length;
			for ( ; i < length; i++ ) {
				value = callback( elems[ i ], i, arg );

				if ( value != null ) {
					ret.push( value );
				}
			}

		// Go through every key on the object,
		} else {
			for ( i in elems ) {
				value = callback( elems[ i ], i, arg );

				if ( value != null ) {
					ret.push( value );
				}
			}
		}

		// Flatten any nested arrays
		return concat.apply( [], ret );
	},

	// A global GUID counter for objects
	guid: 1,

	// Bind a function to a context, optionally partially applying any
	// arguments.
	proxy: function( fn, context ) {
		var tmp, args, proxy;

		if ( typeof context === "string" ) {
			tmp = fn[ context ];
			context = fn;
			fn = tmp;
		}

		// Quick check to determine if target is callable, in the spec
		// this throws a TypeError, but we will just return undefined.
		if ( !jQuery.isFunction( fn ) ) {
			return undefined;
		}

		// Simulated bind
		args = slice.call( arguments, 2 );
		proxy = function() {
			return fn.apply( context || this, args.concat( slice.call( arguments ) ) );
		};

		// Set the guid of unique handler to the same of original handler, so it can be removed
		proxy.guid = fn.guid = fn.guid || jQuery.guid++;

		return proxy;
	},

	now: Date.now,

	// jQuery.support is not used in Core but other projects attach their
	// properties to it so it needs to exist.
	support: support
} );

if ( typeof Symbol === "function" ) {
	jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ];
}

// Populate the class2type map
jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
function( i, name ) {
	class2type[ "[object " + name + "]" ] = name.toLowerCase();
} );

function isArrayLike( obj ) {

	// Support: real iOS 8.2 only (not reproducible in simulator)
	// `in` check used to prevent JIT error (gh-2145)
	// hasOwn isn't used here due to false negatives
	// regarding Nodelist length in IE
	var length = !!obj && "length" in obj && obj.length,
		type = jQuery.type( obj );

	if ( type === "function" || jQuery.isWindow( obj ) ) {
		return false;
	}

	return type === "array" || length === 0 ||
		typeof length === "number" && length > 0 && ( length - 1 ) in obj;
}
var Sizzle =
/*!
 * Sizzle CSS Selector Engine v2.3.3
 * https://sizzlejs.com/
 *
 * Copyright jQuery Foundation and other contributors
 * Released under the MIT license
 * http://jquery.org/license
 *
 * Date: 2016-08-08
 */
(function( window ) {

var i,
	support,
	Expr,
	getText,
	isXML,
	tokenize,
	compile,
	select,
	outermostContext,
	sortInput,
	hasDuplicate,

	// Local document vars
	setDocument,
	document,
	docElem,
	documentIsHTML,
	rbuggyQSA,
	rbuggyMatches,
	matches,
	contains,

	// Instance-specific data
	expando = "sizzle" + 1 * new Date(),
	preferredDoc = window.document,
	dirruns = 0,
	done = 0,
	classCache = createCache(),
	tokenCache = createCache(),
	compilerCache = createCache(),
	sortOrder = function( a, b ) {
		if ( a === b ) {
			hasDuplicate = true;
		}
		return 0;
	},

	// Instance methods
	hasOwn = ({}).hasOwnProperty,
	arr = [],
	pop = arr.pop,
	push_native = arr.push,
	push = arr.push,
	slice = arr.slice,
	// Use a stripped-down indexOf as it's faster than native
	// https://jsperf.com/thor-indexof-vs-for/5
	indexOf = function( list, elem ) {
		var i = 0,
			len = list.length;
		for ( ; i < len; i++ ) {
			if ( list[i] === elem ) {
				return i;
			}
		}
		return -1;
	},

	booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",

	// Regular expressions

	// http://www.w3.org/TR/css3-selectors/#whitespace
	whitespace = "[\\x20\\t\\r\\n\\f]",

	// http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
	identifier = "(?:\\\\.|[\\w-]|[^\0-\\xa0])+",

	// Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
	attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
		// Operator (capture 2)
		"*([*^$|!~]?=)" + whitespace +
		// "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]"
		"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace +
		"*\\]",

	pseudos = ":(" + identifier + ")(?:\\((" +
		// To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
		// 1. quoted (capture 3; capture 4 or capture 5)
		"('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
		// 2. simple (capture 6)
		"((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
		// 3. anything else (capture 2)
		".*" +
		")\\)|)",

	// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
	rwhitespace = new RegExp( whitespace + "+", "g" ),
	rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),

	rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
	rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),

	rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ),

	rpseudo = new RegExp( pseudos ),
	ridentifier = new RegExp( "^" + identifier + "$" ),

	matchExpr = {
		"ID": new RegExp( "^#(" + identifier + ")" ),
		"CLASS": new RegExp( "^\\.(" + identifier + ")" ),
		"TAG": new RegExp( "^(" + identifier + "|[*])" ),
		"ATTR": new RegExp( "^" + attributes ),
		"PSEUDO": new RegExp( "^" + pseudos ),
		"CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
			"*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
			"*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
		"bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
		// For use in libraries implementing .is()
		// We use this for POS matching in `select`
		"needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
			whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
	},

	rinputs = /^(?:input|select|textarea|button)$/i,
	rheader = /^h\d$/i,

	rnative = /^[^{]+\{\s*\[native \w/,

	// Easily-parseable/retrievable ID or TAG or CLASS selectors
	rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,

	rsibling = /[+~]/,

	// CSS escapes
	// http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
	runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
	funescape = function( _, escaped, escapedWhitespace ) {
		var high = "0x" + escaped - 0x10000;
		// NaN means non-codepoint
		// Support: Firefox<24
		// Workaround erroneous numeric interpretation of +"0x"
		return high !== high || escapedWhitespace ?
			escaped :
			high < 0 ?
				// BMP codepoint
				String.fromCharCode( high + 0x10000 ) :
				// Supplemental Plane codepoint (surrogate pair)
				String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
	},

	// CSS string/identifier serialization
	// https://drafts.csswg.org/cssom/#common-serializing-idioms
	rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,
	fcssescape = function( ch, asCodePoint ) {
		if ( asCodePoint ) {

			// U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER
			if ( ch === "\0" ) {
				return "\uFFFD";
			}

			// Control characters and (dependent upon position) numbers get escaped as code points
			return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " ";
		}

		// Other potentially-special ASCII characters get backslash-escaped
		return "\\" + ch;
	},

	// Used for iframes
	// See setDocument()
	// Removing the function wrapper causes a "Permission Denied"
	// error in IE
	unloadHandler = function() {
		setDocument();
	},

	disabledAncestor = addCombinator(
		function( elem ) {
			return elem.disabled === true && ("form" in elem || "label" in elem);
		},
		{ dir: "parentNode", next: "legend" }
	);

// Optimize for push.apply( _, NodeList )
try {
	push.apply(
		(arr = slice.call( preferredDoc.childNodes )),
		preferredDoc.childNodes
	);
	// Support: Android<4.0
	// Detect silently failing push.apply
	arr[ preferredDoc.childNodes.length ].nodeType;
} catch ( e ) {
	push = { apply: arr.length ?

		// Leverage slice if possible
		function( target, els ) {
			push_native.apply( target, slice.call(els) );
		} :

		// Support: IE<9
		// Otherwise append directly
		function( target, els ) {
			var j = target.length,
				i = 0;
			// Can't trust NodeList.length
			while ( (target[j++] = els[i++]) ) {}
			target.length = j - 1;
		}
	};
}

function Sizzle( selector, context, results, seed ) {
	var m, i, elem, nid, match, groups, newSelector,
		newContext = context && context.ownerDocument,

		// nodeType defaults to 9, since context defaults to document
		nodeType = context ? context.nodeType : 9;

	results = results || [];

	// Return early from calls with invalid selector or context
	if ( typeof selector !== "string" || !selector ||
		nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {

		return results;
	}

	// Try to shortcut find operations (as opposed to filters) in HTML documents
	if ( !seed ) {

		if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
			setDocument( context );
		}
		context = context || document;

		if ( documentIsHTML ) {

			// If the selector is sufficiently simple, try using a "get*By*" DOM method
			// (excepting DocumentFragment context, where the methods don't exist)
			if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {

				// ID selector
				if ( (m = match[1]) ) {

					// Document context
					if ( nodeType === 9 ) {
						if ( (elem = context.getElementById( m )) ) {

							// Support: IE, Opera, Webkit
							// TODO: identify versions
							// getElementById can match elements by name instead of ID
							if ( elem.id === m ) {
								results.push( elem );
								return results;
							}
						} else {
							return results;
						}

					// Element context
					} else {

						// Support: IE, Opera, Webkit
						// TODO: identify versions
						// getElementById can match elements by name instead of ID
						if ( newContext && (elem = newContext.getElementById( m )) &&
							contains( context, elem ) &&
							elem.id === m ) {

							results.push( elem );
							return results;
						}
					}

				// Type selector
				} else if ( match[2] ) {
					push.apply( results, context.getElementsByTagName( selector ) );
					return results;

				// Class selector
				} else if ( (m = match[3]) && support.getElementsByClassName &&
					context.getElementsByClassName ) {

					push.apply( results, context.getElementsByClassName( m ) );
					return results;
				}
			}

			// Take advantage of querySelectorAll
			if ( support.qsa &&
				!compilerCache[ selector + " " ] &&
				(!rbuggyQSA || !rbuggyQSA.test( selector )) ) {

				if ( nodeType !== 1 ) {
					newContext = context;
					newSelector = selector;

				// qSA looks outside Element context, which is not what we want
				// Thanks to Andrew Dupont for this workaround technique
				// Support: IE <=8
				// Exclude object elements
				} else if ( context.nodeName.toLowerCase() !== "object" ) {

					// Capture the context ID, setting it first if necessary
					if ( (nid = context.getAttribute( "id" )) ) {
						nid = nid.replace( rcssescape, fcssescape );
					} else {
						context.setAttribute( "id", (nid = expando) );
					}

					// Prefix every selector in the list
					groups = tokenize( selector );
					i = groups.length;
					while ( i-- ) {
						groups[i] = "#" + nid + " " + toSelector( groups[i] );
					}
					newSelector = groups.join( "," );

					// Expand context for sibling selectors
					newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
						context;
				}

				if ( newSelector ) {
					try {
						push.apply( results,
							newContext.querySelectorAll( newSelector )
						);
						return results;
					} catch ( qsaError ) {
					} finally {
						if ( nid === expando ) {
							context.removeAttribute( "id" );
						}
					}
				}
			}
		}
	}

	// All others
	return select( selector.replace( rtrim, "$1" ), context, results, seed );
}

/**
 * Create key-value caches of limited size
 * @returns {function(string, object)} Returns the Object data after storing it on itself with
 *	property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
 *	deleting the oldest entry
 */
function createCache() {
	var keys = [];

	function cache( key, value ) {
		// Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
		if ( keys.push( key + " " ) > Expr.cacheLength ) {
			// Only keep the most recent entries
			delete cache[ keys.shift() ];
		}
		return (cache[ key + " " ] = value);
	}
	return cache;
}

/**
 * Mark a function for special use by Sizzle
 * @param {Function} fn The function to mark
 */
function markFunction( fn ) {
	fn[ expando ] = true;
	return fn;
}

/**
 * Support testing using an element
 * @param {Function} fn Passed the created element and returns a boolean result
 */
function assert( fn ) {
	var el = document.createElement("fieldset");

	try {
		return !!fn( el );
	} catch (e) {
		return false;
	} finally {
		// Remove from its parent by default
		if ( el.parentNode ) {
			el.parentNode.removeChild( el );
		}
		// release memory in IE
		el = null;
	}
}

/**
 * Adds the same handler for all of the specified attrs
 * @param {String} attrs Pipe-separated list of attributes
 * @param {Function} handler The method that will be applied
 */
function addHandle( attrs, handler ) {
	var arr = attrs.split("|"),
		i = arr.length;

	while ( i-- ) {
		Expr.attrHandle[ arr[i] ] = handler;
	}
}

/**
 * Checks document order of two siblings
 * @param {Element} a
 * @param {Element} b
 * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
 */
function siblingCheck( a, b ) {
	var cur = b && a,
		diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
			a.sourceIndex - b.sourceIndex;

	// Use IE sourceIndex if available on both nodes
	if ( diff ) {
		return diff;
	}

	// Check if b follows a
	if ( cur ) {
		while ( (cur = cur.nextSibling) ) {
			if ( cur === b ) {
				return -1;
			}
		}
	}

	return a ? 1 : -1;
}

/**
 * Returns a function to use in pseudos for input types
 * @param {String} type
 */
function createInputPseudo( type ) {
	return function( elem ) {
		var name = elem.nodeName.toLowerCase();
		return name === "input" && elem.type === type;
	};
}

/**
 * Returns a function to use in pseudos for buttons
 * @param {String} type
 */
function createButtonPseudo( type ) {
	return function( elem ) {
		var name = elem.nodeName.toLowerCase();
		return (name === "input" || name === "button") && elem.type === type;
	};
}

/**
 * Returns a function to use in pseudos for :enabled/:disabled
 * @param {Boolean} disabled true for :disabled; false for :enabled
 */
function createDisabledPseudo( disabled ) {

	// Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable
	return function( elem ) {

		// Only certain elements can match :enabled or :disabled
		// https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled
		// https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled
		if ( "form" in elem ) {

			// Check for inherited disabledness on relevant non-disabled elements:
			// * listed form-associated elements in a disabled fieldset
			//   https://html.spec.whatwg.org/multipage/forms.html#category-listed
			//   https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled
			// * option elements in a disabled optgroup
			//   https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled
			// All such elements have a "form" property.
			if ( elem.parentNode && elem.disabled === false ) {

				// Option elements defer to a parent optgroup if present
				if ( "label" in elem ) {
					if ( "label" in elem.parentNode ) {
						return elem.parentNode.disabled === disabled;
					} else {
						return elem.disabled === disabled;
					}
				}

				// Support: IE 6 - 11
				// Use the isDisabled shortcut property to check for disabled fieldset ancestors
				return elem.isDisabled === disabled ||

					// Where there is no isDisabled, check manually
					/* jshint -W018 */
					elem.isDisabled !== !disabled &&
						disabledAncestor( elem ) === disabled;
			}

			return elem.disabled === disabled;

		// Try to winnow out elements that can't be disabled before trusting the disabled property.
		// Some victims get caught in our net (label, legend, menu, track), but it shouldn't
		// even exist on them, let alone have a boolean value.
		} else if ( "label" in elem ) {
			return elem.disabled === disabled;
		}

		// Remaining elements are neither :enabled nor :disabled
		return false;
	};
}

/**
 * Returns a function to use in pseudos for positionals
 * @param {Function} fn
 */
function createPositionalPseudo( fn ) {
	return markFunction(function( argument ) {
		argument = +argument;
		return markFunction(function( seed, matches ) {
			var j,
				matchIndexes = fn( [], seed.length, argument ),
				i = matchIndexes.length;

			// Match elements found at the specified indexes
			while ( i-- ) {
				if ( seed[ (j = matchIndexes[i]) ] ) {
					seed[j] = !(matches[j] = seed[j]);
				}
			}
		});
	});
}

/**
 * Checks a node for validity as a Sizzle context
 * @param {Element|Object=} context
 * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
 */
function testContext( context ) {
	return context && typeof context.getElementsByTagName !== "undefined" && context;
}

// Expose support vars for convenience
support = Sizzle.support = {};

/**
 * Detects XML nodes
 * @param {Element|Object} elem An element or a document
 * @returns {Boolean} True iff elem is a non-HTML XML node
 */
isXML = Sizzle.isXML = function( elem ) {
	// documentElement is verified for cases where it doesn't yet exist
	// (such as loading iframes in IE - #4833)
	var documentElement = elem && (elem.ownerDocument || elem).documentElement;
	return documentElement ? documentElement.nodeName !== "HTML" : false;
};

/**
 * Sets document-related variables once based on the current document
 * @param {Element|Object} [doc] An element or document object to use to set the document
 * @returns {Object} Returns the current document
 */
setDocument = Sizzle.setDocument = function( node ) {
	var hasCompare, subWindow,
		doc = node ? node.ownerDocument || node : preferredDoc;

	// Return early if doc is invalid or already selected
	if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
		return document;
	}

	// Update global variables
	document = doc;
	docElem = document.documentElement;
	documentIsHTML = !isXML( document );

	// Support: IE 9-11, Edge
	// Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936)
	if ( preferredDoc !== document &&
		(subWindow = document.defaultView) && subWindow.top !== subWindow ) {

		// Support: IE 11, Edge
		if ( subWindow.addEventListener ) {
			subWindow.addEventListener( "unload", unloadHandler, false );

		// Support: IE 9 - 10 only
		} else if ( subWindow.attachEvent ) {
			subWindow.attachEvent( "onunload", unloadHandler );
		}
	}

	/* Attributes
	---------------------------------------------------------------------- */

	// Support: IE<8
	// Verify that getAttribute really returns attributes and not properties
	// (excepting IE8 booleans)
	support.attributes = assert(function( el ) {
		el.className = "i";
		return !el.getAttribute("className");
	});

	/* getElement(s)By*
	---------------------------------------------------------------------- */

	// Check if getElementsByTagName("*") returns only elements
	support.getElementsByTagName = assert(function( el ) {
		el.appendChild( document.createComment("") );
		return !el.getElementsByTagName("*").length;
	});

	// Support: IE<9
	support.getElementsByClassName = rnative.test( document.getElementsByClassName );

	// Support: IE<10
	// Check if getElementById returns elements by name
	// The broken getElementById methods don't pick up programmatically-set names,
	// so use a roundabout getElementsByName test
	support.getById = assert(function( el ) {
		docElem.appendChild( el ).id = expando;
		return !document.getElementsByName || !document.getElementsByName( expando ).length;
	});

	// ID filter and find
	if ( support.getById ) {
		Expr.filter["ID"] = function( id ) {
			var attrId = id.replace( runescape, funescape );
			return function( elem ) {
				return elem.getAttribute("id") === attrId;
			};
		};
		Expr.find["ID"] = function( id, context ) {
			if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
				var elem = context.getElementById( id );
				return elem ? [ elem ] : [];
			}
		};
	} else {
		Expr.filter["ID"] =  function( id ) {
			var attrId = id.replace( runescape, funescape );
			return function( elem ) {
				var node = typeof elem.getAttributeNode !== "undefined" &&
					elem.getAttributeNode("id");
				return node && node.value === attrId;
			};
		};

		// Support: IE 6 - 7 only
		// getElementById is not reliable as a find shortcut
		Expr.find["ID"] = function( id, context ) {
			if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
				var node, i, elems,
					elem = context.getElementById( id );

				if ( elem ) {

					// Verify the id attribute
					node = elem.getAttributeNode("id");
					if ( node && node.value === id ) {
						return [ elem ];
					}

					// Fall back on getElementsByName
					elems = context.getElementsByName( id );
					i = 0;
					while ( (elem = elems[i++]) ) {
						node = elem.getAttributeNode("id");
						if ( node && node.value === id ) {
							return [ elem ];
						}
					}
				}

				return [];
			}
		};
	}

	// Tag
	Expr.find["TAG"] = support.getElementsByTagName ?
		function( tag, context ) {
			if ( typeof context.getElementsByTagName !== "undefined" ) {
				return context.getElementsByTagName( tag );

			// DocumentFragment nodes don't have gEBTN
			} else if ( support.qsa ) {
				return context.querySelectorAll( tag );
			}
		} :

		function( tag, context ) {
			var elem,
				tmp = [],
				i = 0,
				// By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too
				results = context.getElementsByTagName( tag );

			// Filter out possible comments
			if ( tag === "*" ) {
				while ( (elem = results[i++]) ) {
					if ( elem.nodeType === 1 ) {
						tmp.push( elem );
					}
				}

				return tmp;
			}
			return results;
		};

	// Class
	Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
		if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) {
			return context.getElementsByClassName( className );
		}
	};

	/* QSA/matchesSelector
	---------------------------------------------------------------------- */

	// QSA and matchesSelector support

	// matchesSelector(:active) reports false when true (IE9/Opera 11.5)
	rbuggyMatches = [];

	// qSa(:focus) reports false when true (Chrome 21)
	// We allow this because of a bug in IE8/9 that throws an error
	// whenever `document.activeElement` is accessed on an iframe
	// So, we allow :focus to pass through QSA all the time to avoid the IE error
	// See https://bugs.jquery.com/ticket/13378
	rbuggyQSA = [];

	if ( (support.qsa = rnative.test( document.querySelectorAll )) ) {
		// Build QSA regex
		// Regex strategy adopted from Diego Perini
		assert(function( el ) {
			// Select is set to empty string on purpose
			// This is to test IE's treatment of not explicitly
			// setting a boolean content attribute,
			// since its presence should be enough
			// https://bugs.jquery.com/ticket/12359
			docElem.appendChild( el ).innerHTML = "<a id='" + expando + "'></a>" +
				"<select id='" + expando + "-\r\\' msallowcapture=''>" +
				"<option selected=''></option></select>";

			// Support: IE8, Opera 11-12.16
			// Nothing should be selected when empty strings follow ^= or $= or *=
			// The test attribute must be unknown in Opera but "safe" for WinRT
			// https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
			if ( el.querySelectorAll("[msallowcapture^='']").length ) {
				rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
			}

			// Support: IE8
			// Boolean attributes and "value" are not treated correctly
			if ( !el.querySelectorAll("[selected]").length ) {
				rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
			}

			// Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+
			if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) {
				rbuggyQSA.push("~=");
			}

			// Webkit/Opera - :checked should return selected option elements
			// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
			// IE8 throws error here and will not see later tests
			if ( !el.querySelectorAll(":checked").length ) {
				rbuggyQSA.push(":checked");
			}

			// Support: Safari 8+, iOS 8+
			// https://bugs.webkit.org/show_bug.cgi?id=136851
			// In-page `selector#id sibling-combinator selector` fails
			if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) {
				rbuggyQSA.push(".#.+[+~]");
			}
		});

		assert(function( el ) {
			el.innerHTML = "<a href='' disabled='disabled'></a>" +
				"<select disabled='disabled'><option/></select>";

			// Support: Windows 8 Native Apps
			// The type and name attributes are restricted during .innerHTML assignment
			var input = document.createElement("input");
			input.setAttribute( "type", "hidden" );
			el.appendChild( input ).setAttribute( "name", "D" );

			// Support: IE8
			// Enforce case-sensitivity of name attribute
			if ( el.querySelectorAll("[name=d]").length ) {
				rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
			}

			// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
			// IE8 throws error here and will not see later tests
			if ( el.querySelectorAll(":enabled").length !== 2 ) {
				rbuggyQSA.push( ":enabled", ":disabled" );
			}

			// Support: IE9-11+
			// IE's :disabled selector does not pick up the children of disabled fieldsets
			docElem.appendChild( el ).disabled = true;
			if ( el.querySelectorAll(":disabled").length !== 2 ) {
				rbuggyQSA.push( ":enabled", ":disabled" );
			}

			// Opera 10-11 does not throw on post-comma invalid pseudos
			el.querySelectorAll("*,:x");
			rbuggyQSA.push(",.*:");
		});
	}

	if ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||
		docElem.webkitMatchesSelector ||
		docElem.mozMatchesSelector ||
		docElem.oMatchesSelector ||
		docElem.msMatchesSelector) )) ) {

		assert(function( el ) {
			// Check to see if it's possible to do matchesSelector
			// on a disconnected node (IE 9)
			support.disconnectedMatch = matches.call( el, "*" );

			// This should fail with an exception
			// Gecko does not error, returns false instead
			matches.call( el, "[s!='']:x" );
			rbuggyMatches.push( "!=", pseudos );
		});
	}

	rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
	rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );

	/* Contains
	---------------------------------------------------------------------- */
	hasCompare = rnative.test( docElem.compareDocumentPosition );

	// Element contains another
	// Purposefully self-exclusive
	// As in, an element does not contain itself
	contains = hasCompare || rnative.test( docElem.contains ) ?
		function( a, b ) {
			var adown = a.nodeType === 9 ? a.documentElement : a,
				bup = b && b.parentNode;
			return a === bup || !!( bup && bup.nodeType === 1 && (
				adown.contains ?
					adown.contains( bup ) :
					a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
			));
		} :
		function( a, b ) {
			if ( b ) {
				while ( (b = b.parentNode) ) {
					if ( b === a ) {
						return true;
					}
				}
			}
			return false;
		};

	/* Sorting
	---------------------------------------------------------------------- */

	// Document order sorting
	sortOrder = hasCompare ?
	function( a, b ) {

		// Flag for duplicate removal
		if ( a === b ) {
			hasDuplicate = true;
			return 0;
		}

		// Sort on method existence if only one input has compareDocumentPosition
		var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
		if ( compare ) {
			return compare;
		}

		// Calculate position if both inputs belong to the same document
		compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?
			a.compareDocumentPosition( b ) :

			// Otherwise we know they are disconnected
			1;

		// Disconnected nodes
		if ( compare & 1 ||
			(!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {

			// Choose the first element that is related to our preferred document
			if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {
				return -1;
			}
			if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {
				return 1;
			}

			// Maintain original order
			return sortInput ?
				( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
				0;
		}

		return compare & 4 ? -1 : 1;
	} :
	function( a, b ) {
		// Exit early if the nodes are identical
		if ( a === b ) {
			hasDuplicate = true;
			return 0;
		}

		var cur,
			i = 0,
			aup = a.parentNode,
			bup = b.parentNode,
			ap = [ a ],
			bp = [ b ];

		// Parentless nodes are either documents or disconnected
		if ( !aup || !bup ) {
			return a === document ? -1 :
				b === document ? 1 :
				aup ? -1 :
				bup ? 1 :
				sortInput ?
				( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
				0;

		// If the nodes are siblings, we can do a quick check
		} else if ( aup === bup ) {
			return siblingCheck( a, b );
		}

		// Otherwise we need full lists of their ancestors for comparison
		cur = a;
		while ( (cur = cur.parentNode) ) {
			ap.unshift( cur );
		}
		cur = b;
		while ( (cur = cur.parentNode) ) {
			bp.unshift( cur );
		}

		// Walk down the tree looking for a discrepancy
		while ( ap[i] === bp[i] ) {
			i++;
		}

		return i ?
			// Do a sibling check if the nodes have a common ancestor
			siblingCheck( ap[i], bp[i] ) :

			// Otherwise nodes in our document sort first
			ap[i] === preferredDoc ? -1 :
			bp[i] === preferredDoc ? 1 :
			0;
	};

	return document;
};

Sizzle.matches = function( expr, elements ) {
	return Sizzle( expr, null, null, elements );
};

Sizzle.matchesSelector = function( elem, expr ) {
	// Set document vars if needed
	if ( ( elem.ownerDocument || elem ) !== document ) {
		setDocument( elem );
	}

	// Make sure that attribute selectors are quoted
	expr = expr.replace( rattributeQuotes, "='$1']" );

	if ( support.matchesSelector && documentIsHTML &&
		!compilerCache[ expr + " " ] &&
		( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
		( !rbuggyQSA     || !rbuggyQSA.test( expr ) ) ) {

		try {
			var ret = matches.call( elem, expr );

			// IE 9's matchesSelector returns false on disconnected nodes
			if ( ret || support.disconnectedMatch ||
					// As well, disconnected nodes are said to be in a document
					// fragment in IE 9
					elem.document && elem.document.nodeType !== 11 ) {
				return ret;
			}
		} catch (e) {}
	}

	return Sizzle( expr, document, null, [ elem ] ).length > 0;
};

Sizzle.contains = function( context, elem ) {
	// Set document vars if needed
	if ( ( context.ownerDocument || context ) !== document ) {
		setDocument( context );
	}
	return contains( context, elem );
};

Sizzle.attr = function( elem, name ) {
	// Set document vars if needed
	if ( ( elem.ownerDocument || elem ) !== document ) {
		setDocument( elem );
	}

	var fn = Expr.attrHandle[ name.toLowerCase() ],
		// Don't get fooled by Object.prototype properties (jQuery #13807)
		val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
			fn( elem, name, !documentIsHTML ) :
			undefined;

	return val !== undefined ?
		val :
		support.attributes || !documentIsHTML ?
			elem.getAttribute( name ) :
			(val = elem.getAttributeNode(name)) && val.specified ?
				val.value :
				null;
};

Sizzle.escape = function( sel ) {
	return (sel + "").replace( rcssescape, fcssescape );
};

Sizzle.error = function( msg ) {
	throw new Error( "Syntax error, unrecognized expression: " + msg );
};

/**
 * Document sorting and removing duplicates
 * @param {ArrayLike} results
 */
Sizzle.uniqueSort = function( results ) {
	var elem,
		duplicates = [],
		j = 0,
		i = 0;

	// Unless we *know* we can detect duplicates, assume their presence
	hasDuplicate = !support.detectDuplicates;
	sortInput = !support.sortStable && results.slice( 0 );
	results.sort( sortOrder );

	if ( hasDuplicate ) {
		while ( (elem = results[i++]) ) {
			if ( elem === results[ i ] ) {
				j = duplicates.push( i );
			}
		}
		while ( j-- ) {
			results.splice( duplicates[ j ], 1 );
		}
	}

	// Clear input after sorting to release objects
	// See https://github.com/jquery/sizzle/pull/225
	sortInput = null;

	return results;
};

/**
 * Utility function for retrieving the text value of an array of DOM nodes
 * @param {Array|Element} elem
 */
getText = Sizzle.getText = function( elem ) {
	var node,
		ret = "",
		i = 0,
		nodeType = elem.nodeType;

	if ( !nodeType ) {
		// If no nodeType, this is expected to be an array
		while ( (node = elem[i++]) ) {
			// Do not traverse comment nodes
			ret += getText( node );
		}
	} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
		// Use textContent for elements
		// innerText usage removed for consistency of new lines (jQuery #11153)
		if ( typeof elem.textContent === "string" ) {
			return elem.textContent;
		} else {
			// Traverse its children
			for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
				ret += getText( elem );
			}
		}
	} else if ( nodeType === 3 || nodeType === 4 ) {
		return elem.nodeValue;
	}
	// Do not include comment or processing instruction nodes

	return ret;
};

Expr = Sizzle.selectors = {

	// Can be adjusted by the user
	cacheLength: 50,

	createPseudo: markFunction,

	match: matchExpr,

	attrHandle: {},

	find: {},

	relative: {
		">": { dir: "parentNode", first: true },
		" ": { dir: "parentNode" },
		"+": { dir: "previousSibling", first: true },
		"~": { dir: "previousSibling" }
	},

	preFilter: {
		"ATTR": function( match ) {
			match[1] = match[1].replace( runescape, funescape );

			// Move the given value to match[3] whether quoted or unquoted
			match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape );

			if ( match[2] === "~=" ) {
				match[3] = " " + match[3] + " ";
			}

			return match.slice( 0, 4 );
		},

		"CHILD": function( match ) {
			/* matches from matchExpr["CHILD"]
				1 type (only|nth|...)
				2 what (child|of-type)
				3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
				4 xn-component of xn+y argument ([+-]?\d*n|)
				5 sign of xn-component
				6 x of xn-component
				7 sign of y-component
				8 y of y-component
			*/
			match[1] = match[1].toLowerCase();

			if ( match[1].slice( 0, 3 ) === "nth" ) {
				// nth-* requires argument
				if ( !match[3] ) {
					Sizzle.error( match[0] );
				}

				// numeric x and y parameters for Expr.filter.CHILD
				// remember that false/true cast respectively to 0/1
				match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
				match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );

			// other types prohibit arguments
			} else if ( match[3] ) {
				Sizzle.error( match[0] );
			}

			return match;
		},

		"PSEUDO": function( match ) {
			var excess,
				unquoted = !match[6] && match[2];

			if ( matchExpr["CHILD"].test( match[0] ) ) {
				return null;
			}

			// Accept quoted arguments as-is
			if ( match[3] ) {
				match[2] = match[4] || match[5] || "";

			// Strip excess characters from unquoted arguments
			} else if ( unquoted && rpseudo.test( unquoted ) &&
				// Get excess from tokenize (recursively)
				(excess = tokenize( unquoted, true )) &&
				// advance to the next closing parenthesis
				(excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {

				// excess is a negative index
				match[0] = match[0].slice( 0, excess );
				match[2] = unquoted.slice( 0, excess );
			}

			// Return only captures needed by the pseudo filter method (type and argument)
			return match.slice( 0, 3 );
		}
	},

	filter: {

		"TAG": function( nodeNameSelector ) {
			var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
			return nodeNameSelector === "*" ?
				function() { return true; } :
				function( elem ) {
					return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
				};
		},

		"CLASS": function( className ) {
			var pattern = classCache[ className + " " ];

			return pattern ||
				(pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
				classCache( className, function( elem ) {
					return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" );
				});
		},

		"ATTR": function( name, operator, check ) {
			return function( elem ) {
				var result = Sizzle.attr( elem, name );

				if ( result == null ) {
					return operator === "!=";
				}
				if ( !operator ) {
					return true;
				}

				result += "";

				return operator === "=" ? result === check :
					operator === "!=" ? result !== check :
					operator === "^=" ? check && result.indexOf( check ) === 0 :
					operator === "*=" ? check && result.indexOf( check ) > -1 :
					operator === "$=" ? check && result.slice( -check.length ) === check :
					operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 :
					operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
					false;
			};
		},

		"CHILD": function( type, what, argument, first, last ) {
			var simple = type.slice( 0, 3 ) !== "nth",
				forward = type.slice( -4 ) !== "last",
				ofType = what === "of-type";

			return first === 1 && last === 0 ?

				// Shortcut for :nth-*(n)
				function( elem ) {
					return !!elem.parentNode;
				} :

				function( elem, context, xml ) {
					var cache, uniqueCache, outerCache, node, nodeIndex, start,
						dir = simple !== forward ? "nextSibling" : "previousSibling",
						parent = elem.parentNode,
						name = ofType && elem.nodeName.toLowerCase(),
						useCache = !xml && !ofType,
						diff = false;

					if ( parent ) {

						// :(first|last|only)-(child|of-type)
						if ( simple ) {
							while ( dir ) {
								node = elem;
								while ( (node = node[ dir ]) ) {
									if ( ofType ?
										node.nodeName.toLowerCase() === name :
										node.nodeType === 1 ) {

										return false;
									}
								}
								// Reverse direction for :only-* (if we haven't yet done so)
								start = dir = type === "only" && !start && "nextSibling";
							}
							return true;
						}

						start = [ forward ? parent.firstChild : parent.lastChild ];

						// non-xml :nth-child(...) stores cache data on `parent`
						if ( forward && useCache ) {

							// Seek `elem` from a previously-cached index

							// ...in a gzip-friendly way
							node = parent;
							outerCache = node[ expando ] || (node[ expando ] = {});

							// Support: IE <9 only
							// Defend against cloned attroperties (jQuery gh-1709)
							uniqueCache = outerCache[ node.uniqueID ] ||
								(outerCache[ node.uniqueID ] = {});

							cache = uniqueCache[ type ] || [];
							nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
							diff = nodeIndex && cache[ 2 ];
							node = nodeIndex && parent.childNodes[ nodeIndex ];

							while ( (node = ++nodeIndex && node && node[ dir ] ||

								// Fallback to seeking `elem` from the start
								(diff = nodeIndex = 0) || start.pop()) ) {

								// When found, cache indexes on `parent` and break
								if ( node.nodeType === 1 && ++diff && node === elem ) {
									uniqueCache[ type ] = [ dirruns, nodeIndex, diff ];
									break;
								}
							}

						} else {
							// Use previously-cached element index if available
							if ( useCache ) {
								// ...in a gzip-friendly way
								node = elem;
								outerCache = node[ expando ] || (node[ expando ] = {});

								// Support: IE <9 only
								// Defend against cloned attroperties (jQuery gh-1709)
								uniqueCache = outerCache[ node.uniqueID ] ||
									(outerCache[ node.uniqueID ] = {});

								cache = uniqueCache[ type ] || [];
								nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
								diff = nodeIndex;
							}

							// xml :nth-child(...)
							// or :nth-last-child(...) or :nth(-last)?-of-type(...)
							if ( diff === false ) {
								// Use the same loop as above to seek `elem` from the start
								while ( (node = ++nodeIndex && node && node[ dir ] ||
									(diff = nodeIndex = 0) || start.pop()) ) {

									if ( ( ofType ?
										node.nodeName.toLowerCase() === name :
										node.nodeType === 1 ) &&
										++diff ) {

										// Cache the index of each encountered element
										if ( useCache ) {
											outerCache = node[ expando ] || (node[ expando ] = {});

											// Support: IE <9 only
											// Defend against cloned attroperties (jQuery gh-1709)
											uniqueCache = outerCache[ node.uniqueID ] ||
												(outerCache[ node.uniqueID ] = {});

											uniqueCache[ type ] = [ dirruns, diff ];
										}

										if ( node === elem ) {
											break;
										}
									}
								}
							}
						}

						// Incorporate the offset, then check against cycle size
						diff -= last;
						return diff === first || ( diff % first === 0 && diff / first >= 0 );
					}
				};
		},

		"PSEUDO": function( pseudo, argument ) {
			// pseudo-class names are case-insensitive
			// http://www.w3.org/TR/selectors/#pseudo-classes
			// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
			// Remember that setFilters inherits from pseudos
			var args,
				fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
					Sizzle.error( "unsupported pseudo: " + pseudo );

			// The user may use createPseudo to indicate that
			// arguments are needed to create the filter function
			// just as Sizzle does
			if ( fn[ expando ] ) {
				return fn( argument );
			}

			// But maintain support for old signatures
			if ( fn.length > 1 ) {
				args = [ pseudo, pseudo, "", argument ];
				return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
					markFunction(function( seed, matches ) {
						var idx,
							matched = fn( seed, argument ),
							i = matched.length;
						while ( i-- ) {
							idx = indexOf( seed, matched[i] );
							seed[ idx ] = !( matches[ idx ] = matched[i] );
						}
					}) :
					function( elem ) {
						return fn( elem, 0, args );
					};
			}

			return fn;
		}
	},

	pseudos: {
		// Potentially complex pseudos
		"not": markFunction(function( selector ) {
			// Trim the selector passed to compile
			// to avoid treating leading and trailing
			// spaces as combinators
			var input = [],
				results = [],
				matcher = compile( selector.replace( rtrim, "$1" ) );

			return matcher[ expando ] ?
				markFunction(function( seed, matches, context, xml ) {
					var elem,
						unmatched = matcher( seed, null, xml, [] ),
						i = seed.length;

					// Match elements unmatched by `matcher`
					while ( i-- ) {
						if ( (elem = unmatched[i]) ) {
							seed[i] = !(matches[i] = elem);
						}
					}
				}) :
				function( elem, context, xml ) {
					input[0] = elem;
					matcher( input, null, xml, results );
					// Don't keep the element (issue #299)
					input[0] = null;
					return !results.pop();
				};
		}),

		"has": markFunction(function( selector ) {
			return function( elem ) {
				return Sizzle( selector, elem ).length > 0;
			};
		}),

		"contains": markFunction(function( text ) {
			text = text.replace( runescape, funescape );
			return function( elem ) {
				return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
			};
		}),

		// "Whether an element is represented by a :lang() selector
		// is based solely on the element's language value
		// being equal to the identifier C,
		// or beginning with the identifier C immediately followed by "-".
		// The matching of C against the element's language value is performed case-insensitively.
		// The identifier C does not have to be a valid language name."
		// http://www.w3.org/TR/selectors/#lang-pseudo
		"lang": markFunction( function( lang ) {
			// lang value must be a valid identifier
			if ( !ridentifier.test(lang || "") ) {
				Sizzle.error( "unsupported lang: " + lang );
			}
			lang = lang.replace( runescape, funescape ).toLowerCase();
			return function( elem ) {
				var elemLang;
				do {
					if ( (elemLang = documentIsHTML ?
						elem.lang :
						elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {

						elemLang = elemLang.toLowerCase();
						return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
					}
				} while ( (elem = elem.parentNode) && elem.nodeType === 1 );
				return false;
			};
		}),

		// Miscellaneous
		"target": function( elem ) {
			var hash = window.location && window.location.hash;
			return hash && hash.slice( 1 ) === elem.id;
		},

		"root": function( elem ) {
			return elem === docElem;
		},

		"focus": function( elem ) {
			return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
		},

		// Boolean properties
		"enabled": createDisabledPseudo( false ),
		"disabled": createDisabledPseudo( true ),

		"checked": function( elem ) {
			// In CSS3, :checked should return both checked and selected elements
			// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
			var nodeName = elem.nodeName.toLowerCase();
			return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
		},

		"selected": function( elem ) {
			// Accessing this property makes selected-by-default
			// options in Safari work properly
			if ( elem.parentNode ) {
				elem.parentNode.selectedIndex;
			}

			return elem.selected === true;
		},

		// Contents
		"empty": function( elem ) {
			// http://www.w3.org/TR/selectors/#empty-pseudo
			// :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
			//   but not by others (comment: 8; processing instruction: 7; etc.)
			// nodeType < 6 works because attributes (2) do not appear as children
			for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
				if ( elem.nodeType < 6 ) {
					return false;
				}
			}
			return true;
		},

		"parent": function( elem ) {
			return !Expr.pseudos["empty"]( elem );
		},

		// Element/input types
		"header": function( elem ) {
			return rheader.test( elem.nodeName );
		},

		"input": function( elem ) {
			return rinputs.test( elem.nodeName );
		},

		"button": function( elem ) {
			var name = elem.nodeName.toLowerCase();
			return name === "input" && elem.type === "button" || name === "button";
		},

		"text": function( elem ) {
			var attr;
			return elem.nodeName.toLowerCase() === "input" &&
				elem.type === "text" &&

				// Support: IE<8
				// New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
				( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" );
		},

		// Position-in-collection
		"first": createPositionalPseudo(function() {
			return [ 0 ];
		}),

		"last": createPositionalPseudo(function( matchIndexes, length ) {
			return [ length - 1 ];
		}),

		"eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
			return [ argument < 0 ? argument + length : argument ];
		}),

		"even": createPositionalPseudo(function( matchIndexes, length ) {
			var i = 0;
			for ( ; i < length; i += 2 ) {
				matchIndexes.push( i );
			}
			return matchIndexes;
		}),

		"odd": createPositionalPseudo(function( matchIndexes, length ) {
			var i = 1;
			for ( ; i < length; i += 2 ) {
				matchIndexes.push( i );
			}
			return matchIndexes;
		}),

		"lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
			var i = argument < 0 ? argument + length : argument;
			for ( ; --i >= 0; ) {
				matchIndexes.push( i );
			}
			return matchIndexes;
		}),

		"gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
			var i = argument < 0 ? argument + length : argument;
			for ( ; ++i < length; ) {
				matchIndexes.push( i );
			}
			return matchIndexes;
		})
	}
};

Expr.pseudos["nth"] = Expr.pseudos["eq"];

// Add button/input type pseudos
for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
	Expr.pseudos[ i ] = createInputPseudo( i );
}
for ( i in { submit: true, reset: true } ) {
	Expr.pseudos[ i ] = createButtonPseudo( i );
}

// Easy API for creating new setFilters
function setFilters() {}
setFilters.prototype = Expr.filters = Expr.pseudos;
Expr.setFilters = new setFilters();

tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
	var matched, match, tokens, type,
		soFar, groups, preFilters,
		cached = tokenCache[ selector + " " ];

	if ( cached ) {
		return parseOnly ? 0 : cached.slice( 0 );
	}

	soFar = selector;
	groups = [];
	preFilters = Expr.preFilter;

	while ( soFar ) {

		// Comma and first run
		if ( !matched || (match = rcomma.exec( soFar )) ) {
			if ( match ) {
				// Don't consume trailing commas as valid
				soFar = soFar.slice( match[0].length ) || soFar;
			}
			groups.push( (tokens = []) );
		}

		matched = false;

		// Combinators
		if ( (match = rcombinators.exec( soFar )) ) {
			matched = match.shift();
			tokens.push({
				value: matched,
				// Cast descendant combinators to space
				type: match[0].replace( rtrim, " " )
			});
			soFar = soFar.slice( matched.length );
		}

		// Filters
		for ( type in Expr.filter ) {
			if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
				(match = preFilters[ type ]( match ))) ) {
				matched = match.shift();
				tokens.push({
					value: matched,
					type: type,
					matches: match
				});
				soFar = soFar.slice( matched.length );
			}
		}

		if ( !matched ) {
			break;
		}
	}

	// Return the length of the invalid excess
	// if we're just parsing
	// Otherwise, throw an error or return tokens
	return parseOnly ?
		soFar.length :
		soFar ?
			Sizzle.error( selector ) :
			// Cache the tokens
			tokenCache( selector, groups ).slice( 0 );
};

function toSelector( tokens ) {
	var i = 0,
		len = tokens.length,
		selector = "";
	for ( ; i < len; i++ ) {
		selector += tokens[i].value;
	}
	return selector;
}

function addCombinator( matcher, combinator, base ) {
	var dir = combinator.dir,
		skip = combinator.next,
		key = skip || dir,
		checkNonElements = base && key === "parentNode",
		doneName = done++;

	return combinator.first ?
		// Check against closest ancestor/preceding element
		function( elem, context, xml ) {
			while ( (elem = elem[ dir ]) ) {
				if ( elem.nodeType === 1 || checkNonElements ) {
					return matcher( elem, context, xml );
				}
			}
			return false;
		} :

		// Check against all ancestor/preceding elements
		function( elem, context, xml ) {
			var oldCache, uniqueCache, outerCache,
				newCache = [ dirruns, doneName ];

			// We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching
			if ( xml ) {
				while ( (elem = elem[ dir ]) ) {
					if ( elem.nodeType === 1 || checkNonElements ) {
						if ( matcher( elem, context, xml ) ) {
							return true;
						}
					}
				}
			} else {
				while ( (elem = elem[ dir ]) ) {
					if ( elem.nodeType === 1 || checkNonElements ) {
						outerCache = elem[ expando ] || (elem[ expando ] = {});

						// Support: IE <9 only
						// Defend against cloned attroperties (jQuery gh-1709)
						uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {});

						if ( skip && skip === elem.nodeName.toLowerCase() ) {
							elem = elem[ dir ] || elem;
						} else if ( (oldCache = uniqueCache[ key ]) &&
							oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {

							// Assign to newCache so results back-propagate to previous elements
							return (newCache[ 2 ] = oldCache[ 2 ]);
						} else {
							// Reuse newcache so results back-propagate to previous elements
							uniqueCache[ key ] = newCache;

							// A match means we're done; a fail means we have to keep checking
							if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {
								return true;
							}
						}
					}
				}
			}
			return false;
		};
}

function elementMatcher( matchers ) {
	return matchers.length > 1 ?
		function( elem, context, xml ) {
			var i = matchers.length;
			while ( i-- ) {
				if ( !matchers[i]( elem, context, xml ) ) {
					return false;
				}
			}
			return true;
		} :
		matchers[0];
}

function multipleContexts( selector, contexts, results ) {
	var i = 0,
		len = contexts.length;
	for ( ; i < len; i++ ) {
		Sizzle( selector, contexts[i], results );
	}
	return results;
}

function condense( unmatched, map, filter, context, xml ) {
	var elem,
		newUnmatched = [],
		i = 0,
		len = unmatched.length,
		mapped = map != null;

	for ( ; i < len; i++ ) {
		if ( (elem = unmatched[i]) ) {
			if ( !filter || filter( elem, context, xml ) ) {
				newUnmatched.push( elem );
				if ( mapped ) {
					map.push( i );
				}
			}
		}
	}

	return newUnmatched;
}

function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
	if ( postFilter && !postFilter[ expando ] ) {
		postFilter = setMatcher( postFilter );
	}
	if ( postFinder && !postFinder[ expando ] ) {
		postFinder = setMatcher( postFinder, postSelector );
	}
	return markFunction(function( seed, results, context, xml ) {
		var temp, i, elem,
			preMap = [],
			postMap = [],
			preexisting = results.length,

			// Get initial elements from seed or context
			elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),

			// Prefilter to get matcher input, preserving a map for seed-results synchronization
			matcherIn = preFilter && ( seed || !selector ) ?
				condense( elems, preMap, preFilter, context, xml ) :
				elems,

			matcherOut = matcher ?
				// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
				postFinder || ( seed ? preFilter : preexisting || postFilter ) ?

					// ...intermediate processing is necessary
					[] :

					// ...otherwise use results directly
					results :
				matcherIn;

		// Find primary matches
		if ( matcher ) {
			matcher( matcherIn, matcherOut, context, xml );
		}

		// Apply postFilter
		if ( postFilter ) {
			temp = condense( matcherOut, postMap );
			postFilter( temp, [], context, xml );

			// Un-match failing elements by moving them back to matcherIn
			i = temp.length;
			while ( i-- ) {
				if ( (elem = temp[i]) ) {
					matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
				}
			}
		}

		if ( seed ) {
			if ( postFinder || preFilter ) {
				if ( postFinder ) {
					// Get the final matcherOut by condensing this intermediate into postFinder contexts
					temp = [];
					i = matcherOut.length;
					while ( i-- ) {
						if ( (elem = matcherOut[i]) ) {
							// Restore matcherIn since elem is not yet a final match
							temp.push( (matcherIn[i] = elem) );
						}
					}
					postFinder( null, (matcherOut = []), temp, xml );
				}

				// Move matched elements from seed to results to keep them synchronized
				i = matcherOut.length;
				while ( i-- ) {
					if ( (elem = matcherOut[i]) &&
						(temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) {

						seed[temp] = !(results[temp] = elem);
					}
				}
			}

		// Add elements to results, through postFinder if defined
		} else {
			matcherOut = condense(
				matcherOut === results ?
					matcherOut.splice( preexisting, matcherOut.length ) :
					matcherOut
			);
			if ( postFinder ) {
				postFinder( null, results, matcherOut, xml );
			} else {
				push.apply( results, matcherOut );
			}
		}
	});
}

function matcherFromTokens( tokens ) {
	var checkContext, matcher, j,
		len = tokens.length,
		leadingRelative = Expr.relative[ tokens[0].type ],
		implicitRelative = leadingRelative || Expr.relative[" "],
		i = leadingRelative ? 1 : 0,

		// The foundational matcher ensures that elements are reachable from top-level context(s)
		matchContext = addCombinator( function( elem ) {
			return elem === checkContext;
		}, implicitRelative, true ),
		matchAnyContext = addCombinator( function( elem ) {
			return indexOf( checkContext, elem ) > -1;
		}, implicitRelative, true ),
		matchers = [ function( elem, context, xml ) {
			var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
				(checkContext = context).nodeType ?
					matchContext( elem, context, xml ) :
					matchAnyContext( elem, context, xml ) );
			// Avoid hanging onto element (issue #299)
			checkContext = null;
			return ret;
		} ];

	for ( ; i < len; i++ ) {
		if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
			matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
		} else {
			matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );

			// Return special upon seeing a positional matcher
			if ( matcher[ expando ] ) {
				// Find the next relative operator (if any) for proper handling
				j = ++i;
				for ( ; j < len; j++ ) {
					if ( Expr.relative[ tokens[j].type ] ) {
						break;
					}
				}
				return setMatcher(
					i > 1 && elementMatcher( matchers ),
					i > 1 && toSelector(
						// If the preceding token was a descendant combinator, insert an implicit any-element `*`
						tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" })
					).replace( rtrim, "$1" ),
					matcher,
					i < j && matcherFromTokens( tokens.slice( i, j ) ),
					j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
					j < len && toSelector( tokens )
				);
			}
			matchers.push( matcher );
		}
	}

	return elementMatcher( matchers );
}

function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
	var bySet = setMatchers.length > 0,
		byElement = elementMatchers.length > 0,
		superMatcher = function( seed, context, xml, results, outermost ) {
			var elem, j, matcher,
				matchedCount = 0,
				i = "0",
				unmatched = seed && [],
				setMatched = [],
				contextBackup = outermostContext,
				// We must always have either seed elements or outermost context
				elems = seed || byElement && Expr.find["TAG"]( "*", outermost ),
				// Use integer dirruns iff this is the outermost matcher
				dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
				len = elems.length;

			if ( outermost ) {
				outermostContext = context === document || context || outermost;
			}

			// Add elements passing elementMatchers directly to results
			// Support: IE<9, Safari
			// Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id
			for ( ; i !== len && (elem = elems[i]) != null; i++ ) {
				if ( byElement && elem ) {
					j = 0;
					if ( !context && elem.ownerDocument !== document ) {
						setDocument( elem );
						xml = !documentIsHTML;
					}
					while ( (matcher = elementMatchers[j++]) ) {
						if ( matcher( elem, context || document, xml) ) {
							results.push( elem );
							break;
						}
					}
					if ( outermost ) {
						dirruns = dirrunsUnique;
					}
				}

				// Track unmatched elements for set filters
				if ( bySet ) {
					// They will have gone through all possible matchers
					if ( (elem = !matcher && elem) ) {
						matchedCount--;
					}

					// Lengthen the array for every element, matched or not
					if ( seed ) {
						unmatched.push( elem );
					}
				}
			}

			// `i` is now the count of elements visited above, and adding it to `matchedCount`
			// makes the latter nonnegative.
			matchedCount += i;

			// Apply set filters to unmatched elements
			// NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`
			// equals `i`), unless we didn't visit _any_ elements in the above loop because we have
			// no element matchers and no seed.
			// Incrementing an initially-string "0" `i` allows `i` to remain a string only in that
			// case, which will result in a "00" `matchedCount` that differs from `i` but is also
			// numerically zero.
			if ( bySet && i !== matchedCount ) {
				j = 0;
				while ( (matcher = setMatchers[j++]) ) {
					matcher( unmatched, setMatched, context, xml );
				}

				if ( seed ) {
					// Reintegrate element matches to eliminate the need for sorting
					if ( matchedCount > 0 ) {
						while ( i-- ) {
							if ( !(unmatched[i] || setMatched[i]) ) {
								setMatched[i] = pop.call( results );
							}
						}
					}

					// Discard index placeholder values to get only actual matches
					setMatched = condense( setMatched );
				}

				// Add matches to results
				push.apply( results, setMatched );

				// Seedless set matches succeeding multiple successful matchers stipulate sorting
				if ( outermost && !seed && setMatched.length > 0 &&
					( matchedCount + setMatchers.length ) > 1 ) {

					Sizzle.uniqueSort( results );
				}
			}

			// Override manipulation of globals by nested matchers
			if ( outermost ) {
				dirruns = dirrunsUnique;
				outermostContext = contextBackup;
			}

			return unmatched;
		};

	return bySet ?
		markFunction( superMatcher ) :
		superMatcher;
}

compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
	var i,
		setMatchers = [],
		elementMatchers = [],
		cached = compilerCache[ selector + " " ];

	if ( !cached ) {
		// Generate a function of recursive functions that can be used to check each element
		if ( !match ) {
			match = tokenize( selector );
		}
		i = match.length;
		while ( i-- ) {
			cached = matcherFromTokens( match[i] );
			if ( cached[ expando ] ) {
				setMatchers.push( cached );
			} else {
				elementMatchers.push( cached );
			}
		}

		// Cache the compiled function
		cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );

		// Save selector and tokenization
		cached.selector = selector;
	}
	return cached;
};

/**
 * A low-level selection function that works with Sizzle's compiled
 *  selector functions
 * @param {String|Function} selector A selector or a pre-compiled
 *  selector function built with Sizzle.compile
 * @param {Element} context
 * @param {Array} [results]
 * @param {Array} [seed] A set of elements to match against
 */
select = Sizzle.select = function( selector, context, results, seed ) {
	var i, tokens, token, type, find,
		compiled = typeof selector === "function" && selector,
		match = !seed && tokenize( (selector = compiled.selector || selector) );

	results = results || [];

	// Try to minimize operations if there is only one selector in the list and no seed
	// (the latter of which guarantees us context)
	if ( match.length === 1 ) {

		// Reduce context if the leading compound selector is an ID
		tokens = match[0] = match[0].slice( 0 );
		if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
				context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[1].type ] ) {

			context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
			if ( !context ) {
				return results;

			// Precompiled matchers will still verify ancestry, so step up a level
			} else if ( compiled ) {
				context = context.parentNode;
			}

			selector = selector.slice( tokens.shift().value.length );
		}

		// Fetch a seed set for right-to-left matching
		i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
		while ( i-- ) {
			token = tokens[i];

			// Abort if we hit a combinator
			if ( Expr.relative[ (type = token.type) ] ) {
				break;
			}
			if ( (find = Expr.find[ type ]) ) {
				// Search, expanding context for leading sibling combinators
				if ( (seed = find(
					token.matches[0].replace( runescape, funescape ),
					rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context
				)) ) {

					// If seed is empty or no tokens remain, we can return early
					tokens.splice( i, 1 );
					selector = seed.length && toSelector( tokens );
					if ( !selector ) {
						push.apply( results, seed );
						return results;
					}

					break;
				}
			}
		}
	}

	// Compile and execute a filtering function if one is not provided
	// Provide `match` to avoid retokenization if we modified the selector above
	( compiled || compile( selector, match ) )(
		seed,
		context,
		!documentIsHTML,
		results,
		!context || rsibling.test( selector ) && testContext( context.parentNode ) || context
	);
	return results;
};

// One-time assignments

// Sort stability
support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;

// Support: Chrome 14-35+
// Always assume duplicates if they aren't passed to the comparison function
support.detectDuplicates = !!hasDuplicate;

// Initialize against the default document
setDocument();

// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
// Detached nodes confoundingly follow *each other*
support.sortDetached = assert(function( el ) {
	// Should return 1, but returns 4 (following)
	return el.compareDocumentPosition( document.createElement("fieldset") ) & 1;
});

// Support: IE<8
// Prevent attribute/property "interpolation"
// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
if ( !assert(function( el ) {
	el.innerHTML = "<a href='#'></a>";
	return el.firstChild.getAttribute("href") === "#" ;
}) ) {
	addHandle( "type|href|height|width", function( elem, name, isXML ) {
		if ( !isXML ) {
			return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
		}
	});
}

// Support: IE<9
// Use defaultValue in place of getAttribute("value")
if ( !support.attributes || !assert(function( el ) {
	el.innerHTML = "<input/>";
	el.firstChild.setAttribute( "value", "" );
	return el.firstChild.getAttribute( "value" ) === "";
}) ) {
	addHandle( "value", function( elem, name, isXML ) {
		if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
			return elem.defaultValue;
		}
	});
}

// Support: IE<9
// Use getAttributeNode to fetch booleans when getAttribute lies
if ( !assert(function( el ) {
	return el.getAttribute("disabled") == null;
}) ) {
	addHandle( booleans, function( elem, name, isXML ) {
		var val;
		if ( !isXML ) {
			return elem[ name ] === true ? name.toLowerCase() :
					(val = elem.getAttributeNode( name )) && val.specified ?
					val.value :
				null;
		}
	});
}

return Sizzle;

})( window );



jQuery.find = Sizzle;
jQuery.expr = Sizzle.selectors;

// Deprecated
jQuery.expr[ ":" ] = jQuery.expr.pseudos;
jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort;
jQuery.text = Sizzle.getText;
jQuery.isXMLDoc = Sizzle.isXML;
jQuery.contains = Sizzle.contains;
jQuery.escapeSelector = Sizzle.escape;




var dir = function( elem, dir, until ) {
	var matched = [],
		truncate = until !== undefined;

	while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {
		if ( elem.nodeType === 1 ) {
			if ( truncate && jQuery( elem ).is( until ) ) {
				break;
			}
			matched.push( elem );
		}
	}
	return matched;
};


var siblings = function( n, elem ) {
	var matched = [];

	for ( ; n; n = n.nextSibling ) {
		if ( n.nodeType === 1 && n !== elem ) {
			matched.push( n );
		}
	}

	return matched;
};


var rneedsContext = jQuery.expr.match.needsContext;



function nodeName( elem, name ) {

  return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();

};
var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i );



var risSimple = /^.[^:#\[\.,]*$/;

// Implement the identical functionality for filter and not
function winnow( elements, qualifier, not ) {
	if ( jQuery.isFunction( qualifier ) ) {
		return jQuery.grep( elements, function( elem, i ) {
			return !!qualifier.call( elem, i, elem ) !== not;
		} );
	}

	// Single element
	if ( qualifier.nodeType ) {
		return jQuery.grep( elements, function( elem ) {
			return ( elem === qualifier ) !== not;
		} );
	}

	// Arraylike of elements (jQuery, arguments, Array)
	if ( typeof qualifier !== "string" ) {
		return jQuery.grep( elements, function( elem ) {
			return ( indexOf.call( qualifier, elem ) > -1 ) !== not;
		} );
	}

	// Simple selector that can be filtered directly, removing non-Elements
	if ( risSimple.test( qualifier ) ) {
		return jQuery.filter( qualifier, elements, not );
	}

	// Complex selector, compare the two sets, removing non-Elements
	qualifier = jQuery.filter( qualifier, elements );
	return jQuery.grep( elements, function( elem ) {
		return ( indexOf.call( qualifier, elem ) > -1 ) !== not && elem.nodeType === 1;
	} );
}

jQuery.filter = function( expr, elems, not ) {
	var elem = elems[ 0 ];

	if ( not ) {
		expr = ":not(" + expr + ")";
	}

	if ( elems.length === 1 && elem.nodeType === 1 ) {
		return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [];
	}

	return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
		return elem.nodeType === 1;
	} ) );
};

jQuery.fn.extend( {
	find: function( selector ) {
		var i, ret,
			len = this.length,
			self = this;

		if ( typeof selector !== "string" ) {
			return this.pushStack( jQuery( selector ).filter( function() {
				for ( i = 0; i < len; i++ ) {
					if ( jQuery.contains( self[ i ], this ) ) {
						return true;
					}
				}
			} ) );
		}

		ret = this.pushStack( [] );

		for ( i = 0; i < len; i++ ) {
			jQuery.find( selector, self[ i ], ret );
		}

		return len > 1 ? jQuery.uniqueSort( ret ) : ret;
	},
	filter: function( selector ) {
		return this.pushStack( winnow( this, selector || [], false ) );
	},
	not: function( selector ) {
		return this.pushStack( winnow( this, selector || [], true ) );
	},
	is: function( selector ) {
		return !!winnow(
			this,

			// If this is a positional/relative selector, check membership in the returned set
			// so $("p:first").is("p:last") won't return true for a doc with two "p".
			typeof selector === "string" && rneedsContext.test( selector ) ?
				jQuery( selector ) :
				selector || [],
			false
		).length;
	}
} );


// Initialize a jQuery object


// A central reference to the root jQuery(document)
var rootjQuery,

	// A simple way to check for HTML strings
	// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
	// Strict HTML recognition (#11290: must start with <)
	// Shortcut simple #id case for speed
	rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,

	init = jQuery.fn.init = function( selector, context, root ) {
		var match, elem;

		// HANDLE: $(""), $(null), $(undefined), $(false)
		if ( !selector ) {
			return this;
		}

		// Method init() accepts an alternate rootjQuery
		// so migrate can support jQuery.sub (gh-2101)
		root = root || rootjQuery;

		// Handle HTML strings
		if ( typeof selector === "string" ) {
			if ( selector[ 0 ] === "<" &&
				selector[ selector.length - 1 ] === ">" &&
				selector.length >= 3 ) {

				// Assume that strings that start and end with <> are HTML and skip the regex check
				match = [ null, selector, null ];

			} else {
				match = rquickExpr.exec( selector );
			}

			// Match html or make sure no context is specified for #id
			if ( match && ( match[ 1 ] || !context ) ) {

				// HANDLE: $(html) -> $(array)
				if ( match[ 1 ] ) {
					context = context instanceof jQuery ? context[ 0 ] : context;

					// Option to run scripts is true for back-compat
					// Intentionally let the error be thrown if parseHTML is not present
					jQuery.merge( this, jQuery.parseHTML(
						match[ 1 ],
						context && context.nodeType ? context.ownerDocument || context : document,
						true
					) );

					// HANDLE: $(html, props)
					if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) {
						for ( match in context ) {

							// Properties of context are called as methods if possible
							if ( jQuery.isFunction( this[ match ] ) ) {
								this[ match ]( context[ match ] );

							// ...and otherwise set as attributes
							} else {
								this.attr( match, context[ match ] );
							}
						}
					}

					return this;

				// HANDLE: $(#id)
				} else {
					elem = document.getElementById( match[ 2 ] );

					if ( elem ) {

						// Inject the element directly into the jQuery object
						this[ 0 ] = elem;
						this.length = 1;
					}
					return this;
				}

			// HANDLE: $(expr, $(...))
			} else if ( !context || context.jquery ) {
				return ( context || root ).find( selector );

			// HANDLE: $(expr, context)
			// (which is just equivalent to: $(context).find(expr)
			} else {
				return this.constructor( context ).find( selector );
			}

		// HANDLE: $(DOMElement)
		} else if ( selector.nodeType ) {
			this[ 0 ] = selector;
			this.length = 1;
			return this;

		// HANDLE: $(function)
		// Shortcut for document ready
		} else if ( jQuery.isFunction( selector ) ) {
			return root.ready !== undefined ?
				root.ready( selector ) :

				// Execute immediately if ready is not present
				selector( jQuery );
		}

		return jQuery.makeArray( selector, this );
	};

// Give the init function the jQuery prototype for later instantiation
init.prototype = jQuery.fn;

// Initialize central reference
rootjQuery = jQuery( document );


var rparentsprev = /^(?:parents|prev(?:Until|All))/,

	// Methods guaranteed to produce a unique set when starting from a unique set
	guaranteedUnique = {
		children: true,
		contents: true,
		next: true,
		prev: true
	};

jQuery.fn.extend( {
	has: function( target ) {
		var targets = jQuery( target, this ),
			l = targets.length;

		return this.filter( function() {
			var i = 0;
			for ( ; i < l; i++ ) {
				if ( jQuery.contains( this, targets[ i ] ) ) {
					return true;
				}
			}
		} );
	},

	closest: function( selectors, context ) {
		var cur,
			i = 0,
			l = this.length,
			matched = [],
			targets = typeof selectors !== "string" && jQuery( selectors );

		// Positional selectors never match, since there's no _selection_ context
		if ( !rneedsContext.test( selectors ) ) {
			for ( ; i < l; i++ ) {
				for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) {

					// Always skip document fragments
					if ( cur.nodeType < 11 && ( targets ?
						targets.index( cur ) > -1 :

						// Don't pass non-elements to Sizzle
						cur.nodeType === 1 &&
							jQuery.find.matchesSelector( cur, selectors ) ) ) {

						matched.push( cur );
						break;
					}
				}
			}
		}

		return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched );
	},

	// Determine the position of an element within the set
	index: function( elem ) {

		// No argument, return index in parent
		if ( !elem ) {
			return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;
		}

		// Index in selector
		if ( typeof elem === "string" ) {
			return indexOf.call( jQuery( elem ), this[ 0 ] );
		}

		// Locate the position of the desired element
		return indexOf.call( this,

			// If it receives a jQuery object, the first element is used
			elem.jquery ? elem[ 0 ] : elem
		);
	},

	add: function( selector, context ) {
		return this.pushStack(
			jQuery.uniqueSort(
				jQuery.merge( this.get(), jQuery( selector, context ) )
			)
		);
	},

	addBack: function( selector ) {
		return this.add( selector == null ?
			this.prevObject : this.prevObject.filter( selector )
		);
	}
} );

function sibling( cur, dir ) {
	while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {}
	return cur;
}

jQuery.each( {
	parent: function( elem ) {
		var parent = elem.parentNode;
		return parent && parent.nodeType !== 11 ? parent : null;
	},
	parents: function( elem ) {
		return dir( elem, "parentNode" );
	},
	parentsUntil: function( elem, i, until ) {
		return dir( elem, "parentNode", until );
	},
	next: function( elem ) {
		return sibling( elem, "nextSibling" );
	},
	prev: function( elem ) {
		return sibling( elem, "previousSibling" );
	},
	nextAll: function( elem ) {
		return dir( elem, "nextSibling" );
	},
	prevAll: function( elem ) {
		return dir( elem, "previousSibling" );
	},
	nextUntil: function( elem, i, until ) {
		return dir( elem, "nextSibling", until );
	},
	prevUntil: function( elem, i, until ) {
		return dir( elem, "previousSibling", until );
	},
	siblings: function( elem ) {
		return siblings( ( elem.parentNode || {} ).firstChild, elem );
	},
	children: function( elem ) {
		return siblings( elem.firstChild );
	},
	contents: function( elem ) {
        if ( nodeName( elem, "iframe" ) ) {
            return elem.contentDocument;
        }

        // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only
        // Treat the template element as a regular one in browsers that
        // don't support it.
        if ( nodeName( elem, "template" ) ) {
            elem = elem.content || elem;
        }

        return jQuery.merge( [], elem.childNodes );
	}
}, function( name, fn ) {
	jQuery.fn[ name ] = function( until, selector ) {
		var matched = jQuery.map( this, fn, until );

		if ( name.slice( -5 ) !== "Until" ) {
			selector = until;
		}

		if ( selector && typeof selector === "string" ) {
			matched = jQuery.filter( selector, matched );
		}

		if ( this.length > 1 ) {

			// Remove duplicates
			if ( !guaranteedUnique[ name ] ) {
				jQuery.uniqueSort( matched );
			}

			// Reverse order for parents* and prev-derivatives
			if ( rparentsprev.test( name ) ) {
				matched.reverse();
			}
		}

		return this.pushStack( matched );
	};
} );
var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g );



// Convert String-formatted options into Object-formatted ones
function createOptions( options ) {
	var object = {};
	jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) {
		object[ flag ] = true;
	} );
	return object;
}

/*
 * Create a callback list using the following parameters:
 *
 *	options: an optional list of space-separated options that will change how
 *			the callback list behaves or a more traditional option object
 *
 * By default a callback list will act like an event callback list and can be
 * "fired" multiple times.
 *
 * Possible options:
 *
 *	once:			will ensure the callback list can only be fired once (like a Deferred)
 *
 *	memory:			will keep track of previous values and will call any callback added
 *					after the list has been fired right away with the latest "memorized"
 *					values (like a Deferred)
 *
 *	unique:			will ensure a callback can only be added once (no duplicate in the list)
 *
 *	stopOnFalse:	interrupt callings when a callback returns false
 *
 */
jQuery.Callbacks = function( options ) {

	// Convert options from String-formatted to Object-formatted if needed
	// (we check in cache first)
	options = typeof options === "string" ?
		createOptions( options ) :
		jQuery.extend( {}, options );

	var // Flag to know if list is currently firing
		firing,

		// Last fire value for non-forgettable lists
		memory,

		// Flag to know if list was already fired
		fired,

		// Flag to prevent firing
		locked,

		// Actual callback list
		list = [],

		// Queue of execution data for repeatable lists
		queue = [],

		// Index of currently firing callback (modified by add/remove as needed)
		firingIndex = -1,

		// Fire callbacks
		fire = function() {

			// Enforce single-firing
			locked = locked || options.once;

			// Execute callbacks for all pending executions,
			// respecting firingIndex overrides and runtime changes
			fired = firing = true;
			for ( ; queue.length; firingIndex = -1 ) {
				memory = queue.shift();
				while ( ++firingIndex < list.length ) {

					// Run callback and check for early termination
					if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
						options.stopOnFalse ) {

						// Jump to end and forget the data so .add doesn't re-fire
						firingIndex = list.length;
						memory = false;
					}
				}
			}

			// Forget the data if we're done with it
			if ( !options.memory ) {
				memory = false;
			}

			firing = false;

			// Clean up if we're done firing for good
			if ( locked ) {

				// Keep an empty list if we have data for future add calls
				if ( memory ) {
					list = [];

				// Otherwise, this object is spent
				} else {
					list = "";
				}
			}
		},

		// Actual Callbacks object
		self = {

			// Add a callback or a collection of callbacks to the list
			add: function() {
				if ( list ) {

					// If we have memory from a past run, we should fire after adding
					if ( memory && !firing ) {
						firingIndex = list.length - 1;
						queue.push( memory );
					}

					( function add( args ) {
						jQuery.each( args, function( _, arg ) {
							if ( jQuery.isFunction( arg ) ) {
								if ( !options.unique || !self.has( arg ) ) {
									list.push( arg );
								}
							} else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) {

								// Inspect recursively
								add( arg );
							}
						} );
					} )( arguments );

					if ( memory && !firing ) {
						fire();
					}
				}
				return this;
			},

			// Remove a callback from the list
			remove: function() {
				jQuery.each( arguments, function( _, arg ) {
					var index;
					while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
						list.splice( index, 1 );

						// Handle firing indexes
						if ( index <= firingIndex ) {
							firingIndex--;
						}
					}
				} );
				return this;
			},

			// Check if a given callback is in the list.
			// If no argument is given, return whether or not list has callbacks attached.
			has: function( fn ) {
				return fn ?
					jQuery.inArray( fn, list ) > -1 :
					list.length > 0;
			},

			// Remove all callbacks from the list
			empty: function() {
				if ( list ) {
					list = [];
				}
				return this;
			},

			// Disable .fire and .add
			// Abort any current/pending executions
			// Clear all callbacks and values
			disable: function() {
				locked = queue = [];
				list = memory = "";
				return this;
			},
			disabled: function() {
				return !list;
			},

			// Disable .fire
			// Also disable .add unless we have memory (since it would have no effect)
			// Abort any pending executions
			lock: function() {
				locked = queue = [];
				if ( !memory && !firing ) {
					list = memory = "";
				}
				return this;
			},
			locked: function() {
				return !!locked;
			},

			// Call all callbacks with the given context and arguments
			fireWith: function( context, args ) {
				if ( !locked ) {
					args = args || [];
					args = [ context, args.slice ? args.slice() : args ];
					queue.push( args );
					if ( !firing ) {
						fire();
					}
				}
				return this;
			},

			// Call all the callbacks with the given arguments
			fire: function() {
				self.fireWith( this, arguments );
				return this;
			},

			// To know if the callbacks have already been called at least once
			fired: function() {
				return !!fired;
			}
		};

	return self;
};


function Identity( v ) {
	return v;
}
function Thrower( ex ) {
	throw ex;
}

function adoptValue( value, resolve, reject, noValue ) {
	var method;

	try {

		// Check for promise aspect first to privilege synchronous behavior
		if ( value && jQuery.isFunction( ( method = value.promise ) ) ) {
			method.call( value ).done( resolve ).fail( reject );

		// Other thenables
		} else if ( value && jQuery.isFunction( ( method = value.then ) ) ) {
			method.call( value, resolve, reject );

		// Other non-thenables
		} else {

			// Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer:
			// * false: [ value ].slice( 0 ) => resolve( value )
			// * true: [ value ].slice( 1 ) => resolve()
			resolve.apply( undefined, [ value ].slice( noValue ) );
		}

	// For Promises/A+, convert exceptions into rejections
	// Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in
	// Deferred#then to conditionally suppress rejection.
	} catch ( value ) {

		// Support: Android 4.0 only
		// Strict mode functions invoked without .call/.apply get global-object context
		reject.apply( undefined, [ value ] );
	}
}

jQuery.extend( {

	Deferred: function( func ) {
		var tuples = [

				// action, add listener, callbacks,
				// ... .then handlers, argument index, [final state]
				[ "notify", "progress", jQuery.Callbacks( "memory" ),
					jQuery.Callbacks( "memory" ), 2 ],
				[ "resolve", "done", jQuery.Callbacks( "once memory" ),
					jQuery.Callbacks( "once memory" ), 0, "resolved" ],
				[ "reject", "fail", jQuery.Callbacks( "once memory" ),
					jQuery.Callbacks( "once memory" ), 1, "rejected" ]
			],
			state = "pending",
			promise = {
				state: function() {
					return state;
				},
				always: function() {
					deferred.done( arguments ).fail( arguments );
					return this;
				},
				"catch": function( fn ) {
					return promise.then( null, fn );
				},

				// Keep pipe for back-compat
				pipe: function( /* fnDone, fnFail, fnProgress */ ) {
					var fns = arguments;

					return jQuery.Deferred( function( newDefer ) {
						jQuery.each( tuples, function( i, tuple ) {

							// Map tuples (progress, done, fail) to arguments (done, fail, progress)
							var fn = jQuery.isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ];

							// deferred.progress(function() { bind to newDefer or newDefer.notify })
							// deferred.done(function() { bind to newDefer or newDefer.resolve })
							// deferred.fail(function() { bind to newDefer or newDefer.reject })
							deferred[ tuple[ 1 ] ]( function() {
								var returned = fn && fn.apply( this, arguments );
								if ( returned && jQuery.isFunction( returned.promise ) ) {
									returned.promise()
										.progress( newDefer.notify )
										.done( newDefer.resolve )
										.fail( newDefer.reject );
								} else {
									newDefer[ tuple[ 0 ] + "With" ](
										this,
										fn ? [ returned ] : arguments
									);
								}
							} );
						} );
						fns = null;
					} ).promise();
				},
				then: function( onFulfilled, onRejected, onProgress ) {
					var maxDepth = 0;
					function resolve( depth, deferred, handler, special ) {
						return function() {
							var that = this,
								args = arguments,
								mightThrow = function() {
									var returned, then;

									// Support: Promises/A+ section 2.3.3.3.3
									// https://promisesaplus.com/#point-59
									// Ignore double-resolution attempts
									if ( depth < maxDepth ) {
										return;
									}

									returned = handler.apply( that, args );

									// Support: Promises/A+ section 2.3.1
									// https://promisesaplus.com/#point-48
									if ( returned === deferred.promise() ) {
										throw new TypeError( "Thenable self-resolution" );
									}

									// Support: Promises/A+ sections 2.3.3.1, 3.5
									// https://promisesaplus.com/#point-54
									// https://promisesaplus.com/#point-75
									// Retrieve `then` only once
									then = returned &&

										// Support: Promises/A+ section 2.3.4
										// https://promisesaplus.com/#point-64
										// Only check objects and functions for thenability
										( typeof returned === "object" ||
											typeof returned === "function" ) &&
										returned.then;

									// Handle a returned thenable
									if ( jQuery.isFunction( then ) ) {

										// Special processors (notify) just wait for resolution
										if ( special ) {
											then.call(
												returned,
												resolve( maxDepth, deferred, Identity, special ),
												resolve( maxDepth, deferred, Thrower, special )
											);

										// Normal processors (resolve) also hook into progress
										} else {

											// ...and disregard older resolution values
											maxDepth++;

											then.call(
												returned,
												resolve( maxDepth, deferred, Identity, special ),
												resolve( maxDepth, deferred, Thrower, special ),
												resolve( maxDepth, deferred, Identity,
													deferred.notifyWith )
											);
										}

									// Handle all other returned values
									} else {

										// Only substitute handlers pass on context
										// and multiple values (non-spec behavior)
										if ( handler !== Identity ) {
											that = undefined;
											args = [ returned ];
										}

										// Process the value(s)
										// Default process is resolve
										( special || deferred.resolveWith )( that, args );
									}
								},

								// Only normal processors (resolve) catch and reject exceptions
								process = special ?
									mightThrow :
									function() {
										try {
											mightThrow();
										} catch ( e ) {

											if ( jQuery.Deferred.exceptionHook ) {
												jQuery.Deferred.exceptionHook( e,
													process.stackTrace );
											}

											// Support: Promises/A+ section 2.3.3.3.4.1
											// https://promisesaplus.com/#point-61
											// Ignore post-resolution exceptions
											if ( depth + 1 >= maxDepth ) {

												// Only substitute handlers pass on context
												// and multiple values (non-spec behavior)
												if ( handler !== Thrower ) {
													that = undefined;
													args = [ e ];
												}

												deferred.rejectWith( that, args );
											}
										}
									};

							// Support: Promises/A+ section 2.3.3.3.1
							// https://promisesaplus.com/#point-57
							// Re-resolve promises immediately to dodge false rejection from
							// subsequent errors
							if ( depth ) {
								process();
							} else {

								// Call an optional hook to record the stack, in case of exception
								// since it's otherwise lost when execution goes async
								if ( jQuery.Deferred.getStackHook ) {
									process.stackTrace = jQuery.Deferred.getStackHook();
								}
								window.setTimeout( process );
							}
						};
					}

					return jQuery.Deferred( function( newDefer ) {

						// progress_handlers.add( ... )
						tuples[ 0 ][ 3 ].add(
							resolve(
								0,
								newDefer,
								jQuery.isFunction( onProgress ) ?
									onProgress :
									Identity,
								newDefer.notifyWith
							)
						);

						// fulfilled_handlers.add( ... )
						tuples[ 1 ][ 3 ].add(
							resolve(
								0,
								newDefer,
								jQuery.isFunction( onFulfilled ) ?
									onFulfilled :
									Identity
							)
						);

						// rejected_handlers.add( ... )
						tuples[ 2 ][ 3 ].add(
							resolve(
								0,
								newDefer,
								jQuery.isFunction( onRejected ) ?
									onRejected :
									Thrower
							)
						);
					} ).promise();
				},

				// Get a promise for this deferred
				// If obj is provided, the promise aspect is added to the object
				promise: function( obj ) {
					return obj != null ? jQuery.extend( obj, promise ) : promise;
				}
			},
			deferred = {};

		// Add list-specific methods
		jQuery.each( tuples, function( i, tuple ) {
			var list = tuple[ 2 ],
				stateString = tuple[ 5 ];

			// promise.progress = list.add
			// promise.done = list.add
			// promise.fail = list.add
			promise[ tuple[ 1 ] ] = list.add;

			// Handle state
			if ( stateString ) {
				list.add(
					function() {

						// state = "resolved" (i.e., fulfilled)
						// state = "rejected"
						state = stateString;
					},

					// rejected_callbacks.disable
					// fulfilled_callbacks.disable
					tuples[ 3 - i ][ 2 ].disable,

					// progress_callbacks.lock
					tuples[ 0 ][ 2 ].lock
				);
			}

			// progress_handlers.fire
			// fulfilled_handlers.fire
			// rejected_handlers.fire
			list.add( tuple[ 3 ].fire );

			// deferred.notify = function() { deferred.notifyWith(...) }
			// deferred.resolve = function() { deferred.resolveWith(...) }
			// deferred.reject = function() { deferred.rejectWith(...) }
			deferred[ tuple[ 0 ] ] = function() {
				deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments );
				return this;
			};

			// deferred.notifyWith = list.fireWith
			// deferred.resolveWith = list.fireWith
			// deferred.rejectWith = list.fireWith
			deferred[ tuple[ 0 ] + "With" ] = list.fireWith;
		} );

		// Make the deferred a promise
		promise.promise( deferred );

		// Call given func if any
		if ( func ) {
			func.call( deferred, deferred );
		}

		// All done!
		return deferred;
	},

	// Deferred helper
	when: function( singleValue ) {
		var

			// count of uncompleted subordinates
			remaining = arguments.length,

			// count of unprocessed arguments
			i = remaining,

			// subordinate fulfillment data
			resolveContexts = Array( i ),
			resolveValues = slice.call( arguments ),

			// the master Deferred
			master = jQuery.Deferred(),

			// subordinate callback factory
			updateFunc = function( i ) {
				return function( value ) {
					resolveContexts[ i ] = this;
					resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
					if ( !( --remaining ) ) {
						master.resolveWith( resolveContexts, resolveValues );
					}
				};
			};

		// Single- and empty arguments are adopted like Promise.resolve
		if ( remaining <= 1 ) {
			adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject,
				!remaining );

			// Use .then() to unwrap secondary thenables (cf. gh-3000)
			if ( master.state() === "pending" ||
				jQuery.isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {

				return master.then();
			}
		}

		// Multiple arguments are aggregated like Promise.all array elements
		while ( i-- ) {
			adoptValue( resolveValues[ i ], updateFunc( i ), master.reject );
		}

		return master.promise();
	}
} );


// These usually indicate a programmer mistake during development,
// warn about them ASAP rather than swallowing them by default.
var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;

jQuery.Deferred.exceptionHook = function( error, stack ) {

	// Support: IE 8 - 9 only
	// Console exists when dev tools are open, which can happen at any time
	if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) {
		window.console.warn( "jQuery.Deferred exception: " + error.message, error.stack, stack );
	}
};




jQuery.readyException = function( error ) {
	window.setTimeout( function() {
		throw error;
	} );
};




// The deferred used on DOM ready
var readyList = jQuery.Deferred();

jQuery.fn.ready = function( fn ) {

	readyList
		.then( fn )

		// Wrap jQuery.readyException in a function so that the lookup
		// happens at the time of error handling instead of callback
		// registration.
		.catch( function( error ) {
			jQuery.readyException( error );
		} );

	return this;
};

jQuery.extend( {

	// Is the DOM ready to be used? Set to true once it occurs.
	isReady: false,

	// A counter to track how many items to wait for before
	// the ready event fires. See #6781
	readyWait: 1,

	// Handle when the DOM is ready
	ready: function( wait ) {

		// Abort if there are pending holds or we're already ready
		if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
			return;
		}

		// Remember that the DOM is ready
		jQuery.isReady = true;

		// If a normal DOM Ready event fired, decrement, and wait if need be
		if ( wait !== true && --jQuery.readyWait > 0 ) {
			return;
		}

		// If there are functions bound, to execute
		readyList.resolveWith( document, [ jQuery ] );
	}
} );

jQuery.ready.then = readyList.then;

// The ready event handler and self cleanup method
function completed() {
	document.removeEventListener( "DOMContentLoaded", completed );
	window.removeEventListener( "load", completed );
	jQuery.ready();
}

// Catch cases where $(document).ready() is called
// after the browser event has already occurred.
// Support: IE <=9 - 10 only
// Older IE sometimes signals "interactive" too soon
if ( document.readyState === "complete" ||
	( document.readyState !== "loading" && !document.documentElement.doScroll ) ) {

	// Handle it asynchronously to allow scripts the opportunity to delay ready
	window.setTimeout( jQuery.ready );

} else {

	// Use the handy event callback
	document.addEventListener( "DOMContentLoaded", completed );

	// A fallback to window.onload, that will always work
	window.addEventListener( "load", completed );
}




// Multifunctional method to get and set values of a collection
// The value/s can optionally be executed if it's a function
var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
	var i = 0,
		len = elems.length,
		bulk = key == null;

	// Sets many values
	if ( jQuery.type( key ) === "object" ) {
		chainable = true;
		for ( i in key ) {
			access( elems, fn, i, key[ i ], true, emptyGet, raw );
		}

	// Sets one value
	} else if ( value !== undefined ) {
		chainable = true;

		if ( !jQuery.isFunction( value ) ) {
			raw = true;
		}

		if ( bulk ) {

			// Bulk operations run against the entire set
			if ( raw ) {
				fn.call( elems, value );
				fn = null;

			// ...except when executing function values
			} else {
				bulk = fn;
				fn = function( elem, key, value ) {
					return bulk.call( jQuery( elem ), value );
				};
			}
		}

		if ( fn ) {
			for ( ; i < len; i++ ) {
				fn(
					elems[ i ], key, raw ?
					value :
					value.call( elems[ i ], i, fn( elems[ i ], key ) )
				);
			}
		}
	}

	if ( chainable ) {
		return elems;
	}

	// Gets
	if ( bulk ) {
		return fn.call( elems );
	}

	return len ? fn( elems[ 0 ], key ) : emptyGet;
};
var acceptData = function( owner ) {

	// Accepts only:
	//  - Node
	//    - Node.ELEMENT_NODE
	//    - Node.DOCUMENT_NODE
	//  - Object
	//    - Any
	return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );
};




function Data() {
	this.expando = jQuery.expando + Data.uid++;
}

Data.uid = 1;

Data.prototype = {

	cache: function( owner ) {

		// Check if the owner object already has a cache
		var value = owner[ this.expando ];

		// If not, create one
		if ( !value ) {
			value = {};

			// We can accept data for non-element nodes in modern browsers,
			// but we should not, see #8335.
			// Always return an empty object.
			if ( acceptData( owner ) ) {

				// If it is a node unlikely to be stringify-ed or looped over
				// use plain assignment
				if ( owner.nodeType ) {
					owner[ this.expando ] = value;

				// Otherwise secure it in a non-enumerable property
				// configurable must be true to allow the property to be
				// deleted when data is removed
				} else {
					Object.defineProperty( owner, this.expando, {
						value: value,
						configurable: true
					} );
				}
			}
		}

		return value;
	},
	set: function( owner, data, value ) {
		var prop,
			cache = this.cache( owner );

		// Handle: [ owner, key, value ] args
		// Always use camelCase key (gh-2257)
		if ( typeof data === "string" ) {
			cache[ jQuery.camelCase( data ) ] = value;

		// Handle: [ owner, { properties } ] args
		} else {

			// Copy the properties one-by-one to the cache object
			for ( prop in data ) {
				cache[ jQuery.camelCase( prop ) ] = data[ prop ];
			}
		}
		return cache;
	},
	get: function( owner, key ) {
		return key === undefined ?
			this.cache( owner ) :

			// Always use camelCase key (gh-2257)
			owner[ this.expando ] && owner[ this.expando ][ jQuery.camelCase( key ) ];
	},
	access: function( owner, key, value ) {

		// In cases where either:
		//
		//   1. No key was specified
		//   2. A string key was specified, but no value provided
		//
		// Take the "read" path and allow the get method to determine
		// which value to return, respectively either:
		//
		//   1. The entire cache object
		//   2. The data stored at the key
		//
		if ( key === undefined ||
				( ( key && typeof key === "string" ) && value === undefined ) ) {

			return this.get( owner, key );
		}

		// When the key is not a string, or both a key and value
		// are specified, set or extend (existing objects) with either:
		//
		//   1. An object of properties
		//   2. A key and value
		//
		this.set( owner, key, value );

		// Since the "set" path can have two possible entry points
		// return the expected data based on which path was taken[*]
		return value !== undefined ? value : key;
	},
	remove: function( owner, key ) {
		var i,
			cache = owner[ this.expando ];

		if ( cache === undefined ) {
			return;
		}

		if ( key !== undefined ) {

			// Support array or space separated string of keys
			if ( Array.isArray( key ) ) {

				// If key is an array of keys...
				// We always set camelCase keys, so remove that.
				key = key.map( jQuery.camelCase );
			} else {
				key = jQuery.camelCase( key );

				// If a key with the spaces exists, use it.
				// Otherwise, create an array by matching non-whitespace
				key = key in cache ?
					[ key ] :
					( key.match( rnothtmlwhite ) || [] );
			}

			i = key.length;

			while ( i-- ) {
				delete cache[ key[ i ] ];
			}
		}

		// Remove the expando if there's no more data
		if ( key === undefined || jQuery.isEmptyObject( cache ) ) {

			// Support: Chrome <=35 - 45
			// Webkit & Blink performance suffers when deleting properties
			// from DOM nodes, so set to undefined instead
			// https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted)
			if ( owner.nodeType ) {
				owner[ this.expando ] = undefined;
			} else {
				delete owner[ this.expando ];
			}
		}
	},
	hasData: function( owner ) {
		var cache = owner[ this.expando ];
		return cache !== undefined && !jQuery.isEmptyObject( cache );
	}
};
var dataPriv = new Data();

var dataUser = new Data();



//	Implementation Summary
//
//	1. Enforce API surface and semantic compatibility with 1.9.x branch
//	2. Improve the module's maintainability by reducing the storage
//		paths to a single mechanism.
//	3. Use the same single mechanism to support "private" and "user" data.
//	4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData)
//	5. Avoid exposing implementation details on user objects (eg. expando properties)
//	6. Provide a clear path for implementation upgrade to WeakMap in 2014

var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,
	rmultiDash = /[A-Z]/g;

function getData( data ) {
	if ( data === "true" ) {
		return true;
	}

	if ( data === "false" ) {
		return false;
	}

	if ( data === "null" ) {
		return null;
	}

	// Only convert to a number if it doesn't change the string
	if ( data === +data + "" ) {
		return +data;
	}

	if ( rbrace.test( data ) ) {
		return JSON.parse( data );
	}

	return data;
}

function dataAttr( elem, key, data ) {
	var name;

	// If nothing was found internally, try to fetch any
	// data from the HTML5 data-* attribute
	if ( data === undefined && elem.nodeType === 1 ) {
		name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase();
		data = elem.getAttribute( name );

		if ( typeof data === "string" ) {
			try {
				data = getData( data );
			} catch ( e ) {}

			// Make sure we set the data so it isn't changed later
			dataUser.set( elem, key, data );
		} else {
			data = undefined;
		}
	}
	return data;
}

jQuery.extend( {
	hasData: function( elem ) {
		return dataUser.hasData( elem ) || dataPriv.hasData( elem );
	},

	data: function( elem, name, data ) {
		return dataUser.access( elem, name, data );
	},

	removeData: function( elem, name ) {
		dataUser.remove( elem, name );
	},

	// TODO: Now that all calls to _data and _removeData have been replaced
	// with direct calls to dataPriv methods, these can be deprecated.
	_data: function( elem, name, data ) {
		return dataPriv.access( elem, name, data );
	},

	_removeData: function( elem, name ) {
		dataPriv.remove( elem, name );
	}
} );

jQuery.fn.extend( {
	data: function( key, value ) {
		var i, name, data,
			elem = this[ 0 ],
			attrs = elem && elem.attributes;

		// Gets all values
		if ( key === undefined ) {
			if ( this.length ) {
				data = dataUser.get( elem );

				if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) {
					i = attrs.length;
					while ( i-- ) {

						// Support: IE 11 only
						// The attrs elements can be null (#14894)
						if ( attrs[ i ] ) {
							name = attrs[ i ].name;
							if ( name.indexOf( "data-" ) === 0 ) {
								name = jQuery.camelCase( name.slice( 5 ) );
								dataAttr( elem, name, data[ name ] );
							}
						}
					}
					dataPriv.set( elem, "hasDataAttrs", true );
				}
			}

			return data;
		}

		// Sets multiple values
		if ( typeof key === "object" ) {
			return this.each( function() {
				dataUser.set( this, key );
			} );
		}

		return access( this, function( value ) {
			var data;

			// The calling jQuery object (element matches) is not empty
			// (and therefore has an element appears at this[ 0 ]) and the
			// `value` parameter was not undefined. An empty jQuery object
			// will result in `undefined` for elem = this[ 0 ] which will
			// throw an exception if an attempt to read a data cache is made.
			if ( elem && value === undefined ) {

				// Attempt to get data from the cache
				// The key will always be camelCased in Data
				data = dataUser.get( elem, key );
				if ( data !== undefined ) {
					return data;
				}

				// Attempt to "discover" the data in
				// HTML5 custom data-* attrs
				data = dataAttr( elem, key );
				if ( data !== undefined ) {
					return data;
				}

				// We tried really hard, but the data doesn't exist.
				return;
			}

			// Set the data...
			this.each( function() {

				// We always store the camelCased key
				dataUser.set( this, key, value );
			} );
		}, null, value, arguments.length > 1, null, true );
	},

	removeData: function( key ) {
		return this.each( function() {
			dataUser.remove( this, key );
		} );
	}
} );


jQuery.extend( {
	queue: function( elem, type, data ) {
		var queue;

		if ( elem ) {
			type = ( type || "fx" ) + "queue";
			queue = dataPriv.get( elem, type );

			// Speed up dequeue by getting out quickly if this is just a lookup
			if ( data ) {
				if ( !queue || Array.isArray( data ) ) {
					queue = dataPriv.access( elem, type, jQuery.makeArray( data ) );
				} else {
					queue.push( data );
				}
			}
			return queue || [];
		}
	},

	dequeue: function( elem, type ) {
		type = type || "fx";

		var queue = jQuery.queue( elem, type ),
			startLength = queue.length,
			fn = queue.shift(),
			hooks = jQuery._queueHooks( elem, type ),
			next = function() {
				jQuery.dequeue( elem, type );
			};

		// If the fx queue is dequeued, always remove the progress sentinel
		if ( fn === "inprogress" ) {
			fn = queue.shift();
			startLength--;
		}

		if ( fn ) {

			// Add a progress sentinel to prevent the fx queue from being
			// automatically dequeued
			if ( type === "fx" ) {
				queue.unshift( "inprogress" );
			}

			// Clear up the last queue stop function
			delete hooks.stop;
			fn.call( elem, next, hooks );
		}

		if ( !startLength && hooks ) {
			hooks.empty.fire();
		}
	},

	// Not public - generate a queueHooks object, or return the current one
	_queueHooks: function( elem, type ) {
		var key = type + "queueHooks";
		return dataPriv.get( elem, key ) || dataPriv.access( elem, key, {
			empty: jQuery.Callbacks( "once memory" ).add( function() {
				dataPriv.remove( elem, [ type + "queue", key ] );
			} )
		} );
	}
} );

jQuery.fn.extend( {
	queue: function( type, data ) {
		var setter = 2;

		if ( typeof type !== "string" ) {
			data = type;
			type = "fx";
			setter--;
		}

		if ( arguments.length < setter ) {
			return jQuery.queue( this[ 0 ], type );
		}

		return data === undefined ?
			this :
			this.each( function() {
				var queue = jQuery.queue( this, type, data );

				// Ensure a hooks for this queue
				jQuery._queueHooks( this, type );

				if ( type === "fx" && queue[ 0 ] !== "inprogress" ) {
					jQuery.dequeue( this, type );
				}
			} );
	},
	dequeue: function( type ) {
		return this.each( function() {
			jQuery.dequeue( this, type );
		} );
	},
	clearQueue: function( type ) {
		return this.queue( type || "fx", [] );
	},

	// Get a promise resolved when queues of a certain type
	// are emptied (fx is the type by default)
	promise: function( type, obj ) {
		var tmp,
			count = 1,
			defer = jQuery.Deferred(),
			elements = this,
			i = this.length,
			resolve = function() {
				if ( !( --count ) ) {
					defer.resolveWith( elements, [ elements ] );
				}
			};

		if ( typeof type !== "string" ) {
			obj = type;
			type = undefined;
		}
		type = type || "fx";

		while ( i-- ) {
			tmp = dataPriv.get( elements[ i ], type + "queueHooks" );
			if ( tmp && tmp.empty ) {
				count++;
				tmp.empty.add( resolve );
			}
		}
		resolve();
		return defer.promise( obj );
	}
} );
var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source;

var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" );


var cssExpand = [ "Top", "Right", "Bottom", "Left" ];

var isHiddenWithinTree = function( elem, el ) {

		// isHiddenWithinTree might be called from jQuery#filter function;
		// in that case, element will be second argument
		elem = el || elem;

		// Inline style trumps all
		return elem.style.display === "none" ||
			elem.style.display === "" &&

			// Otherwise, check computed style
			// Support: Firefox <=43 - 45
			// Disconnected elements can have computed display: none, so first confirm that elem is
			// in the document.
			jQuery.contains( elem.ownerDocument, elem ) &&

			jQuery.css( elem, "display" ) === "none";
	};

var swap = function( elem, options, callback, args ) {
	var ret, name,
		old = {};

	// Remember the old values, and insert the new ones
	for ( name in options ) {
		old[ name ] = elem.style[ name ];
		elem.style[ name ] = options[ name ];
	}

	ret = callback.apply( elem, args || [] );

	// Revert the old values
	for ( name in options ) {
		elem.style[ name ] = old[ name ];
	}

	return ret;
};




function adjustCSS( elem, prop, valueParts, tween ) {
	var adjusted,
		scale = 1,
		maxIterations = 20,
		currentValue = tween ?
			function() {
				return tween.cur();
			} :
			function() {
				return jQuery.css( elem, prop, "" );
			},
		initial = currentValue(),
		unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),

		// Starting value computation is required for potential unit mismatches
		initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) &&
			rcssNum.exec( jQuery.css( elem, prop ) );

	if ( initialInUnit && initialInUnit[ 3 ] !== unit ) {

		// Trust units reported by jQuery.css
		unit = unit || initialInUnit[ 3 ];

		// Make sure we update the tween properties later on
		valueParts = valueParts || [];

		// Iteratively approximate from a nonzero starting point
		initialInUnit = +initial || 1;

		do {

			// If previous iteration zeroed out, double until we get *something*.
			// Use string for doubling so we don't accidentally see scale as unchanged below
			scale = scale || ".5";

			// Adjust and apply
			initialInUnit = initialInUnit / scale;
			jQuery.style( elem, prop, initialInUnit + unit );

		// Update scale, tolerating zero or NaN from tween.cur()
		// Break the loop if scale is unchanged or perfect, or if we've just had enough.
		} while (
			scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations
		);
	}

	if ( valueParts ) {
		initialInUnit = +initialInUnit || +initial || 0;

		// Apply relative offset (+=/-=) if specified
		adjusted = valueParts[ 1 ] ?
			initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] :
			+valueParts[ 2 ];
		if ( tween ) {
			tween.unit = unit;
			tween.start = initialInUnit;
			tween.end = adjusted;
		}
	}
	return adjusted;
}


var defaultDisplayMap = {};

function getDefaultDisplay( elem ) {
	var temp,
		doc = elem.ownerDocument,
		nodeName = elem.nodeName,
		display = defaultDisplayMap[ nodeName ];

	if ( display ) {
		return display;
	}

	temp = doc.body.appendChild( doc.createElement( nodeName ) );
	display = jQuery.css( temp, "display" );

	temp.parentNode.removeChild( temp );

	if ( display === "none" ) {
		display = "block";
	}
	defaultDisplayMap[ nodeName ] = display;

	return display;
}

function showHide( elements, show ) {
	var display, elem,
		values = [],
		index = 0,
		length = elements.length;

	// Determine new display value for elements that need to change
	for ( ; index < length; index++ ) {
		elem = elements[ index ];
		if ( !elem.style ) {
			continue;
		}

		display = elem.style.display;
		if ( show ) {

			// Since we force visibility upon cascade-hidden elements, an immediate (and slow)
			// check is required in this first loop unless we have a nonempty display value (either
			// inline or about-to-be-restored)
			if ( display === "none" ) {
				values[ index ] = dataPriv.get( elem, "display" ) || null;
				if ( !values[ index ] ) {
					elem.style.display = "";
				}
			}
			if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) {
				values[ index ] = getDefaultDisplay( elem );
			}
		} else {
			if ( display !== "none" ) {
				values[ index ] = "none";

				// Remember what we're overwriting
				dataPriv.set( elem, "display", display );
			}
		}
	}

	// Set the display of the elements in a second loop to avoid constant reflow
	for ( index = 0; index < length; index++ ) {
		if ( values[ index ] != null ) {
			elements[ index ].style.display = values[ index ];
		}
	}

	return elements;
}

jQuery.fn.extend( {
	show: function() {
		return showHide( this, true );
	},
	hide: function() {
		return showHide( this );
	},
	toggle: function( state ) {
		if ( typeof state === "boolean" ) {
			return state ? this.show() : this.hide();
		}

		return this.each( function() {
			if ( isHiddenWithinTree( this ) ) {
				jQuery( this ).show();
			} else {
				jQuery( this ).hide();
			}
		} );
	}
} );
var rcheckableType = ( /^(?:checkbox|radio)$/i );

var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]+)/i );

var rscriptType = ( /^$|\/(?:java|ecma)script/i );



// We have to close these tags to support XHTML (#13200)
var wrapMap = {

	// Support: IE <=9 only
	option: [ 1, "<select multiple='multiple'>", "</select>" ],

	// XHTML parsers do not magically insert elements in the
	// same way that tag soup parsers do. So we cannot shorten
	// this by omitting <tbody> or other required elements.
	thead: [ 1, "<table>", "</table>" ],
	col: [ 2, "<table><colgroup>", "</colgroup></table>" ],
	tr: [ 2, "<table><tbody>", "</tbody></table>" ],
	td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],

	_default: [ 0, "", "" ]
};

// Support: IE <=9 only
wrapMap.optgroup = wrapMap.option;

wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
wrapMap.th = wrapMap.td;


function getAll( context, tag ) {

	// Support: IE <=9 - 11 only
	// Use typeof to avoid zero-argument method invocation on host objects (#15151)
	var ret;

	if ( typeof context.getElementsByTagName !== "undefined" ) {
		ret = context.getElementsByTagName( tag || "*" );

	} else if ( typeof context.querySelectorAll !== "undefined" ) {
		ret = context.querySelectorAll( tag || "*" );

	} else {
		ret = [];
	}

	if ( tag === undefined || tag && nodeName( context, tag ) ) {
		return jQuery.merge( [ context ], ret );
	}

	return ret;
}


// Mark scripts as having already been evaluated
function setGlobalEval( elems, refElements ) {
	var i = 0,
		l = elems.length;

	for ( ; i < l; i++ ) {
		dataPriv.set(
			elems[ i ],
			"globalEval",
			!refElements || dataPriv.get( refElements[ i ], "globalEval" )
		);
	}
}


var rhtml = /<|&#?\w+;/;

function buildFragment( elems, context, scripts, selection, ignored ) {
	var elem, tmp, tag, wrap, contains, j,
		fragment = context.createDocumentFragment(),
		nodes = [],
		i = 0,
		l = elems.length;

	for ( ; i < l; i++ ) {
		elem = elems[ i ];

		if ( elem || elem === 0 ) {

			// Add nodes directly
			if ( jQuery.type( elem ) === "object" ) {

				// Support: Android <=4.0 only, PhantomJS 1 only
				// push.apply(_, arraylike) throws on ancient WebKit
				jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );

			// Convert non-html into a text node
			} else if ( !rhtml.test( elem ) ) {
				nodes.push( context.createTextNode( elem ) );

			// Convert html into DOM nodes
			} else {
				tmp = tmp || fragment.appendChild( context.createElement( "div" ) );

				// Deserialize a standard representation
				tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
				wrap = wrapMap[ tag ] || wrapMap._default;
				tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];

				// Descend through wrappers to the right content
				j = wrap[ 0 ];
				while ( j-- ) {
					tmp = tmp.lastChild;
				}

				// Support: Android <=4.0 only, PhantomJS 1 only
				// push.apply(_, arraylike) throws on ancient WebKit
				jQuery.merge( nodes, tmp.childNodes );

				// Remember the top-level container
				tmp = fragment.firstChild;

				// Ensure the created nodes are orphaned (#12392)
				tmp.textContent = "";
			}
		}
	}

	// Remove wrapper from fragment
	fragment.textContent = "";

	i = 0;
	while ( ( elem = nodes[ i++ ] ) ) {

		// Skip elements already in the context collection (trac-4087)
		if ( selection && jQuery.inArray( elem, selection ) > -1 ) {
			if ( ignored ) {
				ignored.push( elem );
			}
			continue;
		}

		contains = jQuery.contains( elem.ownerDocument, elem );

		// Append to fragment
		tmp = getAll( fragment.appendChild( elem ), "script" );

		// Preserve script evaluation history
		if ( contains ) {
			setGlobalEval( tmp );
		}

		// Capture executables
		if ( scripts ) {
			j = 0;
			while ( ( elem = tmp[ j++ ] ) ) {
				if ( rscriptType.test( elem.type || "" ) ) {
					scripts.push( elem );
				}
			}
		}
	}

	return fragment;
}


( function() {
	var fragment = document.createDocumentFragment(),
		div = fragment.appendChild( document.createElement( "div" ) ),
		input = document.createElement( "input" );

	// Support: Android 4.0 - 4.3 only
	// Check state lost if the name is set (#11217)
	// Support: Windows Web Apps (WWA)
	// `name` and `type` must use .setAttribute for WWA (#14901)
	input.setAttribute( "type", "radio" );
	input.setAttribute( "checked", "checked" );
	input.setAttribute( "name", "t" );

	div.appendChild( input );

	// Support: Android <=4.1 only
	// Older WebKit doesn't clone checked state correctly in fragments
	support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;

	// Support: IE <=11 only
	// Make sure textarea (and checkbox) defaultValue is properly cloned
	div.innerHTML = "<textarea>x</textarea>";
	support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;
} )();
var documentElement = document.documentElement;



var
	rkeyEvent = /^key/,
	rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,
	rtypenamespace = /^([^.]*)(?:\.(.+)|)/;

function returnTrue() {
	return true;
}

function returnFalse() {
	return false;
}

// Support: IE <=9 only
// See #13393 for more info
function safeActiveElement() {
	try {
		return document.activeElement;
	} catch ( err ) { }
}

function on( elem, types, selector, data, fn, one ) {
	var origFn, type;

	// Types can be a map of types/handlers
	if ( typeof types === "object" ) {

		// ( types-Object, selector, data )
		if ( typeof selector !== "string" ) {

			// ( types-Object, data )
			data = data || selector;
			selector = undefined;
		}
		for ( type in types ) {
			on( elem, type, selector, data, types[ type ], one );
		}
		return elem;
	}

	if ( data == null && fn == null ) {

		// ( types, fn )
		fn = selector;
		data = selector = undefined;
	} else if ( fn == null ) {
		if ( typeof selector === "string" ) {

			// ( types, selector, fn )
			fn = data;
			data = undefined;
		} else {

			// ( types, data, fn )
			fn = data;
			data = selector;
			selector = undefined;
		}
	}
	if ( fn === false ) {
		fn = returnFalse;
	} else if ( !fn ) {
		return elem;
	}

	if ( one === 1 ) {
		origFn = fn;
		fn = function( event ) {

			// Can use an empty set, since event contains the info
			jQuery().off( event );
			return origFn.apply( this, arguments );
		};

		// Use same guid so caller can remove using origFn
		fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
	}
	return elem.each( function() {
		jQuery.event.add( this, types, fn, data, selector );
	} );
}

/*
 * Helper functions for managing events -- not part of the public interface.
 * Props to Dean Edwards' addEvent library for many of the ideas.
 */
jQuery.event = {

	global: {},

	add: function( elem, types, handler, data, selector ) {

		var handleObjIn, eventHandle, tmp,
			events, t, handleObj,
			special, handlers, type, namespaces, origType,
			elemData = dataPriv.get( elem );

		// Don't attach events to noData or text/comment nodes (but allow plain objects)
		if ( !elemData ) {
			return;
		}

		// Caller can pass in an object of custom data in lieu of the handler
		if ( handler.handler ) {
			handleObjIn = handler;
			handler = handleObjIn.handler;
			selector = handleObjIn.selector;
		}

		// Ensure that invalid selectors throw exceptions at attach time
		// Evaluate against documentElement in case elem is a non-element node (e.g., document)
		if ( selector ) {
			jQuery.find.matchesSelector( documentElement, selector );
		}

		// Make sure that the handler has a unique ID, used to find/remove it later
		if ( !handler.guid ) {
			handler.guid = jQuery.guid++;
		}

		// Init the element's event structure and main handler, if this is the first
		if ( !( events = elemData.events ) ) {
			events = elemData.events = {};
		}
		if ( !( eventHandle = elemData.handle ) ) {
			eventHandle = elemData.handle = function( e ) {

				// Discard the second event of a jQuery.event.trigger() and
				// when an event is called after a page has unloaded
				return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
					jQuery.event.dispatch.apply( elem, arguments ) : undefined;
			};
		}

		// Handle multiple events separated by a space
		types = ( types || "" ).match( rnothtmlwhite ) || [ "" ];
		t = types.length;
		while ( t-- ) {
			tmp = rtypenamespace.exec( types[ t ] ) || [];
			type = origType = tmp[ 1 ];
			namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();

			// There *must* be a type, no attaching namespace-only handlers
			if ( !type ) {
				continue;
			}

			// If event changes its type, use the special event handlers for the changed type
			special = jQuery.event.special[ type ] || {};

			// If selector defined, determine special event api type, otherwise given type
			type = ( selector ? special.delegateType : special.bindType ) || type;

			// Update special based on newly reset type
			special = jQuery.event.special[ type ] || {};

			// handleObj is passed to all event handlers
			handleObj = jQuery.extend( {
				type: type,
				origType: origType,
				data: data,
				handler: handler,
				guid: handler.guid,
				selector: selector,
				needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
				namespace: namespaces.join( "." )
			}, handleObjIn );

			// Init the event handler queue if we're the first
			if ( !( handlers = events[ type ] ) ) {
				handlers = events[ type ] = [];
				handlers.delegateCount = 0;

				// Only use addEventListener if the special events handler returns false
				if ( !special.setup ||
					special.setup.call( elem, data, namespaces, eventHandle ) === false ) {

					if ( elem.addEventListener ) {
						elem.addEventListener( type, eventHandle );
					}
				}
			}

			if ( special.add ) {
				special.add.call( elem, handleObj );

				if ( !handleObj.handler.guid ) {
					handleObj.handler.guid = handler.guid;
				}
			}

			// Add to the element's handler list, delegates in front
			if ( selector ) {
				handlers.splice( handlers.delegateCount++, 0, handleObj );
			} else {
				handlers.push( handleObj );
			}

			// Keep track of which events have ever been used, for event optimization
			jQuery.event.global[ type ] = true;
		}

	},

	// Detach an event or set of events from an element
	remove: function( elem, types, handler, selector, mappedTypes ) {

		var j, origCount, tmp,
			events, t, handleObj,
			special, handlers, type, namespaces, origType,
			elemData = dataPriv.hasData( elem ) && dataPriv.get( elem );

		if ( !elemData || !( events = elemData.events ) ) {
			return;
		}

		// Once for each type.namespace in types; type may be omitted
		types = ( types || "" ).match( rnothtmlwhite ) || [ "" ];
		t = types.length;
		while ( t-- ) {
			tmp = rtypenamespace.exec( types[ t ] ) || [];
			type = origType = tmp[ 1 ];
			namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();

			// Unbind all events (on this namespace, if provided) for the element
			if ( !type ) {
				for ( type in events ) {
					jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
				}
				continue;
			}

			special = jQuery.event.special[ type ] || {};
			type = ( selector ? special.delegateType : special.bindType ) || type;
			handlers = events[ type ] || [];
			tmp = tmp[ 2 ] &&
				new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" );

			// Remove matching events
			origCount = j = handlers.length;
			while ( j-- ) {
				handleObj = handlers[ j ];

				if ( ( mappedTypes || origType === handleObj.origType ) &&
					( !handler || handler.guid === handleObj.guid ) &&
					( !tmp || tmp.test( handleObj.namespace ) ) &&
					( !selector || selector === handleObj.selector ||
						selector === "**" && handleObj.selector ) ) {
					handlers.splice( j, 1 );

					if ( handleObj.selector ) {
						handlers.delegateCount--;
					}
					if ( special.remove ) {
						special.remove.call( elem, handleObj );
					}
				}
			}

			// Remove generic event handler if we removed something and no more handlers exist
			// (avoids potential for endless recursion during removal of special event handlers)
			if ( origCount && !handlers.length ) {
				if ( !special.teardown ||
					special.teardown.call( elem, namespaces, elemData.handle ) === false ) {

					jQuery.removeEvent( elem, type, elemData.handle );
				}

				delete events[ type ];
			}
		}

		// Remove data and the expando if it's no longer used
		if ( jQuery.isEmptyObject( events ) ) {
			dataPriv.remove( elem, "handle events" );
		}
	},

	dispatch: function( nativeEvent ) {

		// Make a writable jQuery.Event from the native event object
		var event = jQuery.event.fix( nativeEvent );

		var i, j, ret, matched, handleObj, handlerQueue,
			args = new Array( arguments.length ),
			handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [],
			special = jQuery.event.special[ event.type ] || {};

		// Use the fix-ed jQuery.Event rather than the (read-only) native event
		args[ 0 ] = event;

		for ( i = 1; i < arguments.length; i++ ) {
			args[ i ] = arguments[ i ];
		}

		event.delegateTarget = this;

		// Call the preDispatch hook for the mapped type, and let it bail if desired
		if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
			return;
		}

		// Determine handlers
		handlerQueue = jQuery.event.handlers.call( this, event, handlers );

		// Run delegates first; they may want to stop propagation beneath us
		i = 0;
		while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {
			event.currentTarget = matched.elem;

			j = 0;
			while ( ( handleObj = matched.handlers[ j++ ] ) &&
				!event.isImmediatePropagationStopped() ) {

				// Triggered event must either 1) have no namespace, or 2) have namespace(s)
				// a subset or equal to those in the bound event (both can have no namespace).
				if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) {

					event.handleObj = handleObj;
					event.data = handleObj.data;

					ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||
						handleObj.handler ).apply( matched.elem, args );

					if ( ret !== undefined ) {
						if ( ( event.result = ret ) === false ) {
							event.preventDefault();
							event.stopPropagation();
						}
					}
				}
			}
		}

		// Call the postDispatch hook for the mapped type
		if ( special.postDispatch ) {
			special.postDispatch.call( this, event );
		}

		return event.result;
	},

	handlers: function( event, handlers ) {
		var i, handleObj, sel, matchedHandlers, matchedSelectors,
			handlerQueue = [],
			delegateCount = handlers.delegateCount,
			cur = event.target;

		// Find delegate handlers
		if ( delegateCount &&

			// Support: IE <=9
			// Black-hole SVG <use> instance trees (trac-13180)
			cur.nodeType &&

			// Support: Firefox <=42
			// Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861)
			// https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click
			// Support: IE 11 only
			// ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343)
			!( event.type === "click" && event.button >= 1 ) ) {

			for ( ; cur !== this; cur = cur.parentNode || this ) {

				// Don't check non-elements (#13208)
				// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
				if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) {
					matchedHandlers = [];
					matchedSelectors = {};
					for ( i = 0; i < delegateCount; i++ ) {
						handleObj = handlers[ i ];

						// Don't conflict with Object.prototype properties (#13203)
						sel = handleObj.selector + " ";

						if ( matchedSelectors[ sel ] === undefined ) {
							matchedSelectors[ sel ] = handleObj.needsContext ?
								jQuery( sel, this ).index( cur ) > -1 :
								jQuery.find( sel, this, null, [ cur ] ).length;
						}
						if ( matchedSelectors[ sel ] ) {
							matchedHandlers.push( handleObj );
						}
					}
					if ( matchedHandlers.length ) {
						handlerQueue.push( { elem: cur, handlers: matchedHandlers } );
					}
				}
			}
		}

		// Add the remaining (directly-bound) handlers
		cur = this;
		if ( delegateCount < handlers.length ) {
			handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } );
		}

		return handlerQueue;
	},

	addProp: function( name, hook ) {
		Object.defineProperty( jQuery.Event.prototype, name, {
			enumerable: true,
			configurable: true,

			get: jQuery.isFunction( hook ) ?
				function() {
					if ( this.originalEvent ) {
							return hook( this.originalEvent );
					}
				} :
				function() {
					if ( this.originalEvent ) {
							return this.originalEvent[ name ];
					}
				},

			set: function( value ) {
				Object.defineProperty( this, name, {
					enumerable: true,
					configurable: true,
					writable: true,
					value: value
				} );
			}
		} );
	},

	fix: function( originalEvent ) {
		return originalEvent[ jQuery.expando ] ?
			originalEvent :
			new jQuery.Event( originalEvent );
	},

	special: {
		load: {

			// Prevent triggered image.load events from bubbling to window.load
			noBubble: true
		},
		focus: {

			// Fire native event if possible so blur/focus sequence is correct
			trigger: function() {
				if ( this !== safeActiveElement() && this.focus ) {
					this.focus();
					return false;
				}
			},
			delegateType: "focusin"
		},
		blur: {
			trigger: function() {
				if ( this === safeActiveElement() && this.blur ) {
					this.blur();
					return false;
				}
			},
			delegateType: "focusout"
		},
		click: {

			// For checkable types, fire native event so checked state will be right
			trigger: function() {
				if ( rcheckableType.test( this.type ) &&
					this.click && nodeName( this, "input" ) ) {

					this.click();
					return false;
				}
			},

			// For cross-browser consistency, don't fire native .click() on links
			_default: function( event ) {
				return nodeName( event.target, "a" );
			}
		},

		beforeunload: {
			postDispatch: function( event ) {

				// Support: Firefox 20+
				// Firefox doesn't alert if the returnValue field is not set.
				if ( event.result !== undefined && event.originalEvent ) {
					event.originalEvent.returnValue = event.result;
				}
			}
		}
	}
};

jQuery.removeEvent = function( elem, type, handle ) {

	// This "if" is needed for plain objects
	if ( elem.removeEventListener ) {
		elem.removeEventListener( type, handle );
	}
};

jQuery.Event = function( src, props ) {

	// Allow instantiation without the 'new' keyword
	if ( !( this instanceof jQuery.Event ) ) {
		return new jQuery.Event( src, props );
	}

	// Event object
	if ( src && src.type ) {
		this.originalEvent = src;
		this.type = src.type;

		// Events bubbling up the document may have been marked as prevented
		// by a handler lower down the tree; reflect the correct value.
		this.isDefaultPrevented = src.defaultPrevented ||
				src.defaultPrevented === undefined &&

				// Support: Android <=2.3 only
				src.returnValue === false ?
			returnTrue :
			returnFalse;

		// Create target properties
		// Support: Safari <=6 - 7 only
		// Target should not be a text node (#504, #13143)
		this.target = ( src.target && src.target.nodeType === 3 ) ?
			src.target.parentNode :
			src.target;

		this.currentTarget = src.currentTarget;
		this.relatedTarget = src.relatedTarget;

	// Event type
	} else {
		this.type = src;
	}

	// Put explicitly provided properties onto the event object
	if ( props ) {
		jQuery.extend( this, props );
	}

	// Create a timestamp if incoming event doesn't have one
	this.timeStamp = src && src.timeStamp || jQuery.now();

	// Mark it as fixed
	this[ jQuery.expando ] = true;
};

// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
jQuery.Event.prototype = {
	constructor: jQuery.Event,
	isDefaultPrevented: returnFalse,
	isPropagationStopped: returnFalse,
	isImmediatePropagationStopped: returnFalse,
	isSimulated: false,

	preventDefault: function() {
		var e = this.originalEvent;

		this.isDefaultPrevented = returnTrue;

		if ( e && !this.isSimulated ) {
			e.preventDefault();
		}
	},
	stopPropagation: function() {
		var e = this.originalEvent;

		this.isPropagationStopped = returnTrue;

		if ( e && !this.isSimulated ) {
			e.stopPropagation();
		}
	},
	stopImmediatePropagation: function() {
		var e = this.originalEvent;

		this.isImmediatePropagationStopped = returnTrue;

		if ( e && !this.isSimulated ) {
			e.stopImmediatePropagation();
		}

		this.stopPropagation();
	}
};

// Includes all common event props including KeyEvent and MouseEvent specific props
jQuery.each( {
	altKey: true,
	bubbles: true,
	cancelable: true,
	changedTouches: true,
	ctrlKey: true,
	detail: true,
	eventPhase: true,
	metaKey: true,
	pageX: true,
	pageY: true,
	shiftKey: true,
	view: true,
	"char": true,
	charCode: true,
	key: true,
	keyCode: true,
	button: true,
	buttons: true,
	clientX: true,
	clientY: true,
	offsetX: true,
	offsetY: true,
	pointerId: true,
	pointerType: true,
	screenX: true,
	screenY: true,
	targetTouches: true,
	toElement: true,
	touches: true,

	which: function( event ) {
		var button = event.button;

		// Add which for key events
		if ( event.which == null && rkeyEvent.test( event.type ) ) {
			return event.charCode != null ? event.charCode : event.keyCode;
		}

		// Add which for click: 1 === left; 2 === middle; 3 === right
		if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) {
			if ( button & 1 ) {
				return 1;
			}

			if ( button & 2 ) {
				return 3;
			}

			if ( button & 4 ) {
				return 2;
			}

			return 0;
		}

		return event.which;
	}
}, jQuery.event.addProp );

// Create mouseenter/leave events using mouseover/out and event-time checks
// so that event delegation works in jQuery.
// Do the same for pointerenter/pointerleave and pointerover/pointerout
//
// Support: Safari 7 only
// Safari sends mouseenter too often; see:
// https://bugs.chromium.org/p/chromium/issues/detail?id=470258
// for the description of the bug (it existed in older Chrome versions as well).
jQuery.each( {
	mouseenter: "mouseover",
	mouseleave: "mouseout",
	pointerenter: "pointerover",
	pointerleave: "pointerout"
}, function( orig, fix ) {
	jQuery.event.special[ orig ] = {
		delegateType: fix,
		bindType: fix,

		handle: function( event ) {
			var ret,
				target = this,
				related = event.relatedTarget,
				handleObj = event.handleObj;

			// For mouseenter/leave call the handler if related is outside the target.
			// NB: No relatedTarget if the mouse left/entered the browser window
			if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) {
				event.type = handleObj.origType;
				ret = handleObj.handler.apply( this, arguments );
				event.type = fix;
			}
			return ret;
		}
	};
} );

jQuery.fn.extend( {

	on: function( types, selector, data, fn ) {
		return on( this, types, selector, data, fn );
	},
	one: function( types, selector, data, fn ) {
		return on( this, types, selector, data, fn, 1 );
	},
	off: function( types, selector, fn ) {
		var handleObj, type;
		if ( types && types.preventDefault && types.handleObj ) {

			// ( event )  dispatched jQuery.Event
			handleObj = types.handleObj;
			jQuery( types.delegateTarget ).off(
				handleObj.namespace ?
					handleObj.origType + "." + handleObj.namespace :
					handleObj.origType,
				handleObj.selector,
				handleObj.handler
			);
			return this;
		}
		if ( typeof types === "object" ) {

			// ( types-object [, selector] )
			for ( type in types ) {
				this.off( type, selector, types[ type ] );
			}
			return this;
		}
		if ( selector === false || typeof selector === "function" ) {

			// ( types [, fn] )
			fn = selector;
			selector = undefined;
		}
		if ( fn === false ) {
			fn = returnFalse;
		}
		return this.each( function() {
			jQuery.event.remove( this, types, fn, selector );
		} );
	}
} );


var

	/* eslint-disable max-len */

	// See https://github.com/eslint/eslint/issues/3229
	rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,

	/* eslint-enable */

	// Support: IE <=10 - 11, Edge 12 - 13
	// In IE/Edge using regex groups here causes severe slowdowns.
	// See https://connect.microsoft.com/IE/feedback/details/1736512/
	rnoInnerhtml = /<script|<style|<link/i,

	// checked="checked" or checked
	rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
	rscriptTypeMasked = /^true\/(.*)/,
	rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;

// Prefer a tbody over its parent table for containing new rows
function manipulationTarget( elem, content ) {
	if ( nodeName( elem, "table" ) &&
		nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) {

		return jQuery( ">tbody", elem )[ 0 ] || elem;
	}

	return elem;
}

// Replace/restore the type attribute of script elements for safe DOM manipulation
function disableScript( elem ) {
	elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type;
	return elem;
}
function restoreScript( elem ) {
	var match = rscriptTypeMasked.exec( elem.type );

	if ( match ) {
		elem.type = match[ 1 ];
	} else {
		elem.removeAttribute( "type" );
	}

	return elem;
}

function cloneCopyEvent( src, dest ) {
	var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;

	if ( dest.nodeType !== 1 ) {
		return;
	}

	// 1. Copy private data: events, handlers, etc.
	if ( dataPriv.hasData( src ) ) {
		pdataOld = dataPriv.access( src );
		pdataCur = dataPriv.set( dest, pdataOld );
		events = pdataOld.events;

		if ( events ) {
			delete pdataCur.handle;
			pdataCur.events = {};

			for ( type in events ) {
				for ( i = 0, l = events[ type ].length; i < l; i++ ) {
					jQuery.event.add( dest, type, events[ type ][ i ] );
				}
			}
		}
	}

	// 2. Copy user data
	if ( dataUser.hasData( src ) ) {
		udataOld = dataUser.access( src );
		udataCur = jQuery.extend( {}, udataOld );

		dataUser.set( dest, udataCur );
	}
}

// Fix IE bugs, see support tests
function fixInput( src, dest ) {
	var nodeName = dest.nodeName.toLowerCase();

	// Fails to persist the checked state of a cloned checkbox or radio button.
	if ( nodeName === "input" && rcheckableType.test( src.type ) ) {
		dest.checked = src.checked;

	// Fails to return the selected option to the default selected state when cloning options
	} else if ( nodeName === "input" || nodeName === "textarea" ) {
		dest.defaultValue = src.defaultValue;
	}
}

function domManip( collection, args, callback, ignored ) {

	// Flatten any nested arrays
	args = concat.apply( [], args );

	var fragment, first, scripts, hasScripts, node, doc,
		i = 0,
		l = collection.length,
		iNoClone = l - 1,
		value = args[ 0 ],
		isFunction = jQuery.isFunction( value );

	// We can't cloneNode fragments that contain checked, in WebKit
	if ( isFunction ||
			( l > 1 && typeof value === "string" &&
				!support.checkClone && rchecked.test( value ) ) ) {
		return collection.each( function( index ) {
			var self = collection.eq( index );
			if ( isFunction ) {
				args[ 0 ] = value.call( this, index, self.html() );
			}
			domManip( self, args, callback, ignored );
		} );
	}

	if ( l ) {
		fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored );
		first = fragment.firstChild;

		if ( fragment.childNodes.length === 1 ) {
			fragment = first;
		}

		// Require either new content or an interest in ignored elements to invoke the callback
		if ( first || ignored ) {
			scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
			hasScripts = scripts.length;

			// Use the original fragment for the last item
			// instead of the first because it can end up
			// being emptied incorrectly in certain situations (#8070).
			for ( ; i < l; i++ ) {
				node = fragment;

				if ( i !== iNoClone ) {
					node = jQuery.clone( node, true, true );

					// Keep references to cloned scripts for later restoration
					if ( hasScripts ) {

						// Support: Android <=4.0 only, PhantomJS 1 only
						// push.apply(_, arraylike) throws on ancient WebKit
						jQuery.merge( scripts, getAll( node, "script" ) );
					}
				}

				callback.call( collection[ i ], node, i );
			}

			if ( hasScripts ) {
				doc = scripts[ scripts.length - 1 ].ownerDocument;

				// Reenable scripts
				jQuery.map( scripts, restoreScript );

				// Evaluate executable scripts on first document insertion
				for ( i = 0; i < hasScripts; i++ ) {
					node = scripts[ i ];
					if ( rscriptType.test( node.type || "" ) &&
						!dataPriv.access( node, "globalEval" ) &&
						jQuery.contains( doc, node ) ) {

						if ( node.src ) {

							// Optional AJAX dependency, but won't run scripts if not present
							if ( jQuery._evalUrl ) {
								jQuery._evalUrl( node.src );
							}
						} else {
							DOMEval( node.textContent.replace( rcleanScript, "" ), doc );
						}
					}
				}
			}
		}
	}

	return collection;
}

function remove( elem, selector, keepData ) {
	var node,
		nodes = selector ? jQuery.filter( selector, elem ) : elem,
		i = 0;

	for ( ; ( node = nodes[ i ] ) != null; i++ ) {
		if ( !keepData && node.nodeType === 1 ) {
			jQuery.cleanData( getAll( node ) );
		}

		if ( node.parentNode ) {
			if ( keepData && jQuery.contains( node.ownerDocument, node ) ) {
				setGlobalEval( getAll( node, "script" ) );
			}
			node.parentNode.removeChild( node );
		}
	}

	return elem;
}

jQuery.extend( {
	htmlPrefilter: function( html ) {
		return html.replace( rxhtmlTag, "<$1></$2>" );
	},

	clone: function( elem, dataAndEvents, deepDataAndEvents ) {
		var i, l, srcElements, destElements,
			clone = elem.cloneNode( true ),
			inPage = jQuery.contains( elem.ownerDocument, elem );

		// Fix IE cloning issues
		if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&
				!jQuery.isXMLDoc( elem ) ) {

			// We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2
			destElements = getAll( clone );
			srcElements = getAll( elem );

			for ( i = 0, l = srcElements.length; i < l; i++ ) {
				fixInput( srcElements[ i ], destElements[ i ] );
			}
		}

		// Copy the events from the original to the clone
		if ( dataAndEvents ) {
			if ( deepDataAndEvents ) {
				srcElements = srcElements || getAll( elem );
				destElements = destElements || getAll( clone );

				for ( i = 0, l = srcElements.length; i < l; i++ ) {
					cloneCopyEvent( srcElements[ i ], destElements[ i ] );
				}
			} else {
				cloneCopyEvent( elem, clone );
			}
		}

		// Preserve script evaluation history
		destElements = getAll( clone, "script" );
		if ( destElements.length > 0 ) {
			setGlobalEval( destElements, !inPage && getAll( elem, "script" ) );
		}

		// Return the cloned set
		return clone;
	},

	cleanData: function( elems ) {
		var data, elem, type,
			special = jQuery.event.special,
			i = 0;

		for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {
			if ( acceptData( elem ) ) {
				if ( ( data = elem[ dataPriv.expando ] ) ) {
					if ( data.events ) {
						for ( type in data.events ) {
							if ( special[ type ] ) {
								jQuery.event.remove( elem, type );

							// This is a shortcut to avoid jQuery.event.remove's overhead
							} else {
								jQuery.removeEvent( elem, type, data.handle );
							}
						}
					}

					// Support: Chrome <=35 - 45+
					// Assign undefined instead of using delete, see Data#remove
					elem[ dataPriv.expando ] = undefined;
				}
				if ( elem[ dataUser.expando ] ) {

					// Support: Chrome <=35 - 45+
					// Assign undefined instead of using delete, see Data#remove
					elem[ dataUser.expando ] = undefined;
				}
			}
		}
	}
} );

jQuery.fn.extend( {
	detach: function( selector ) {
		return remove( this, selector, true );
	},

	remove: function( selector ) {
		return remove( this, selector );
	},

	text: function( value ) {
		return access( this, function( value ) {
			return value === undefined ?
				jQuery.text( this ) :
				this.empty().each( function() {
					if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
						this.textContent = value;
					}
				} );
		}, null, value, arguments.length );
	},

	append: function() {
		return domManip( this, arguments, function( elem ) {
			if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
				var target = manipulationTarget( this, elem );
				target.appendChild( elem );
			}
		} );
	},

	prepend: function() {
		return domManip( this, arguments, function( elem ) {
			if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
				var target = manipulationTarget( this, elem );
				target.insertBefore( elem, target.firstChild );
			}
		} );
	},

	before: function() {
		return domManip( this, arguments, function( elem ) {
			if ( this.parentNode ) {
				this.parentNode.insertBefore( elem, this );
			}
		} );
	},

	after: function() {
		return domManip( this, arguments, function( elem ) {
			if ( this.parentNode ) {
				this.parentNode.insertBefore( elem, this.nextSibling );
			}
		} );
	},

	empty: function() {
		var elem,
			i = 0;

		for ( ; ( elem = this[ i ] ) != null; i++ ) {
			if ( elem.nodeType === 1 ) {

				// Prevent memory leaks
				jQuery.cleanData( getAll( elem, false ) );

				// Remove any remaining nodes
				elem.textContent = "";
			}
		}

		return this;
	},

	clone: function( dataAndEvents, deepDataAndEvents ) {
		dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
		deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;

		return this.map( function() {
			return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
		} );
	},

	html: function( value ) {
		return access( this, function( value ) {
			var elem = this[ 0 ] || {},
				i = 0,
				l = this.length;

			if ( value === undefined && elem.nodeType === 1 ) {
				return elem.innerHTML;
			}

			// See if we can take a shortcut and just use innerHTML
			if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
				!wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) {

				value = jQuery.htmlPrefilter( value );

				try {
					for ( ; i < l; i++ ) {
						elem = this[ i ] || {};

						// Remove element nodes and prevent memory leaks
						if ( elem.nodeType === 1 ) {
							jQuery.cleanData( getAll( elem, false ) );
							elem.innerHTML = value;
						}
					}

					elem = 0;

				// If using innerHTML throws an exception, use the fallback method
				} catch ( e ) {}
			}

			if ( elem ) {
				this.empty().append( value );
			}
		}, null, value, arguments.length );
	},

	replaceWith: function() {
		var ignored = [];

		// Make the changes, replacing each non-ignored context element with the new content
		return domManip( this, arguments, function( elem ) {
			var parent = this.parentNode;

			if ( jQuery.inArray( this, ignored ) < 0 ) {
				jQuery.cleanData( getAll( this ) );
				if ( parent ) {
					parent.replaceChild( elem, this );
				}
			}

		// Force callback invocation
		}, ignored );
	}
} );

jQuery.each( {
	appendTo: "append",
	prependTo: "prepend",
	insertBefore: "before",
	insertAfter: "after",
	replaceAll: "replaceWith"
}, function( name, original ) {
	jQuery.fn[ name ] = function( selector ) {
		var elems,
			ret = [],
			insert = jQuery( selector ),
			last = insert.length - 1,
			i = 0;

		for ( ; i <= last; i++ ) {
			elems = i === last ? this : this.clone( true );
			jQuery( insert[ i ] )[ original ]( elems );

			// Support: Android <=4.0 only, PhantomJS 1 only
			// .get() because push.apply(_, arraylike) throws on ancient WebKit
			push.apply( ret, elems.get() );
		}

		return this.pushStack( ret );
	};
} );
var rmargin = ( /^margin/ );

var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" );

var getStyles = function( elem ) {

		// Support: IE <=11 only, Firefox <=30 (#15098, #14150)
		// IE throws on elements created in popups
		// FF meanwhile throws on frame elements through "defaultView.getComputedStyle"
		var view = elem.ownerDocument.defaultView;

		if ( !view || !view.opener ) {
			view = window;
		}

		return view.getComputedStyle( elem );
	};



( function() {

	// Executing both pixelPosition & boxSizingReliable tests require only one layout
	// so they're executed at the same time to save the second computation.
	function computeStyleTests() {

		// This is a singleton, we need to execute it only once
		if ( !div ) {
			return;
		}

		div.style.cssText =
			"box-sizing:border-box;" +
			"position:relative;display:block;" +
			"margin:auto;border:1px;padding:1px;" +
			"top:1%;width:50%";
		div.innerHTML = "";
		documentElement.appendChild( container );

		var divStyle = window.getComputedStyle( div );
		pixelPositionVal = divStyle.top !== "1%";

		// Support: Android 4.0 - 4.3 only, Firefox <=3 - 44
		reliableMarginLeftVal = divStyle.marginLeft === "2px";
		boxSizingReliableVal = divStyle.width === "4px";

		// Support: Android 4.0 - 4.3 only
		// Some styles come back with percentage values, even though they shouldn't
		div.style.marginRight = "50%";
		pixelMarginRightVal = divStyle.marginRight === "4px";

		documentElement.removeChild( container );

		// Nullify the div so it wouldn't be stored in the memory and
		// it will also be a sign that checks already performed
		div = null;
	}

	var pixelPositionVal, boxSizingReliableVal, pixelMarginRightVal, reliableMarginLeftVal,
		container = document.createElement( "div" ),
		div = document.createElement( "div" );

	// Finish early in limited (non-browser) environments
	if ( !div.style ) {
		return;
	}

	// Support: IE <=9 - 11 only
	// Style of cloned element affects source element cloned (#8908)
	div.style.backgroundClip = "content-box";
	div.cloneNode( true ).style.backgroundClip = "";
	support.clearCloneStyle = div.style.backgroundClip === "content-box";

	container.style.cssText = "border:0;width:8px;height:0;top:0;left:-9999px;" +
		"padding:0;margin-top:1px;position:absolute";
	container.appendChild( div );

	jQuery.extend( support, {
		pixelPosition: function() {
			computeStyleTests();
			return pixelPositionVal;
		},
		boxSizingReliable: function() {
			computeStyleTests();
			return boxSizingReliableVal;
		},
		pixelMarginRight: function() {
			computeStyleTests();
			return pixelMarginRightVal;
		},
		reliableMarginLeft: function() {
			computeStyleTests();
			return reliableMarginLeftVal;
		}
	} );
} )();


function curCSS( elem, name, computed ) {
	var width, minWidth, maxWidth, ret,
		style = elem.style;

	computed = computed || getStyles( elem );

	// getPropertyValue is needed for:
	//   .css('filter') (IE 9 only, #12537)
	//   .css('--customProperty) (#3144)
	if ( computed ) {
		ret = computed.getPropertyValue( name ) || computed[ name ];

		if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) {
			ret = jQuery.style( elem, name );
		}

		// A tribute to the "awesome hack by Dean Edwards"
		// Android Browser returns percentage for some values,
		// but width seems to be reliably pixels.
		// This is against the CSSOM draft spec:
		// https://drafts.csswg.org/cssom/#resolved-values
		if ( !support.pixelMarginRight() && rnumnonpx.test( ret ) && rmargin.test( name ) ) {

			// Remember the original values
			width = style.width;
			minWidth = style.minWidth;
			maxWidth = style.maxWidth;

			// Put in the new values to get a computed value out
			style.minWidth = style.maxWidth = style.width = ret;
			ret = computed.width;

			// Revert the changed values
			style.width = width;
			style.minWidth = minWidth;
			style.maxWidth = maxWidth;
		}
	}

	return ret !== undefined ?

		// Support: IE <=9 - 11 only
		// IE returns zIndex value as an integer.
		ret + "" :
		ret;
}


function addGetHookIf( conditionFn, hookFn ) {

	// Define the hook, we'll check on the first run if it's really needed.
	return {
		get: function() {
			if ( conditionFn() ) {

				// Hook not needed (or it's not possible to use it due
				// to missing dependency), remove it.
				delete this.get;
				return;
			}

			// Hook needed; redefine it so that the support test is not executed again.
			return ( this.get = hookFn ).apply( this, arguments );
		}
	};
}


var

	// Swappable if display is none or starts with table
	// except "table", "table-cell", or "table-caption"
	// See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
	rdisplayswap = /^(none|table(?!-c[ea]).+)/,
	rcustomProp = /^--/,
	cssShow = { position: "absolute", visibility: "hidden", display: "block" },
	cssNormalTransform = {
		letterSpacing: "0",
		fontWeight: "400"
	},

	cssPrefixes = [ "Webkit", "Moz", "ms" ],
	emptyStyle = document.createElement( "div" ).style;

// Return a css property mapped to a potentially vendor prefixed property
function vendorPropName( name ) {

	// Shortcut for names that are not vendor prefixed
	if ( name in emptyStyle ) {
		return name;
	}

	// Check for vendor prefixed names
	var capName = name[ 0 ].toUpperCase() + name.slice( 1 ),
		i = cssPrefixes.length;

	while ( i-- ) {
		name = cssPrefixes[ i ] + capName;
		if ( name in emptyStyle ) {
			return name;
		}
	}
}

// Return a property mapped along what jQuery.cssProps suggests or to
// a vendor prefixed property.
function finalPropName( name ) {
	var ret = jQuery.cssProps[ name ];
	if ( !ret ) {
		ret = jQuery.cssProps[ name ] = vendorPropName( name ) || name;
	}
	return ret;
}

function setPositiveNumber( elem, value, subtract ) {

	// Any relative (+/-) values have already been
	// normalized at this point
	var matches = rcssNum.exec( value );
	return matches ?

		// Guard against undefined "subtract", e.g., when used as in cssHooks
		Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) :
		value;
}

function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
	var i,
		val = 0;

	// If we already have the right measurement, avoid augmentation
	if ( extra === ( isBorderBox ? "border" : "content" ) ) {
		i = 4;

	// Otherwise initialize for horizontal or vertical properties
	} else {
		i = name === "width" ? 1 : 0;
	}

	for ( ; i < 4; i += 2 ) {

		// Both box models exclude margin, so add it if we want it
		if ( extra === "margin" ) {
			val += jQuery.css( elem, extra + cssExpand[ i ], true, styles );
		}

		if ( isBorderBox ) {

			// border-box includes padding, so remove it if we want content
			if ( extra === "content" ) {
				val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
			}

			// At this point, extra isn't border nor margin, so remove border
			if ( extra !== "margin" ) {
				val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
			}
		} else {

			// At this point, extra isn't content, so add padding
			val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );

			// At this point, extra isn't content nor padding, so add border
			if ( extra !== "padding" ) {
				val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
			}
		}
	}

	return val;
}

function getWidthOrHeight( elem, name, extra ) {

	// Start with computed style
	var valueIsBorderBox,
		styles = getStyles( elem ),
		val = curCSS( elem, name, styles ),
		isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box";

	// Computed unit is not pixels. Stop here and return.
	if ( rnumnonpx.test( val ) ) {
		return val;
	}

	// Check for style in case a browser which returns unreliable values
	// for getComputedStyle silently falls back to the reliable elem.style
	valueIsBorderBox = isBorderBox &&
		( support.boxSizingReliable() || val === elem.style[ name ] );

	// Normalize "", auto, and prepare for extra
	val = parseFloat( val ) || 0;

	// Use the active box-sizing model to add/subtract irrelevant styles
	return ( val +
		augmentWidthOrHeight(
			elem,
			name,
			extra || ( isBorderBox ? "border" : "content" ),
			valueIsBorderBox,
			styles
		)
	) + "px";
}

jQuery.extend( {

	// Add in style property hooks for overriding the default
	// behavior of getting and setting a style property
	cssHooks: {
		opacity: {
			get: function( elem, computed ) {
				if ( computed ) {

					// We should always get a number back from opacity
					var ret = curCSS( elem, "opacity" );
					return ret === "" ? "1" : ret;
				}
			}
		}
	},

	// Don't automatically add "px" to these possibly-unitless properties
	cssNumber: {
		"animationIterationCount": true,
		"columnCount": true,
		"fillOpacity": true,
		"flexGrow": true,
		"flexShrink": true,
		"fontWeight": true,
		"lineHeight": true,
		"opacity": true,
		"order": true,
		"orphans": true,
		"widows": true,
		"zIndex": true,
		"zoom": true
	},

	// Add in properties whose names you wish to fix before
	// setting or getting the value
	cssProps: {
		"float": "cssFloat"
	},

	// Get and set the style property on a DOM Node
	style: function( elem, name, value, extra ) {

		// Don't set styles on text and comment nodes
		if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
			return;
		}

		// Make sure that we're working with the right name
		var ret, type, hooks,
			origName = jQuery.camelCase( name ),
			isCustomProp = rcustomProp.test( name ),
			style = elem.style;

		// Make sure that we're working with the right name. We don't
		// want to query the value if it is a CSS custom property
		// since they are user-defined.
		if ( !isCustomProp ) {
			name = finalPropName( origName );
		}

		// Gets hook for the prefixed version, then unprefixed version
		hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];

		// Check if we're setting a value
		if ( value !== undefined ) {
			type = typeof value;

			// Convert "+=" or "-=" to relative numbers (#7345)
			if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {
				value = adjustCSS( elem, name, ret );

				// Fixes bug #9237
				type = "number";
			}

			// Make sure that null and NaN values aren't set (#7116)
			if ( value == null || value !== value ) {
				return;
			}

			// If a number was passed in, add the unit (except for certain CSS properties)
			if ( type === "number" ) {
				value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" );
			}

			// background-* props affect original clone's values
			if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) {
				style[ name ] = "inherit";
			}

			// If a hook was provided, use that value, otherwise just set the specified value
			if ( !hooks || !( "set" in hooks ) ||
				( value = hooks.set( elem, value, extra ) ) !== undefined ) {

				if ( isCustomProp ) {
					style.setProperty( name, value );
				} else {
					style[ name ] = value;
				}
			}

		} else {

			// If a hook was provided get the non-computed value from there
			if ( hooks && "get" in hooks &&
				( ret = hooks.get( elem, false, extra ) ) !== undefined ) {

				return ret;
			}

			// Otherwise just get the value from the style object
			return style[ name ];
		}
	},

	css: function( elem, name, extra, styles ) {
		var val, num, hooks,
			origName = jQuery.camelCase( name ),
			isCustomProp = rcustomProp.test( name );

		// Make sure that we're working with the right name. We don't
		// want to modify the value if it is a CSS custom property
		// since they are user-defined.
		if ( !isCustomProp ) {
			name = finalPropName( origName );
		}

		// Try prefixed name followed by the unprefixed name
		hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];

		// If a hook was provided get the computed value from there
		if ( hooks && "get" in hooks ) {
			val = hooks.get( elem, true, extra );
		}

		// Otherwise, if a way to get the computed value exists, use that
		if ( val === undefined ) {
			val = curCSS( elem, name, styles );
		}

		// Convert "normal" to computed value
		if ( val === "normal" && name in cssNormalTransform ) {
			val = cssNormalTransform[ name ];
		}

		// Make numeric if forced or a qualifier was provided and val looks numeric
		if ( extra === "" || extra ) {
			num = parseFloat( val );
			return extra === true || isFinite( num ) ? num || 0 : val;
		}

		return val;
	}
} );

jQuery.each( [ "height", "width" ], function( i, name ) {
	jQuery.cssHooks[ name ] = {
		get: function( elem, computed, extra ) {
			if ( computed ) {

				// Certain elements can have dimension info if we invisibly show them
				// but it must have a current display style that would benefit
				return rdisplayswap.test( jQuery.css( elem, "display" ) ) &&

					// Support: Safari 8+
					// Table columns in Safari have non-zero offsetWidth & zero
					// getBoundingClientRect().width unless display is changed.
					// Support: IE <=11 only
					// Running getBoundingClientRect on a disconnected node
					// in IE throws an error.
					( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ?
						swap( elem, cssShow, function() {
							return getWidthOrHeight( elem, name, extra );
						} ) :
						getWidthOrHeight( elem, name, extra );
			}
		},

		set: function( elem, value, extra ) {
			var matches,
				styles = extra && getStyles( elem ),
				subtract = extra && augmentWidthOrHeight(
					elem,
					name,
					extra,
					jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
					styles
				);

			// Convert to pixels if value adjustment is needed
			if ( subtract && ( matches = rcssNum.exec( value ) ) &&
				( matches[ 3 ] || "px" ) !== "px" ) {

				elem.style[ name ] = value;
				value = jQuery.css( elem, name );
			}

			return setPositiveNumber( elem, value, subtract );
		}
	};
} );

jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,
	function( elem, computed ) {
		if ( computed ) {
			return ( parseFloat( curCSS( elem, "marginLeft" ) ) ||
				elem.getBoundingClientRect().left -
					swap( elem, { marginLeft: 0 }, function() {
						return elem.getBoundingClientRect().left;
					} )
				) + "px";
		}
	}
);

// These hooks are used by animate to expand properties
jQuery.each( {
	margin: "",
	padding: "",
	border: "Width"
}, function( prefix, suffix ) {
	jQuery.cssHooks[ prefix + suffix ] = {
		expand: function( value ) {
			var i = 0,
				expanded = {},

				// Assumes a single number if not a string
				parts = typeof value === "string" ? value.split( " " ) : [ value ];

			for ( ; i < 4; i++ ) {
				expanded[ prefix + cssExpand[ i ] + suffix ] =
					parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
			}

			return expanded;
		}
	};

	if ( !rmargin.test( prefix ) ) {
		jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
	}
} );

jQuery.fn.extend( {
	css: function( name, value ) {
		return access( this, function( elem, name, value ) {
			var styles, len,
				map = {},
				i = 0;

			if ( Array.isArray( name ) ) {
				styles = getStyles( elem );
				len = name.length;

				for ( ; i < len; i++ ) {
					map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
				}

				return map;
			}

			return value !== undefined ?
				jQuery.style( elem, name, value ) :
				jQuery.css( elem, name );
		}, name, value, arguments.length > 1 );
	}
} );


function Tween( elem, options, prop, end, easing ) {
	return new Tween.prototype.init( elem, options, prop, end, easing );
}
jQuery.Tween = Tween;

Tween.prototype = {
	constructor: Tween,
	init: function( elem, options, prop, end, easing, unit ) {
		this.elem = elem;
		this.prop = prop;
		this.easing = easing || jQuery.easing._default;
		this.options = options;
		this.start = this.now = this.cur();
		this.end = end;
		this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
	},
	cur: function() {
		var hooks = Tween.propHooks[ this.prop ];

		return hooks && hooks.get ?
			hooks.get( this ) :
			Tween.propHooks._default.get( this );
	},
	run: function( percent ) {
		var eased,
			hooks = Tween.propHooks[ this.prop ];

		if ( this.options.duration ) {
			this.pos = eased = jQuery.easing[ this.easing ](
				percent, this.options.duration * percent, 0, 1, this.options.duration
			);
		} else {
			this.pos = eased = percent;
		}
		this.now = ( this.end - this.start ) * eased + this.start;

		if ( this.options.step ) {
			this.options.step.call( this.elem, this.now, this );
		}

		if ( hooks && hooks.set ) {
			hooks.set( this );
		} else {
			Tween.propHooks._default.set( this );
		}
		return this;
	}
};

Tween.prototype.init.prototype = Tween.prototype;

Tween.propHooks = {
	_default: {
		get: function( tween ) {
			var result;

			// Use a property on the element directly when it is not a DOM element,
			// or when there is no matching style property that exists.
			if ( tween.elem.nodeType !== 1 ||
				tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) {
				return tween.elem[ tween.prop ];
			}

			// Passing an empty string as a 3rd parameter to .css will automatically
			// attempt a parseFloat and fallback to a string if the parse fails.
			// Simple values such as "10px" are parsed to Float;
			// complex values such as "rotate(1rad)" are returned as-is.
			result = jQuery.css( tween.elem, tween.prop, "" );

			// Empty strings, null, undefined and "auto" are converted to 0.
			return !result || result === "auto" ? 0 : result;
		},
		set: function( tween ) {

			// Use step hook for back compat.
			// Use cssHook if its there.
			// Use .style if available and use plain properties where available.
			if ( jQuery.fx.step[ tween.prop ] ) {
				jQuery.fx.step[ tween.prop ]( tween );
			} else if ( tween.elem.nodeType === 1 &&
				( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null ||
					jQuery.cssHooks[ tween.prop ] ) ) {
				jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
			} else {
				tween.elem[ tween.prop ] = tween.now;
			}
		}
	}
};

// Support: IE <=9 only
// Panic based approach to setting things on disconnected nodes
Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
	set: function( tween ) {
		if ( tween.elem.nodeType && tween.elem.parentNode ) {
			tween.elem[ tween.prop ] = tween.now;
		}
	}
};

jQuery.easing = {
	linear: function( p ) {
		return p;
	},
	swing: function( p ) {
		return 0.5 - Math.cos( p * Math.PI ) / 2;
	},
	_default: "swing"
};

jQuery.fx = Tween.prototype.init;

// Back compat <1.8 extension point
jQuery.fx.step = {};




var
	fxNow, inProgress,
	rfxtypes = /^(?:toggle|show|hide)$/,
	rrun = /queueHooks$/;

function schedule() {
	if ( inProgress ) {
		if ( document.hidden === false && window.requestAnimationFrame ) {
			window.requestAnimationFrame( schedule );
		} else {
			window.setTimeout( schedule, jQuery.fx.interval );
		}

		jQuery.fx.tick();
	}
}

// Animations created synchronously will run synchronously
function createFxNow() {
	window.setTimeout( function() {
		fxNow = undefined;
	} );
	return ( fxNow = jQuery.now() );
}

// Generate parameters to create a standard animation
function genFx( type, includeWidth ) {
	var which,
		i = 0,
		attrs = { height: type };

	// If we include width, step value is 1 to do all cssExpand values,
	// otherwise step value is 2 to skip over Left and Right
	includeWidth = includeWidth ? 1 : 0;
	for ( ; i < 4; i += 2 - includeWidth ) {
		which = cssExpand[ i ];
		attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
	}

	if ( includeWidth ) {
		attrs.opacity = attrs.width = type;
	}

	return attrs;
}

function createTween( value, prop, animation ) {
	var tween,
		collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ),
		index = 0,
		length = collection.length;
	for ( ; index < length; index++ ) {
		if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) {

			// We're done with this property
			return tween;
		}
	}
}

function defaultPrefilter( elem, props, opts ) {
	var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display,
		isBox = "width" in props || "height" in props,
		anim = this,
		orig = {},
		style = elem.style,
		hidden = elem.nodeType && isHiddenWithinTree( elem ),
		dataShow = dataPriv.get( elem, "fxshow" );

	// Queue-skipping animations hijack the fx hooks
	if ( !opts.queue ) {
		hooks = jQuery._queueHooks( elem, "fx" );
		if ( hooks.unqueued == null ) {
			hooks.unqueued = 0;
			oldfire = hooks.empty.fire;
			hooks.empty.fire = function() {
				if ( !hooks.unqueued ) {
					oldfire();
				}
			};
		}
		hooks.unqueued++;

		anim.always( function() {

			// Ensure the complete handler is called before this completes
			anim.always( function() {
				hooks.unqueued--;
				if ( !jQuery.queue( elem, "fx" ).length ) {
					hooks.empty.fire();
				}
			} );
		} );
	}

	// Detect show/hide animations
	for ( prop in props ) {
		value = props[ prop ];
		if ( rfxtypes.test( value ) ) {
			delete props[ prop ];
			toggle = toggle || value === "toggle";
			if ( value === ( hidden ? "hide" : "show" ) ) {

				// Pretend to be hidden if this is a "show" and
				// there is still data from a stopped show/hide
				if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) {
					hidden = true;

				// Ignore all other no-op show/hide data
				} else {
					continue;
				}
			}
			orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );
		}
	}

	// Bail out if this is a no-op like .hide().hide()
	propTween = !jQuery.isEmptyObject( props );
	if ( !propTween && jQuery.isEmptyObject( orig ) ) {
		return;
	}

	// Restrict "overflow" and "display" styles during box animations
	if ( isBox && elem.nodeType === 1 ) {

		// Support: IE <=9 - 11, Edge 12 - 13
		// Record all 3 overflow attributes because IE does not infer the shorthand
		// from identically-valued overflowX and overflowY
		opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];

		// Identify a display type, preferring old show/hide data over the CSS cascade
		restoreDisplay = dataShow && dataShow.display;
		if ( restoreDisplay == null ) {
			restoreDisplay = dataPriv.get( elem, "display" );
		}
		display = jQuery.css( elem, "display" );
		if ( display === "none" ) {
			if ( restoreDisplay ) {
				display = restoreDisplay;
			} else {

				// Get nonempty value(s) by temporarily forcing visibility
				showHide( [ elem ], true );
				restoreDisplay = elem.style.display || restoreDisplay;
				display = jQuery.css( elem, "display" );
				showHide( [ elem ] );
			}
		}

		// Animate inline elements as inline-block
		if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) {
			if ( jQuery.css( elem, "float" ) === "none" ) {

				// Restore the original display value at the end of pure show/hide animations
				if ( !propTween ) {
					anim.done( function() {
						style.display = restoreDisplay;
					} );
					if ( restoreDisplay == null ) {
						display = style.display;
						restoreDisplay = display === "none" ? "" : display;
					}
				}
				style.display = "inline-block";
			}
		}
	}

	if ( opts.overflow ) {
		style.overflow = "hidden";
		anim.always( function() {
			style.overflow = opts.overflow[ 0 ];
			style.overflowX = opts.overflow[ 1 ];
			style.overflowY = opts.overflow[ 2 ];
		} );
	}

	// Implement show/hide animations
	propTween = false;
	for ( prop in orig ) {

		// General show/hide setup for this element animation
		if ( !propTween ) {
			if ( dataShow ) {
				if ( "hidden" in dataShow ) {
					hidden = dataShow.hidden;
				}
			} else {
				dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } );
			}

			// Store hidden/visible for toggle so `.stop().toggle()` "reverses"
			if ( toggle ) {
				dataShow.hidden = !hidden;
			}

			// Show elements before animating them
			if ( hidden ) {
				showHide( [ elem ], true );
			}

			/* eslint-disable no-loop-func */

			anim.done( function() {

			/* eslint-enable no-loop-func */

				// The final step of a "hide" animation is actually hiding the element
				if ( !hidden ) {
					showHide( [ elem ] );
				}
				dataPriv.remove( elem, "fxshow" );
				for ( prop in orig ) {
					jQuery.style( elem, prop, orig[ prop ] );
				}
			} );
		}

		// Per-property setup
		propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );
		if ( !( prop in dataShow ) ) {
			dataShow[ prop ] = propTween.start;
			if ( hidden ) {
				propTween.end = propTween.start;
				propTween.start = 0;
			}
		}
	}
}

function propFilter( props, specialEasing ) {
	var index, name, easing, value, hooks;

	// camelCase, specialEasing and expand cssHook pass
	for ( index in props ) {
		name = jQuery.camelCase( index );
		easing = specialEasing[ name ];
		value = props[ index ];
		if ( Array.isArray( value ) ) {
			easing = value[ 1 ];
			value = props[ index ] = value[ 0 ];
		}

		if ( index !== name ) {
			props[ name ] = value;
			delete props[ index ];
		}

		hooks = jQuery.cssHooks[ name ];
		if ( hooks && "expand" in hooks ) {
			value = hooks.expand( value );
			delete props[ name ];

			// Not quite $.extend, this won't overwrite existing keys.
			// Reusing 'index' because we have the correct "name"
			for ( index in value ) {
				if ( !( index in props ) ) {
					props[ index ] = value[ index ];
					specialEasing[ index ] = easing;
				}
			}
		} else {
			specialEasing[ name ] = easing;
		}
	}
}

function Animation( elem, properties, options ) {
	var result,
		stopped,
		index = 0,
		length = Animation.prefilters.length,
		deferred = jQuery.Deferred().always( function() {

			// Don't match elem in the :animated selector
			delete tick.elem;
		} ),
		tick = function() {
			if ( stopped ) {
				return false;
			}
			var currentTime = fxNow || createFxNow(),
				remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),

				// Support: Android 2.3 only
				// Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497)
				temp = remaining / animation.duration || 0,
				percent = 1 - temp,
				index = 0,
				length = animation.tweens.length;

			for ( ; index < length; index++ ) {
				animation.tweens[ index ].run( percent );
			}

			deferred.notifyWith( elem, [ animation, percent, remaining ] );

			// If there's more to do, yield
			if ( percent < 1 && length ) {
				return remaining;
			}

			// If this was an empty animation, synthesize a final progress notification
			if ( !length ) {
				deferred.notifyWith( elem, [ animation, 1, 0 ] );
			}

			// Resolve the animation and report its conclusion
			deferred.resolveWith( elem, [ animation ] );
			return false;
		},
		animation = deferred.promise( {
			elem: elem,
			props: jQuery.extend( {}, properties ),
			opts: jQuery.extend( true, {
				specialEasing: {},
				easing: jQuery.easing._default
			}, options ),
			originalProperties: properties,
			originalOptions: options,
			startTime: fxNow || createFxNow(),
			duration: options.duration,
			tweens: [],
			createTween: function( prop, end ) {
				var tween = jQuery.Tween( elem, animation.opts, prop, end,
						animation.opts.specialEasing[ prop ] || animation.opts.easing );
				animation.tweens.push( tween );
				return tween;
			},
			stop: function( gotoEnd ) {
				var index = 0,

					// If we are going to the end, we want to run all the tweens
					// otherwise we skip this part
					length = gotoEnd ? animation.tweens.length : 0;
				if ( stopped ) {
					return this;
				}
				stopped = true;
				for ( ; index < length; index++ ) {
					animation.tweens[ index ].run( 1 );
				}

				// Resolve when we played the last frame; otherwise, reject
				if ( gotoEnd ) {
					deferred.notifyWith( elem, [ animation, 1, 0 ] );
					deferred.resolveWith( elem, [ animation, gotoEnd ] );
				} else {
					deferred.rejectWith( elem, [ animation, gotoEnd ] );
				}
				return this;
			}
		} ),
		props = animation.props;

	propFilter( props, animation.opts.specialEasing );

	for ( ; index < length; index++ ) {
		result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts );
		if ( result ) {
			if ( jQuery.isFunction( result.stop ) ) {
				jQuery._queueHooks( animation.elem, animation.opts.queue ).stop =
					jQuery.proxy( result.stop, result );
			}
			return result;
		}
	}

	jQuery.map( props, createTween, animation );

	if ( jQuery.isFunction( animation.opts.start ) ) {
		animation.opts.start.call( elem, animation );
	}

	// Attach callbacks from options
	animation
		.progress( animation.opts.progress )
		.done( animation.opts.done, animation.opts.complete )
		.fail( animation.opts.fail )
		.always( animation.opts.always );

	jQuery.fx.timer(
		jQuery.extend( tick, {
			elem: elem,
			anim: animation,
			queue: animation.opts.queue
		} )
	);

	return animation;
}

jQuery.Animation = jQuery.extend( Animation, {

	tweeners: {
		"*": [ function( prop, value ) {
			var tween = this.createTween( prop, value );
			adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween );
			return tween;
		} ]
	},

	tweener: function( props, callback ) {
		if ( jQuery.isFunction( props ) ) {
			callback = props;
			props = [ "*" ];
		} else {
			props = props.match( rnothtmlwhite );
		}

		var prop,
			index = 0,
			length = props.length;

		for ( ; index < length; index++ ) {
			prop = props[ index ];
			Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || [];
			Animation.tweeners[ prop ].unshift( callback );
		}
	},

	prefilters: [ defaultPrefilter ],

	prefilter: function( callback, prepend ) {
		if ( prepend ) {
			Animation.prefilters.unshift( callback );
		} else {
			Animation.prefilters.push( callback );
		}
	}
} );

jQuery.speed = function( speed, easing, fn ) {
	var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
		complete: fn || !fn && easing ||
			jQuery.isFunction( speed ) && speed,
		duration: speed,
		easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
	};

	// Go to the end state if fx are off
	if ( jQuery.fx.off ) {
		opt.duration = 0;

	} else {
		if ( typeof opt.duration !== "number" ) {
			if ( opt.duration in jQuery.fx.speeds ) {
				opt.duration = jQuery.fx.speeds[ opt.duration ];

			} else {
				opt.duration = jQuery.fx.speeds._default;
			}
		}
	}

	// Normalize opt.queue - true/undefined/null -> "fx"
	if ( opt.queue == null || opt.queue === true ) {
		opt.queue = "fx";
	}

	// Queueing
	opt.old = opt.complete;

	opt.complete = function() {
		if ( jQuery.isFunction( opt.old ) ) {
			opt.old.call( this );
		}

		if ( opt.queue ) {
			jQuery.dequeue( this, opt.queue );
		}
	};

	return opt;
};

jQuery.fn.extend( {
	fadeTo: function( speed, to, easing, callback ) {

		// Show any hidden elements after setting opacity to 0
		return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show()

			// Animate to the value specified
			.end().animate( { opacity: to }, speed, easing, callback );
	},
	animate: function( prop, speed, easing, callback ) {
		var empty = jQuery.isEmptyObject( prop ),
			optall = jQuery.speed( speed, easing, callback ),
			doAnimation = function() {

				// Operate on a copy of prop so per-property easing won't be lost
				var anim = Animation( this, jQuery.extend( {}, prop ), optall );

				// Empty animations, or finishing resolves immediately
				if ( empty || dataPriv.get( this, "finish" ) ) {
					anim.stop( true );
				}
			};
			doAnimation.finish = doAnimation;

		return empty || optall.queue === false ?
			this.each( doAnimation ) :
			this.queue( optall.queue, doAnimation );
	},
	stop: function( type, clearQueue, gotoEnd ) {
		var stopQueue = function( hooks ) {
			var stop = hooks.stop;
			delete hooks.stop;
			stop( gotoEnd );
		};

		if ( typeof type !== "string" ) {
			gotoEnd = clearQueue;
			clearQueue = type;
			type = undefined;
		}
		if ( clearQueue && type !== false ) {
			this.queue( type || "fx", [] );
		}

		return this.each( function() {
			var dequeue = true,
				index = type != null && type + "queueHooks",
				timers = jQuery.timers,
				data = dataPriv.get( this );

			if ( index ) {
				if ( data[ index ] && data[ index ].stop ) {
					stopQueue( data[ index ] );
				}
			} else {
				for ( index in data ) {
					if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
						stopQueue( data[ index ] );
					}
				}
			}

			for ( index = timers.length; index--; ) {
				if ( timers[ index ].elem === this &&
					( type == null || timers[ index ].queue === type ) ) {

					timers[ index ].anim.stop( gotoEnd );
					dequeue = false;
					timers.splice( index, 1 );
				}
			}

			// Start the next in the queue if the last step wasn't forced.
			// Timers currently will call their complete callbacks, which
			// will dequeue but only if they were gotoEnd.
			if ( dequeue || !gotoEnd ) {
				jQuery.dequeue( this, type );
			}
		} );
	},
	finish: function( type ) {
		if ( type !== false ) {
			type = type || "fx";
		}
		return this.each( function() {
			var index,
				data = dataPriv.get( this ),
				queue = data[ type + "queue" ],
				hooks = data[ type + "queueHooks" ],
				timers = jQuery.timers,
				length = queue ? queue.length : 0;

			// Enable finishing flag on private data
			data.finish = true;

			// Empty the queue first
			jQuery.queue( this, type, [] );

			if ( hooks && hooks.stop ) {
				hooks.stop.call( this, true );
			}

			// Look for any active animations, and finish them
			for ( index = timers.length; index--; ) {
				if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
					timers[ index ].anim.stop( true );
					timers.splice( index, 1 );
				}
			}

			// Look for any animations in the old queue and finish them
			for ( index = 0; index < length; index++ ) {
				if ( queue[ index ] && queue[ index ].finish ) {
					queue[ index ].finish.call( this );
				}
			}

			// Turn off finishing flag
			delete data.finish;
		} );
	}
} );

jQuery.each( [ "toggle", "show", "hide" ], function( i, name ) {
	var cssFn = jQuery.fn[ name ];
	jQuery.fn[ name ] = function( speed, easing, callback ) {
		return speed == null || typeof speed === "boolean" ?
			cssFn.apply( this, arguments ) :
			this.animate( genFx( name, true ), speed, easing, callback );
	};
} );

// Generate shortcuts for custom animations
jQuery.each( {
	slideDown: genFx( "show" ),
	slideUp: genFx( "hide" ),
	slideToggle: genFx( "toggle" ),
	fadeIn: { opacity: "show" },
	fadeOut: { opacity: "hide" },
	fadeToggle: { opacity: "toggle" }
}, function( name, props ) {
	jQuery.fn[ name ] = function( speed, easing, callback ) {
		return this.animate( props, speed, easing, callback );
	};
} );

jQuery.timers = [];
jQuery.fx.tick = function() {
	var timer,
		i = 0,
		timers = jQuery.timers;

	fxNow = jQuery.now();

	for ( ; i < timers.length; i++ ) {
		timer = timers[ i ];

		// Run the timer and safely remove it when done (allowing for external removal)
		if ( !timer() && timers[ i ] === timer ) {
			timers.splice( i--, 1 );
		}
	}

	if ( !timers.length ) {
		jQuery.fx.stop();
	}
	fxNow = undefined;
};

jQuery.fx.timer = function( timer ) {
	jQuery.timers.push( timer );
	jQuery.fx.start();
};

jQuery.fx.interval = 13;
jQuery.fx.start = function() {
	if ( inProgress ) {
		return;
	}

	inProgress = true;
	schedule();
};

jQuery.fx.stop = function() {
	inProgress = null;
};

jQuery.fx.speeds = {
	slow: 600,
	fast: 200,

	// Default speed
	_default: 400
};


// Based off of the plugin by Clint Helfers, with permission.
// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/
jQuery.fn.delay = function( time, type ) {
	time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
	type = type || "fx";

	return this.queue( type, function( next, hooks ) {
		var timeout = window.setTimeout( next, time );
		hooks.stop = function() {
			window.clearTimeout( timeout );
		};
	} );
};


( function() {
	var input = document.createElement( "input" ),
		select = document.createElement( "select" ),
		opt = select.appendChild( document.createElement( "option" ) );

	input.type = "checkbox";

	// Support: Android <=4.3 only
	// Default value for a checkbox should be "on"
	support.checkOn = input.value !== "";

	// Support: IE <=11 only
	// Must access selectedIndex to make default options select
	support.optSelected = opt.selected;

	// Support: IE <=11 only
	// An input loses its value after becoming a radio
	input = document.createElement( "input" );
	input.value = "t";
	input.type = "radio";
	support.radioValue = input.value === "t";
} )();


var boolHook,
	attrHandle = jQuery.expr.attrHandle;

jQuery.fn.extend( {
	attr: function( name, value ) {
		return access( this, jQuery.attr, name, value, arguments.length > 1 );
	},

	removeAttr: function( name ) {
		return this.each( function() {
			jQuery.removeAttr( this, name );
		} );
	}
} );

jQuery.extend( {
	attr: function( elem, name, value ) {
		var ret, hooks,
			nType = elem.nodeType;

		// Don't get/set attributes on text, comment and attribute nodes
		if ( nType === 3 || nType === 8 || nType === 2 ) {
			return;
		}

		// Fallback to prop when attributes are not supported
		if ( typeof elem.getAttribute === "undefined" ) {
			return jQuery.prop( elem, name, value );
		}

		// Attribute hooks are determined by the lowercase version
		// Grab necessary hook if one is defined
		if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
			hooks = jQuery.attrHooks[ name.toLowerCase() ] ||
				( jQuery.expr.match.bool.test( name ) ? boolHook : undefined );
		}

		if ( value !== undefined ) {
			if ( value === null ) {
				jQuery.removeAttr( elem, name );
				return;
			}

			if ( hooks && "set" in hooks &&
				( ret = hooks.set( elem, value, name ) ) !== undefined ) {
				return ret;
			}

			elem.setAttribute( name, value + "" );
			return value;
		}

		if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {
			return ret;
		}

		ret = jQuery.find.attr( elem, name );

		// Non-existent attributes return null, we normalize to undefined
		return ret == null ? undefined : ret;
	},

	attrHooks: {
		type: {
			set: function( elem, value ) {
				if ( !support.radioValue && value === "radio" &&
					nodeName( elem, "input" ) ) {
					var val = elem.value;
					elem.setAttribute( "type", value );
					if ( val ) {
						elem.value = val;
					}
					return value;
				}
			}
		}
	},

	removeAttr: function( elem, value ) {
		var name,
			i = 0,

			// Attribute names can contain non-HTML whitespace characters
			// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
			attrNames = value && value.match( rnothtmlwhite );

		if ( attrNames && elem.nodeType === 1 ) {
			while ( ( name = attrNames[ i++ ] ) ) {
				elem.removeAttribute( name );
			}
		}
	}
} );

// Hooks for boolean attributes
boolHook = {
	set: function( elem, value, name ) {
		if ( value === false ) {

			// Remove boolean attributes when set to false
			jQuery.removeAttr( elem, name );
		} else {
			elem.setAttribute( name, name );
		}
		return name;
	}
};

jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) {
	var getter = attrHandle[ name ] || jQuery.find.attr;

	attrHandle[ name ] = function( elem, name, isXML ) {
		var ret, handle,
			lowercaseName = name.toLowerCase();

		if ( !isXML ) {

			// Avoid an infinite loop by temporarily removing this function from the getter
			handle = attrHandle[ lowercaseName ];
			attrHandle[ lowercaseName ] = ret;
			ret = getter( elem, name, isXML ) != null ?
				lowercaseName :
				null;
			attrHandle[ lowercaseName ] = handle;
		}
		return ret;
	};
} );




var rfocusable = /^(?:input|select|textarea|button)$/i,
	rclickable = /^(?:a|area)$/i;

jQuery.fn.extend( {
	prop: function( name, value ) {
		return access( this, jQuery.prop, name, value, arguments.length > 1 );
	},

	removeProp: function( name ) {
		return this.each( function() {
			delete this[ jQuery.propFix[ name ] || name ];
		} );
	}
} );

jQuery.extend( {
	prop: function( elem, name, value ) {
		var ret, hooks,
			nType = elem.nodeType;

		// Don't get/set properties on text, comment and attribute nodes
		if ( nType === 3 || nType === 8 || nType === 2 ) {
			return;
		}

		if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {

			// Fix name and attach hooks
			name = jQuery.propFix[ name ] || name;
			hooks = jQuery.propHooks[ name ];
		}

		if ( value !== undefined ) {
			if ( hooks && "set" in hooks &&
				( ret = hooks.set( elem, value, name ) ) !== undefined ) {
				return ret;
			}

			return ( elem[ name ] = value );
		}

		if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {
			return ret;
		}

		return elem[ name ];
	},

	propHooks: {
		tabIndex: {
			get: function( elem ) {

				// Support: IE <=9 - 11 only
				// elem.tabIndex doesn't always return the
				// correct value when it hasn't been explicitly set
				// https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
				// Use proper attribute retrieval(#12072)
				var tabindex = jQuery.find.attr( elem, "tabindex" );

				if ( tabindex ) {
					return parseInt( tabindex, 10 );
				}

				if (
					rfocusable.test( elem.nodeName ) ||
					rclickable.test( elem.nodeName ) &&
					elem.href
				) {
					return 0;
				}

				return -1;
			}
		}
	},

	propFix: {
		"for": "htmlFor",
		"class": "className"
	}
} );

// Support: IE <=11 only
// Accessing the selectedIndex property
// forces the browser to respect setting selected
// on the option
// The getter ensures a default option is selected
// when in an optgroup
// eslint rule "no-unused-expressions" is disabled for this code
// since it considers such accessions noop
if ( !support.optSelected ) {
	jQuery.propHooks.selected = {
		get: function( elem ) {

			/* eslint no-unused-expressions: "off" */

			var parent = elem.parentNode;
			if ( parent && parent.parentNode ) {
				parent.parentNode.selectedIndex;
			}
			return null;
		},
		set: function( elem ) {

			/* eslint no-unused-expressions: "off" */

			var parent = elem.parentNode;
			if ( parent ) {
				parent.selectedIndex;

				if ( parent.parentNode ) {
					parent.parentNode.selectedIndex;
				}
			}
		}
	};
}

jQuery.each( [
	"tabIndex",
	"readOnly",
	"maxLength",
	"cellSpacing",
	"cellPadding",
	"rowSpan",
	"colSpan",
	"useMap",
	"frameBorder",
	"contentEditable"
], function() {
	jQuery.propFix[ this.toLowerCase() ] = this;
} );




	// Strip and collapse whitespace according to HTML spec
	// https://html.spec.whatwg.org/multipage/infrastructure.html#strip-and-collapse-whitespace
	function stripAndCollapse( value ) {
		var tokens = value.match( rnothtmlwhite ) || [];
		return tokens.join( " " );
	}


function getClass( elem ) {
	return elem.getAttribute && elem.getAttribute( "class" ) || "";
}

jQuery.fn.extend( {
	addClass: function( value ) {
		var classes, elem, cur, curValue, clazz, j, finalValue,
			i = 0;

		if ( jQuery.isFunction( value ) ) {
			return this.each( function( j ) {
				jQuery( this ).addClass( value.call( this, j, getClass( this ) ) );
			} );
		}

		if ( typeof value === "string" && value ) {
			classes = value.match( rnothtmlwhite ) || [];

			while ( ( elem = this[ i++ ] ) ) {
				curValue = getClass( elem );
				cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );

				if ( cur ) {
					j = 0;
					while ( ( clazz = classes[ j++ ] ) ) {
						if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
							cur += clazz + " ";
						}
					}

					// Only assign if different to avoid unneeded rendering.
					finalValue = stripAndCollapse( cur );
					if ( curValue !== finalValue ) {
						elem.setAttribute( "class", finalValue );
					}
				}
			}
		}

		return this;
	},

	removeClass: function( value ) {
		var classes, elem, cur, curValue, clazz, j, finalValue,
			i = 0;

		if ( jQuery.isFunction( value ) ) {
			return this.each( function( j ) {
				jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) );
			} );
		}

		if ( !arguments.length ) {
			return this.attr( "class", "" );
		}

		if ( typeof value === "string" && value ) {
			classes = value.match( rnothtmlwhite ) || [];

			while ( ( elem = this[ i++ ] ) ) {
				curValue = getClass( elem );

				// This expression is here for better compressibility (see addClass)
				cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );

				if ( cur ) {
					j = 0;
					while ( ( clazz = classes[ j++ ] ) ) {

						// Remove *all* instances
						while ( cur.indexOf( " " + clazz + " " ) > -1 ) {
							cur = cur.replace( " " + clazz + " ", " " );
						}
					}

					// Only assign if different to avoid unneeded rendering.
					finalValue = stripAndCollapse( cur );
					if ( curValue !== finalValue ) {
						elem.setAttribute( "class", finalValue );
					}
				}
			}
		}

		return this;
	},

	toggleClass: function( value, stateVal ) {
		var type = typeof value;

		if ( typeof stateVal === "boolean" && type === "string" ) {
			return stateVal ? this.addClass( value ) : this.removeClass( value );
		}

		if ( jQuery.isFunction( value ) ) {
			return this.each( function( i ) {
				jQuery( this ).toggleClass(
					value.call( this, i, getClass( this ), stateVal ),
					stateVal
				);
			} );
		}

		return this.each( function() {
			var className, i, self, classNames;

			if ( type === "string" ) {

				// Toggle individual class names
				i = 0;
				self = jQuery( this );
				classNames = value.match( rnothtmlwhite ) || [];

				while ( ( className = classNames[ i++ ] ) ) {

					// Check each className given, space separated list
					if ( self.hasClass( className ) ) {
						self.removeClass( className );
					} else {
						self.addClass( className );
					}
				}

			// Toggle whole class name
			} else if ( value === undefined || type === "boolean" ) {
				className = getClass( this );
				if ( className ) {

					// Store className if set
					dataPriv.set( this, "__className__", className );
				}

				// If the element has a class name or if we're passed `false`,
				// then remove the whole classname (if there was one, the above saved it).
				// Otherwise bring back whatever was previously saved (if anything),
				// falling back to the empty string if nothing was stored.
				if ( this.setAttribute ) {
					this.setAttribute( "class",
						className || value === false ?
						"" :
						dataPriv.get( this, "__className__" ) || ""
					);
				}
			}
		} );
	},

	hasClass: function( selector ) {
		var className, elem,
			i = 0;

		className = " " + selector + " ";
		while ( ( elem = this[ i++ ] ) ) {
			if ( elem.nodeType === 1 &&
				( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) {
					return true;
			}
		}

		return false;
	}
} );




var rreturn = /\r/g;

jQuery.fn.extend( {
	val: function( value ) {
		var hooks, ret, isFunction,
			elem = this[ 0 ];

		if ( !arguments.length ) {
			if ( elem ) {
				hooks = jQuery.valHooks[ elem.type ] ||
					jQuery.valHooks[ elem.nodeName.toLowerCase() ];

				if ( hooks &&
					"get" in hooks &&
					( ret = hooks.get( elem, "value" ) ) !== undefined
				) {
					return ret;
				}

				ret = elem.value;

				// Handle most common string cases
				if ( typeof ret === "string" ) {
					return ret.replace( rreturn, "" );
				}

				// Handle cases where value is null/undef or number
				return ret == null ? "" : ret;
			}

			return;
		}

		isFunction = jQuery.isFunction( value );

		return this.each( function( i ) {
			var val;

			if ( this.nodeType !== 1 ) {
				return;
			}

			if ( isFunction ) {
				val = value.call( this, i, jQuery( this ).val() );
			} else {
				val = value;
			}

			// Treat null/undefined as ""; convert numbers to string
			if ( val == null ) {
				val = "";

			} else if ( typeof val === "number" ) {
				val += "";

			} else if ( Array.isArray( val ) ) {
				val = jQuery.map( val, function( value ) {
					return value == null ? "" : value + "";
				} );
			}

			hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];

			// If set returns undefined, fall back to normal setting
			if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) {
				this.value = val;
			}
		} );
	}
} );

jQuery.extend( {
	valHooks: {
		option: {
			get: function( elem ) {

				var val = jQuery.find.attr( elem, "value" );
				return val != null ?
					val :

					// Support: IE <=10 - 11 only
					// option.text throws exceptions (#14686, #14858)
					// Strip and collapse whitespace
					// https://html.spec.whatwg.org/#strip-and-collapse-whitespace
					stripAndCollapse( jQuery.text( elem ) );
			}
		},
		select: {
			get: function( elem ) {
				var value, option, i,
					options = elem.options,
					index = elem.selectedIndex,
					one = elem.type === "select-one",
					values = one ? null : [],
					max = one ? index + 1 : options.length;

				if ( index < 0 ) {
					i = max;

				} else {
					i = one ? index : 0;
				}

				// Loop through all the selected options
				for ( ; i < max; i++ ) {
					option = options[ i ];

					// Support: IE <=9 only
					// IE8-9 doesn't update selected after form reset (#2551)
					if ( ( option.selected || i === index ) &&

							// Don't return options that are disabled or in a disabled optgroup
							!option.disabled &&
							( !option.parentNode.disabled ||
								!nodeName( option.parentNode, "optgroup" ) ) ) {

						// Get the specific value for the option
						value = jQuery( option ).val();

						// We don't need an array for one selects
						if ( one ) {
							return value;
						}

						// Multi-Selects return an array
						values.push( value );
					}
				}

				return values;
			},

			set: function( elem, value ) {
				var optionSet, option,
					options = elem.options,
					values = jQuery.makeArray( value ),
					i = options.length;

				while ( i-- ) {
					option = options[ i ];

					/* eslint-disable no-cond-assign */

					if ( option.selected =
						jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1
					) {
						optionSet = true;
					}

					/* eslint-enable no-cond-assign */
				}

				// Force browsers to behave consistently when non-matching value is set
				if ( !optionSet ) {
					elem.selectedIndex = -1;
				}
				return values;
			}
		}
	}
} );

// Radios and checkboxes getter/setter
jQuery.each( [ "radio", "checkbox" ], function() {
	jQuery.valHooks[ this ] = {
		set: function( elem, value ) {
			if ( Array.isArray( value ) ) {
				return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 );
			}
		}
	};
	if ( !support.checkOn ) {
		jQuery.valHooks[ this ].get = function( elem ) {
			return elem.getAttribute( "value" ) === null ? "on" : elem.value;
		};
	}
} );




// Return jQuery for attributes-only inclusion


var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/;

jQuery.extend( jQuery.event, {

	trigger: function( event, data, elem, onlyHandlers ) {

		var i, cur, tmp, bubbleType, ontype, handle, special,
			eventPath = [ elem || document ],
			type = hasOwn.call( event, "type" ) ? event.type : event,
			namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : [];

		cur = tmp = elem = elem || document;

		// Don't do events on text and comment nodes
		if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
			return;
		}

		// focus/blur morphs to focusin/out; ensure we're not firing them right now
		if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
			return;
		}

		if ( type.indexOf( "." ) > -1 ) {

			// Namespaced trigger; create a regexp to match event type in handle()
			namespaces = type.split( "." );
			type = namespaces.shift();
			namespaces.sort();
		}
		ontype = type.indexOf( ":" ) < 0 && "on" + type;

		// Caller can pass in a jQuery.Event object, Object, or just an event type string
		event = event[ jQuery.expando ] ?
			event :
			new jQuery.Event( type, typeof event === "object" && event );

		// Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)
		event.isTrigger = onlyHandlers ? 2 : 3;
		event.namespace = namespaces.join( "." );
		event.rnamespace = event.namespace ?
			new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) :
			null;

		// Clean up the event in case it is being reused
		event.result = undefined;
		if ( !event.target ) {
			event.target = elem;
		}

		// Clone any incoming data and prepend the event, creating the handler arg list
		data = data == null ?
			[ event ] :
			jQuery.makeArray( data, [ event ] );

		// Allow special events to draw outside the lines
		special = jQuery.event.special[ type ] || {};
		if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {
			return;
		}

		// Determine event propagation path in advance, per W3C events spec (#9951)
		// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
		if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {

			bubbleType = special.delegateType || type;
			if ( !rfocusMorph.test( bubbleType + type ) ) {
				cur = cur.parentNode;
			}
			for ( ; cur; cur = cur.parentNode ) {
				eventPath.push( cur );
				tmp = cur;
			}

			// Only add window if we got to document (e.g., not plain obj or detached DOM)
			if ( tmp === ( elem.ownerDocument || document ) ) {
				eventPath.push( tmp.defaultView || tmp.parentWindow || window );
			}
		}

		// Fire handlers on the event path
		i = 0;
		while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) {

			event.type = i > 1 ?
				bubbleType :
				special.bindType || type;

			// jQuery handler
			handle = ( dataPriv.get( cur, "events" ) || {} )[ event.type ] &&
				dataPriv.get( cur, "handle" );
			if ( handle ) {
				handle.apply( cur, data );
			}

			// Native handler
			handle = ontype && cur[ ontype ];
			if ( handle && handle.apply && acceptData( cur ) ) {
				event.result = handle.apply( cur, data );
				if ( event.result === false ) {
					event.preventDefault();
				}
			}
		}
		event.type = type;

		// If nobody prevented the default action, do it now
		if ( !onlyHandlers && !event.isDefaultPrevented() ) {

			if ( ( !special._default ||
				special._default.apply( eventPath.pop(), data ) === false ) &&
				acceptData( elem ) ) {

				// Call a native DOM method on the target with the same name as the event.
				// Don't do default actions on window, that's where global variables be (#6170)
				if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) {

					// Don't re-trigger an onFOO event when we call its FOO() method
					tmp = elem[ ontype ];

					if ( tmp ) {
						elem[ ontype ] = null;
					}

					// Prevent re-triggering of the same event, since we already bubbled it above
					jQuery.event.triggered = type;
					elem[ type ]();
					jQuery.event.triggered = undefined;

					if ( tmp ) {
						elem[ ontype ] = tmp;
					}
				}
			}
		}

		return event.result;
	},

	// Piggyback on a donor event to simulate a different one
	// Used only for `focus(in | out)` events
	simulate: function( type, elem, event ) {
		var e = jQuery.extend(
			new jQuery.Event(),
			event,
			{
				type: type,
				isSimulated: true
			}
		);

		jQuery.event.trigger( e, null, elem );
	}

} );

jQuery.fn.extend( {

	trigger: function( type, data ) {
		return this.each( function() {
			jQuery.event.trigger( type, data, this );
		} );
	},
	triggerHandler: function( type, data ) {
		var elem = this[ 0 ];
		if ( elem ) {
			return jQuery.event.trigger( type, data, elem, true );
		}
	}
} );


jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " +
	"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
	"change select submit keydown keypress keyup contextmenu" ).split( " " ),
	function( i, name ) {

	// Handle event binding
	jQuery.fn[ name ] = function( data, fn ) {
		return arguments.length > 0 ?
			this.on( name, null, data, fn ) :
			this.trigger( name );
	};
} );

jQuery.fn.extend( {
	hover: function( fnOver, fnOut ) {
		return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
	}
} );




support.focusin = "onfocusin" in window;


// Support: Firefox <=44
// Firefox doesn't have focus(in | out) events
// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787
//
// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1
// focus(in | out) events fire after focus & blur events,
// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order
// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857
if ( !support.focusin ) {
	jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) {

		// Attach a single capturing handler on the document while someone wants focusin/focusout
		var handler = function( event ) {
			jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) );
		};

		jQuery.event.special[ fix ] = {
			setup: function() {
				var doc = this.ownerDocument || this,
					attaches = dataPriv.access( doc, fix );

				if ( !attaches ) {
					doc.addEventListener( orig, handler, true );
				}
				dataPriv.access( doc, fix, ( attaches || 0 ) + 1 );
			},
			teardown: function() {
				var doc = this.ownerDocument || this,
					attaches = dataPriv.access( doc, fix ) - 1;

				if ( !attaches ) {
					doc.removeEventListener( orig, handler, true );
					dataPriv.remove( doc, fix );

				} else {
					dataPriv.access( doc, fix, attaches );
				}
			}
		};
	} );
}
var location = window.location;

var nonce = jQuery.now();

var rquery = ( /\?/ );



// Cross-browser xml parsing
jQuery.parseXML = function( data ) {
	var xml;
	if ( !data || typeof data !== "string" ) {
		return null;
	}

	// Support: IE 9 - 11 only
	// IE throws on parseFromString with invalid input.
	try {
		xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" );
	} catch ( e ) {
		xml = undefined;
	}

	if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) {
		jQuery.error( "Invalid XML: " + data );
	}
	return xml;
};


var
	rbracket = /\[\]$/,
	rCRLF = /\r?\n/g,
	rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,
	rsubmittable = /^(?:input|select|textarea|keygen)/i;

function buildParams( prefix, obj, traditional, add ) {
	var name;

	if ( Array.isArray( obj ) ) {

		// Serialize array item.
		jQuery.each( obj, function( i, v ) {
			if ( traditional || rbracket.test( prefix ) ) {

				// Treat each array item as a scalar.
				add( prefix, v );

			} else {

				// Item is non-scalar (array or object), encode its numeric index.
				buildParams(
					prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]",
					v,
					traditional,
					add
				);
			}
		} );

	} else if ( !traditional && jQuery.type( obj ) === "object" ) {

		// Serialize object item.
		for ( name in obj ) {
			buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
		}

	} else {

		// Serialize scalar item.
		add( prefix, obj );
	}
}

// Serialize an array of form elements or a set of
// key/values into a query string
jQuery.param = function( a, traditional ) {
	var prefix,
		s = [],
		add = function( key, valueOrFunction ) {

			// If value is a function, invoke it and use its return value
			var value = jQuery.isFunction( valueOrFunction ) ?
				valueOrFunction() :
				valueOrFunction;

			s[ s.length ] = encodeURIComponent( key ) + "=" +
				encodeURIComponent( value == null ? "" : value );
		};

	// If an array was passed in, assume that it is an array of form elements.
	if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {

		// Serialize the form elements
		jQuery.each( a, function() {
			add( this.name, this.value );
		} );

	} else {

		// If traditional, encode the "old" way (the way 1.3.2 or older
		// did it), otherwise encode params recursively.
		for ( prefix in a ) {
			buildParams( prefix, a[ prefix ], traditional, add );
		}
	}

	// Return the resulting serialization
	return s.join( "&" );
};

jQuery.fn.extend( {
	serialize: function() {
		return jQuery.param( this.serializeArray() );
	},
	serializeArray: function() {
		return this.map( function() {

			// Can add propHook for "elements" to filter or add form elements
			var elements = jQuery.prop( this, "elements" );
			return elements ? jQuery.makeArray( elements ) : this;
		} )
		.filter( function() {
			var type = this.type;

			// Use .is( ":disabled" ) so that fieldset[disabled] works
			return this.name && !jQuery( this ).is( ":disabled" ) &&
				rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
				( this.checked || !rcheckableType.test( type ) );
		} )
		.map( function( i, elem ) {
			var val = jQuery( this ).val();

			if ( val == null ) {
				return null;
			}

			if ( Array.isArray( val ) ) {
				return jQuery.map( val, function( val ) {
					return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
				} );
			}

			return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
		} ).get();
	}
} );


var
	r20 = /%20/g,
	rhash = /#.*$/,
	rantiCache = /([?&])_=[^&]*/,
	rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg,

	// #7653, #8125, #8152: local protocol detection
	rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,
	rnoContent = /^(?:GET|HEAD)$/,
	rprotocol = /^\/\//,

	/* Prefilters
	 * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
	 * 2) These are called:
	 *    - BEFORE asking for a transport
	 *    - AFTER param serialization (s.data is a string if s.processData is true)
	 * 3) key is the dataType
	 * 4) the catchall symbol "*" can be used
	 * 5) execution will start with transport dataType and THEN continue down to "*" if needed
	 */
	prefilters = {},

	/* Transports bindings
	 * 1) key is the dataType
	 * 2) the catchall symbol "*" can be used
	 * 3) selection will start with transport dataType and THEN go to "*" if needed
	 */
	transports = {},

	// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
	allTypes = "*/".concat( "*" ),

	// Anchor tag for parsing the document origin
	originAnchor = document.createElement( "a" );
	originAnchor.href = location.href;

// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
function addToPrefiltersOrTransports( structure ) {

	// dataTypeExpression is optional and defaults to "*"
	return function( dataTypeExpression, func ) {

		if ( typeof dataTypeExpression !== "string" ) {
			func = dataTypeExpression;
			dataTypeExpression = "*";
		}

		var dataType,
			i = 0,
			dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || [];

		if ( jQuery.isFunction( func ) ) {

			// For each dataType in the dataTypeExpression
			while ( ( dataType = dataTypes[ i++ ] ) ) {

				// Prepend if requested
				if ( dataType[ 0 ] === "+" ) {
					dataType = dataType.slice( 1 ) || "*";
					( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func );

				// Otherwise append
				} else {
					( structure[ dataType ] = structure[ dataType ] || [] ).push( func );
				}
			}
		}
	};
}

// Base inspection function for prefilters and transports
function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {

	var inspected = {},
		seekingTransport = ( structure === transports );

	function inspect( dataType ) {
		var selected;
		inspected[ dataType ] = true;
		jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
			var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );
			if ( typeof dataTypeOrTransport === "string" &&
				!seekingTransport && !inspected[ dataTypeOrTransport ] ) {

				options.dataTypes.unshift( dataTypeOrTransport );
				inspect( dataTypeOrTransport );
				return false;
			} else if ( seekingTransport ) {
				return !( selected = dataTypeOrTransport );
			}
		} );
		return selected;
	}

	return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
}

// A special extend for ajax options
// that takes "flat" options (not to be deep extended)
// Fixes #9887
function ajaxExtend( target, src ) {
	var key, deep,
		flatOptions = jQuery.ajaxSettings.flatOptions || {};

	for ( key in src ) {
		if ( src[ key ] !== undefined ) {
			( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];
		}
	}
	if ( deep ) {
		jQuery.extend( true, target, deep );
	}

	return target;
}

/* Handles responses to an ajax request:
 * - finds the right dataType (mediates between content-type and expected dataType)
 * - returns the corresponding response
 */
function ajaxHandleResponses( s, jqXHR, responses ) {

	var ct, type, finalDataType, firstDataType,
		contents = s.contents,
		dataTypes = s.dataTypes;

	// Remove auto dataType and get content-type in the process
	while ( dataTypes[ 0 ] === "*" ) {
		dataTypes.shift();
		if ( ct === undefined ) {
			ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" );
		}
	}

	// Check if we're dealing with a known content-type
	if ( ct ) {
		for ( type in contents ) {
			if ( contents[ type ] && contents[ type ].test( ct ) ) {
				dataTypes.unshift( type );
				break;
			}
		}
	}

	// Check to see if we have a response for the expected dataType
	if ( dataTypes[ 0 ] in responses ) {
		finalDataType = dataTypes[ 0 ];
	} else {

		// Try convertible dataTypes
		for ( type in responses ) {
			if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) {
				finalDataType = type;
				break;
			}
			if ( !firstDataType ) {
				firstDataType = type;
			}
		}

		// Or just use first one
		finalDataType = finalDataType || firstDataType;
	}

	// If we found a dataType
	// We add the dataType to the list if needed
	// and return the corresponding response
	if ( finalDataType ) {
		if ( finalDataType !== dataTypes[ 0 ] ) {
			dataTypes.unshift( finalDataType );
		}
		return responses[ finalDataType ];
	}
}

/* Chain conversions given the request and the original response
 * Also sets the responseXXX fields on the jqXHR instance
 */
function ajaxConvert( s, response, jqXHR, isSuccess ) {
	var conv2, current, conv, tmp, prev,
		converters = {},

		// Work with a copy of dataTypes in case we need to modify it for conversion
		dataTypes = s.dataTypes.slice();

	// Create converters map with lowercased keys
	if ( dataTypes[ 1 ] ) {
		for ( conv in s.converters ) {
			converters[ conv.toLowerCase() ] = s.converters[ conv ];
		}
	}

	current = dataTypes.shift();

	// Convert to each sequential dataType
	while ( current ) {

		if ( s.responseFields[ current ] ) {
			jqXHR[ s.responseFields[ current ] ] = response;
		}

		// Apply the dataFilter if provided
		if ( !prev && isSuccess && s.dataFilter ) {
			response = s.dataFilter( response, s.dataType );
		}

		prev = current;
		current = dataTypes.shift();

		if ( current ) {

			// There's only work to do if current dataType is non-auto
			if ( current === "*" ) {

				current = prev;

			// Convert response if prev dataType is non-auto and differs from current
			} else if ( prev !== "*" && prev !== current ) {

				// Seek a direct converter
				conv = converters[ prev + " " + current ] || converters[ "* " + current ];

				// If none found, seek a pair
				if ( !conv ) {
					for ( conv2 in converters ) {

						// If conv2 outputs current
						tmp = conv2.split( " " );
						if ( tmp[ 1 ] === current ) {

							// If prev can be converted to accepted input
							conv = converters[ prev + " " + tmp[ 0 ] ] ||
								converters[ "* " + tmp[ 0 ] ];
							if ( conv ) {

								// Condense equivalence converters
								if ( conv === true ) {
									conv = converters[ conv2 ];

								// Otherwise, insert the intermediate dataType
								} else if ( converters[ conv2 ] !== true ) {
									current = tmp[ 0 ];
									dataTypes.unshift( tmp[ 1 ] );
								}
								break;
							}
						}
					}
				}

				// Apply converter (if not an equivalence)
				if ( conv !== true ) {

					// Unless errors are allowed to bubble, catch and return them
					if ( conv && s.throws ) {
						response = conv( response );
					} else {
						try {
							response = conv( response );
						} catch ( e ) {
							return {
								state: "parsererror",
								error: conv ? e : "No conversion from " + prev + " to " + current
							};
						}
					}
				}
			}
		}
	}

	return { state: "success", data: response };
}

jQuery.extend( {

	// Counter for holding the number of active queries
	active: 0,

	// Last-Modified header cache for next request
	lastModified: {},
	etag: {},

	ajaxSettings: {
		url: location.href,
		type: "GET",
		isLocal: rlocalProtocol.test( location.protocol ),
		global: true,
		processData: true,
		async: true,
		contentType: "application/x-www-form-urlencoded; charset=UTF-8",

		/*
		timeout: 0,
		data: null,
		dataType: null,
		username: null,
		password: null,
		cache: null,
		throws: false,
		traditional: false,
		headers: {},
		*/

		accepts: {
			"*": allTypes,
			text: "text/plain",
			html: "text/html",
			xml: "application/xml, text/xml",
			json: "application/json, text/javascript"
		},

		contents: {
			xml: /\bxml\b/,
			html: /\bhtml/,
			json: /\bjson\b/
		},

		responseFields: {
			xml: "responseXML",
			text: "responseText",
			json: "responseJSON"
		},

		// Data converters
		// Keys separate source (or catchall "*") and destination types with a single space
		converters: {

			// Convert anything to text
			"* text": String,

			// Text to html (true = no transformation)
			"text html": true,

			// Evaluate text as a json expression
			"text json": JSON.parse,

			// Parse text as xml
			"text xml": jQuery.parseXML
		},

		// For options that shouldn't be deep extended:
		// you can add your own custom options here if
		// and when you create one that shouldn't be
		// deep extended (see ajaxExtend)
		flatOptions: {
			url: true,
			context: true
		}
	},

	// Creates a full fledged settings object into target
	// with both ajaxSettings and settings fields.
	// If target is omitted, writes into ajaxSettings.
	ajaxSetup: function( target, settings ) {
		return settings ?

			// Building a settings object
			ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :

			// Extending ajaxSettings
			ajaxExtend( jQuery.ajaxSettings, target );
	},

	ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
	ajaxTransport: addToPrefiltersOrTransports( transports ),

	// Main method
	ajax: function( url, options ) {

		// If url is an object, simulate pre-1.5 signature
		if ( typeof url === "object" ) {
			options = url;
			url = undefined;
		}

		// Force options to be an object
		options = options || {};

		var transport,

			// URL without anti-cache param
			cacheURL,

			// Response headers
			responseHeadersString,
			responseHeaders,

			// timeout handle
			timeoutTimer,

			// Url cleanup var
			urlAnchor,

			// Request state (becomes false upon send and true upon completion)
			completed,

			// To know if global events are to be dispatched
			fireGlobals,

			// Loop variable
			i,

			// uncached part of the url
			uncached,

			// Create the final options object
			s = jQuery.ajaxSetup( {}, options ),

			// Callbacks context
			callbackContext = s.context || s,

			// Context for global events is callbackContext if it is a DOM node or jQuery collection
			globalEventContext = s.context &&
				( callbackContext.nodeType || callbackContext.jquery ) ?
					jQuery( callbackContext ) :
					jQuery.event,

			// Deferreds
			deferred = jQuery.Deferred(),
			completeDeferred = jQuery.Callbacks( "once memory" ),

			// Status-dependent callbacks
			statusCode = s.statusCode || {},

			// Headers (they are sent all at once)
			requestHeaders = {},
			requestHeadersNames = {},

			// Default abort message
			strAbort = "canceled",

			// Fake xhr
			jqXHR = {
				readyState: 0,

				// Builds headers hashtable if needed
				getResponseHeader: function( key ) {
					var match;
					if ( completed ) {
						if ( !responseHeaders ) {
							responseHeaders = {};
							while ( ( match = rheaders.exec( responseHeadersString ) ) ) {
								responseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ];
							}
						}
						match = responseHeaders[ key.toLowerCase() ];
					}
					return match == null ? null : match;
				},

				// Raw string
				getAllResponseHeaders: function() {
					return completed ? responseHeadersString : null;
				},

				// Caches the header
				setRequestHeader: function( name, value ) {
					if ( completed == null ) {
						name = requestHeadersNames[ name.toLowerCase() ] =
							requestHeadersNames[ name.toLowerCase() ] || name;
						requestHeaders[ name ] = value;
					}
					return this;
				},

				// Overrides response content-type header
				overrideMimeType: function( type ) {
					if ( completed == null ) {
						s.mimeType = type;
					}
					return this;
				},

				// Status-dependent callbacks
				statusCode: function( map ) {
					var code;
					if ( map ) {
						if ( completed ) {

							// Execute the appropriate callbacks
							jqXHR.always( map[ jqXHR.status ] );
						} else {

							// Lazy-add the new callbacks in a way that preserves old ones
							for ( code in map ) {
								statusCode[ code ] = [ statusCode[ code ], map[ code ] ];
							}
						}
					}
					return this;
				},

				// Cancel the request
				abort: function( statusText ) {
					var finalText = statusText || strAbort;
					if ( transport ) {
						transport.abort( finalText );
					}
					done( 0, finalText );
					return this;
				}
			};

		// Attach deferreds
		deferred.promise( jqXHR );

		// Add protocol if not provided (prefilters might expect it)
		// Handle falsy url in the settings object (#10093: consistency with old signature)
		// We also use the url parameter if available
		s.url = ( ( url || s.url || location.href ) + "" )
			.replace( rprotocol, location.protocol + "//" );

		// Alias method option to type as per ticket #12004
		s.type = options.method || options.type || s.method || s.type;

		// Extract dataTypes list
		s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ];

		// A cross-domain request is in order when the origin doesn't match the current origin.
		if ( s.crossDomain == null ) {
			urlAnchor = document.createElement( "a" );

			// Support: IE <=8 - 11, Edge 12 - 13
			// IE throws exception on accessing the href property if url is malformed,
			// e.g. http://example.com:80x/
			try {
				urlAnchor.href = s.url;

				// Support: IE <=8 - 11 only
				// Anchor's host property isn't correctly set when s.url is relative
				urlAnchor.href = urlAnchor.href;
				s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !==
					urlAnchor.protocol + "//" + urlAnchor.host;
			} catch ( e ) {

				// If there is an error parsing the URL, assume it is crossDomain,
				// it can be rejected by the transport if it is invalid
				s.crossDomain = true;
			}
		}

		// Convert data if not already a string
		if ( s.data && s.processData && typeof s.data !== "string" ) {
			s.data = jQuery.param( s.data, s.traditional );
		}

		// Apply prefilters
		inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );

		// If request was aborted inside a prefilter, stop there
		if ( completed ) {
			return jqXHR;
		}

		// We can fire global events as of now if asked to
		// Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118)
		fireGlobals = jQuery.event && s.global;

		// Watch for a new set of requests
		if ( fireGlobals && jQuery.active++ === 0 ) {
			jQuery.event.trigger( "ajaxStart" );
		}

		// Uppercase the type
		s.type = s.type.toUpperCase();

		// Determine if request has content
		s.hasContent = !rnoContent.test( s.type );

		// Save the URL in case we're toying with the If-Modified-Since
		// and/or If-None-Match header later on
		// Remove hash to simplify url manipulation
		cacheURL = s.url.replace( rhash, "" );

		// More options handling for requests with no content
		if ( !s.hasContent ) {

			// Remember the hash so we can put it back
			uncached = s.url.slice( cacheURL.length );

			// If data is available, append data to url
			if ( s.data ) {
				cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data;

				// #9682: remove data so that it's not used in an eventual retry
				delete s.data;
			}

			// Add or update anti-cache param if needed
			if ( s.cache === false ) {
				cacheURL = cacheURL.replace( rantiCache, "$1" );
				uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce++ ) + uncached;
			}

			// Put hash and anti-cache on the URL that will be requested (gh-1732)
			s.url = cacheURL + uncached;

		// Change '%20' to '+' if this is encoded form body content (gh-2658)
		} else if ( s.data && s.processData &&
			( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) {
			s.data = s.data.replace( r20, "+" );
		}

		// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
		if ( s.ifModified ) {
			if ( jQuery.lastModified[ cacheURL ] ) {
				jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] );
			}
			if ( jQuery.etag[ cacheURL ] ) {
				jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] );
			}
		}

		// Set the correct header, if data is being sent
		if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
			jqXHR.setRequestHeader( "Content-Type", s.contentType );
		}

		// Set the Accepts header for the server, depending on the dataType
		jqXHR.setRequestHeader(
			"Accept",
			s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ?
				s.accepts[ s.dataTypes[ 0 ] ] +
					( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
				s.accepts[ "*" ]
		);

		// Check for headers option
		for ( i in s.headers ) {
			jqXHR.setRequestHeader( i, s.headers[ i ] );
		}

		// Allow custom headers/mimetypes and early abort
		if ( s.beforeSend &&
			( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) {

			// Abort if not done already and return
			return jqXHR.abort();
		}

		// Aborting is no longer a cancellation
		strAbort = "abort";

		// Install callbacks on deferreds
		completeDeferred.add( s.complete );
		jqXHR.done( s.success );
		jqXHR.fail( s.error );

		// Get transport
		transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );

		// If no transport, we auto-abort
		if ( !transport ) {
			done( -1, "No Transport" );
		} else {
			jqXHR.readyState = 1;

			// Send global event
			if ( fireGlobals ) {
				globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
			}

			// If request was aborted inside ajaxSend, stop there
			if ( completed ) {
				return jqXHR;
			}

			// Timeout
			if ( s.async && s.timeout > 0 ) {
				timeoutTimer = window.setTimeout( function() {
					jqXHR.abort( "timeout" );
				}, s.timeout );
			}

			try {
				completed = false;
				transport.send( requestHeaders, done );
			} catch ( e ) {

				// Rethrow post-completion exceptions
				if ( completed ) {
					throw e;
				}

				// Propagate others as results
				done( -1, e );
			}
		}

		// Callback for when everything is done
		function done( status, nativeStatusText, responses, headers ) {
			var isSuccess, success, error, response, modified,
				statusText = nativeStatusText;

			// Ignore repeat invocations
			if ( completed ) {
				return;
			}

			completed = true;

			// Clear timeout if it exists
			if ( timeoutTimer ) {
				window.clearTimeout( timeoutTimer );
			}

			// Dereference transport for early garbage collection
			// (no matter how long the jqXHR object will be used)
			transport = undefined;

			// Cache response headers
			responseHeadersString = headers || "";

			// Set readyState
			jqXHR.readyState = status > 0 ? 4 : 0;

			// Determine if successful
			isSuccess = status >= 200 && status < 300 || status === 304;

			// Get response data
			if ( responses ) {
				response = ajaxHandleResponses( s, jqXHR, responses );
			}

			// Convert no matter what (that way responseXXX fields are always set)
			response = ajaxConvert( s, response, jqXHR, isSuccess );

			// If successful, handle type chaining
			if ( isSuccess ) {

				// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
				if ( s.ifModified ) {
					modified = jqXHR.getResponseHeader( "Last-Modified" );
					if ( modified ) {
						jQuery.lastModified[ cacheURL ] = modified;
					}
					modified = jqXHR.getResponseHeader( "etag" );
					if ( modified ) {
						jQuery.etag[ cacheURL ] = modified;
					}
				}

				// if no content
				if ( status === 204 || s.type === "HEAD" ) {
					statusText = "nocontent";

				// if not modified
				} else if ( status === 304 ) {
					statusText = "notmodified";

				// If we have data, let's convert it
				} else {
					statusText = response.state;
					success = response.data;
					error = response.error;
					isSuccess = !error;
				}
			} else {

				// Extract error from statusText and normalize for non-aborts
				error = statusText;
				if ( status || !statusText ) {
					statusText = "error";
					if ( status < 0 ) {
						status = 0;
					}
				}
			}

			// Set data for the fake xhr object
			jqXHR.status = status;
			jqXHR.statusText = ( nativeStatusText || statusText ) + "";

			// Success/Error
			if ( isSuccess ) {
				deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
			} else {
				deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
			}

			// Status-dependent callbacks
			jqXHR.statusCode( statusCode );
			statusCode = undefined;

			if ( fireGlobals ) {
				globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
					[ jqXHR, s, isSuccess ? success : error ] );
			}

			// Complete
			completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );

			if ( fireGlobals ) {
				globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );

				// Handle the global AJAX counter
				if ( !( --jQuery.active ) ) {
					jQuery.event.trigger( "ajaxStop" );
				}
			}
		}

		return jqXHR;
	},

	getJSON: function( url, data, callback ) {
		return jQuery.get( url, data, callback, "json" );
	},

	getScript: function( url, callback ) {
		return jQuery.get( url, undefined, callback, "script" );
	}
} );

jQuery.each( [ "get", "post" ], function( i, method ) {
	jQuery[ method ] = function( url, data, callback, type ) {

		// Shift arguments if data argument was omitted
		if ( jQuery.isFunction( data ) ) {
			type = type || callback;
			callback = data;
			data = undefined;
		}

		// The url can be an options object (which then must have .url)
		return jQuery.ajax( jQuery.extend( {
			url: url,
			type: method,
			dataType: type,
			data: data,
			success: callback
		}, jQuery.isPlainObject( url ) && url ) );
	};
} );


jQuery._evalUrl = function( url ) {
	return jQuery.ajax( {
		url: url,

		// Make this explicit, since user can override this through ajaxSetup (#11264)
		type: "GET",
		dataType: "script",
		cache: true,
		async: false,
		global: false,
		"throws": true
	} );
};


jQuery.fn.extend( {
	wrapAll: function( html ) {
		var wrap;

		if ( this[ 0 ] ) {
			if ( jQuery.isFunction( html ) ) {
				html = html.call( this[ 0 ] );
			}

			// The elements to wrap the target around
			wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );

			if ( this[ 0 ].parentNode ) {
				wrap.insertBefore( this[ 0 ] );
			}

			wrap.map( function() {
				var elem = this;

				while ( elem.firstElementChild ) {
					elem = elem.firstElementChild;
				}

				return elem;
			} ).append( this );
		}

		return this;
	},

	wrapInner: function( html ) {
		if ( jQuery.isFunction( html ) ) {
			return this.each( function( i ) {
				jQuery( this ).wrapInner( html.call( this, i ) );
			} );
		}

		return this.each( function() {
			var self = jQuery( this ),
				contents = self.contents();

			if ( contents.length ) {
				contents.wrapAll( html );

			} else {
				self.append( html );
			}
		} );
	},

	wrap: function( html ) {
		var isFunction = jQuery.isFunction( html );

		return this.each( function( i ) {
			jQuery( this ).wrapAll( isFunction ? html.call( this, i ) : html );
		} );
	},

	unwrap: function( selector ) {
		this.parent( selector ).not( "body" ).each( function() {
			jQuery( this ).replaceWith( this.childNodes );
		} );
		return this;
	}
} );


jQuery.expr.pseudos.hidden = function( elem ) {
	return !jQuery.expr.pseudos.visible( elem );
};
jQuery.expr.pseudos.visible = function( elem ) {
	return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
};




jQuery.ajaxSettings.xhr = function() {
	try {
		return new window.XMLHttpRequest();
	} catch ( e ) {}
};

var xhrSuccessStatus = {

		// File protocol always yields status code 0, assume 200
		0: 200,

		// Support: IE <=9 only
		// #1450: sometimes IE returns 1223 when it should be 204
		1223: 204
	},
	xhrSupported = jQuery.ajaxSettings.xhr();

support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported );
support.ajax = xhrSupported = !!xhrSupported;

jQuery.ajaxTransport( function( options ) {
	var callback, errorCallback;

	// Cross domain only allowed if supported through XMLHttpRequest
	if ( support.cors || xhrSupported && !options.crossDomain ) {
		return {
			send: function( headers, complete ) {
				var i,
					xhr = options.xhr();

				xhr.open(
					options.type,
					options.url,
					options.async,
					options.username,
					options.password
				);

				// Apply custom fields if provided
				if ( options.xhrFields ) {
					for ( i in options.xhrFields ) {
						xhr[ i ] = options.xhrFields[ i ];
					}
				}

				// Override mime type if needed
				if ( options.mimeType && xhr.overrideMimeType ) {
					xhr.overrideMimeType( options.mimeType );
				}

				// X-Requested-With header
				// For cross-domain requests, seeing as conditions for a preflight are
				// akin to a jigsaw puzzle, we simply never set it to be sure.
				// (it can always be set on a per-request basis or even using ajaxSetup)
				// For same-domain requests, won't change header if already provided.
				if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) {
					headers[ "X-Requested-With" ] = "XMLHttpRequest";
				}

				// Set headers
				for ( i in headers ) {
					xhr.setRequestHeader( i, headers[ i ] );
				}

				// Callback
				callback = function( type ) {
					return function() {
						if ( callback ) {
							callback = errorCallback = xhr.onload =
								xhr.onerror = xhr.onabort = xhr.onreadystatechange = null;

							if ( type === "abort" ) {
								xhr.abort();
							} else if ( type === "error" ) {

								// Support: IE <=9 only
								// On a manual native abort, IE9 throws
								// errors on any property access that is not readyState
								if ( typeof xhr.status !== "number" ) {
									complete( 0, "error" );
								} else {
									complete(

										// File: protocol always yields status 0; see #8605, #14207
										xhr.status,
										xhr.statusText
									);
								}
							} else {
								complete(
									xhrSuccessStatus[ xhr.status ] || xhr.status,
									xhr.statusText,

									// Support: IE <=9 only
									// IE9 has no XHR2 but throws on binary (trac-11426)
									// For XHR2 non-text, let the caller handle it (gh-2498)
									( xhr.responseType || "text" ) !== "text"  ||
									typeof xhr.responseText !== "string" ?
										{ binary: xhr.response } :
										{ text: xhr.responseText },
									xhr.getAllResponseHeaders()
								);
							}
						}
					};
				};

				// Listen to events
				xhr.onload = callback();
				errorCallback = xhr.onerror = callback( "error" );

				// Support: IE 9 only
				// Use onreadystatechange to replace onabort
				// to handle uncaught aborts
				if ( xhr.onabort !== undefined ) {
					xhr.onabort = errorCallback;
				} else {
					xhr.onreadystatechange = function() {

						// Check readyState before timeout as it changes
						if ( xhr.readyState === 4 ) {

							// Allow onerror to be called first,
							// but that will not handle a native abort
							// Also, save errorCallback to a variable
							// as xhr.onerror cannot be accessed
							window.setTimeout( function() {
								if ( callback ) {
									errorCallback();
								}
							} );
						}
					};
				}

				// Create the abort callback
				callback = callback( "abort" );

				try {

					// Do send the request (this may raise an exception)
					xhr.send( options.hasContent && options.data || null );
				} catch ( e ) {

					// #14683: Only rethrow if this hasn't been notified as an error yet
					if ( callback ) {
						throw e;
					}
				}
			},

			abort: function() {
				if ( callback ) {
					callback();
				}
			}
		};
	}
} );




// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432)
jQuery.ajaxPrefilter( function( s ) {
	if ( s.crossDomain ) {
		s.contents.script = false;
	}
} );

// Install script dataType
jQuery.ajaxSetup( {
	accepts: {
		script: "text/javascript, application/javascript, " +
			"application/ecmascript, application/x-ecmascript"
	},
	contents: {
		script: /\b(?:java|ecma)script\b/
	},
	converters: {
		"text script": function( text ) {
			jQuery.globalEval( text );
			return text;
		}
	}
} );

// Handle cache's special case and crossDomain
jQuery.ajaxPrefilter( "script", function( s ) {
	if ( s.cache === undefined ) {
		s.cache = false;
	}
	if ( s.crossDomain ) {
		s.type = "GET";
	}
} );

// Bind script tag hack transport
jQuery.ajaxTransport( "script", function( s ) {

	// This transport only deals with cross domain requests
	if ( s.crossDomain ) {
		var script, callback;
		return {
			send: function( _, complete ) {
				script = jQuery( "<script>" ).prop( {
					charset: s.scriptCharset,
					src: s.url
				} ).on(
					"load error",
					callback = function( evt ) {
						script.remove();
						callback = null;
						if ( evt ) {
							complete( evt.type === "error" ? 404 : 200, evt.type );
						}
					}
				);

				// Use native DOM manipulation to avoid our domManip AJAX trickery
				document.head.appendChild( script[ 0 ] );
			},
			abort: function() {
				if ( callback ) {
					callback();
				}
			}
		};
	}
} );




var oldCallbacks = [],
	rjsonp = /(=)\?(?=&|$)|\?\?/;

// Default jsonp settings
jQuery.ajaxSetup( {
	jsonp: "callback",
	jsonpCallback: function() {
		var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) );
		this[ callback ] = true;
		return callback;
	}
} );

// Detect, normalize options and install callbacks for jsonp requests
jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {

	var callbackName, overwritten, responseContainer,
		jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?
			"url" :
			typeof s.data === "string" &&
				( s.contentType || "" )
					.indexOf( "application/x-www-form-urlencoded" ) === 0 &&
				rjsonp.test( s.data ) && "data"
		);

	// Handle iff the expected data type is "jsonp" or we have a parameter to set
	if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {

		// Get callback name, remembering preexisting value associated with it
		callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
			s.jsonpCallback() :
			s.jsonpCallback;

		// Insert callback into url or form data
		if ( jsonProp ) {
			s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
		} else if ( s.jsonp !== false ) {
			s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
		}

		// Use data converter to retrieve json after script execution
		s.converters[ "script json" ] = function() {
			if ( !responseContainer ) {
				jQuery.error( callbackName + " was not called" );
			}
			return responseContainer[ 0 ];
		};

		// Force json dataType
		s.dataTypes[ 0 ] = "json";

		// Install callback
		overwritten = window[ callbackName ];
		window[ callbackName ] = function() {
			responseContainer = arguments;
		};

		// Clean-up function (fires after converters)
		jqXHR.always( function() {

			// If previous value didn't exist - remove it
			if ( overwritten === undefined ) {
				jQuery( window ).removeProp( callbackName );

			// Otherwise restore preexisting value
			} else {
				window[ callbackName ] = overwritten;
			}

			// Save back as free
			if ( s[ callbackName ] ) {

				// Make sure that re-using the options doesn't screw things around
				s.jsonpCallback = originalSettings.jsonpCallback;

				// Save the callback name for future use
				oldCallbacks.push( callbackName );
			}

			// Call if it was a function and we have a response
			if ( responseContainer && jQuery.isFunction( overwritten ) ) {
				overwritten( responseContainer[ 0 ] );
			}

			responseContainer = overwritten = undefined;
		} );

		// Delegate to script
		return "script";
	}
} );




// Support: Safari 8 only
// In Safari 8 documents created via document.implementation.createHTMLDocument
// collapse sibling forms: the second one becomes a child of the first one.
// Because of that, this security measure has to be disabled in Safari 8.
// https://bugs.webkit.org/show_bug.cgi?id=137337
support.createHTMLDocument = ( function() {
	var body = document.implementation.createHTMLDocument( "" ).body;
	body.innerHTML = "<form></form><form></form>";
	return body.childNodes.length === 2;
} )();


// Argument "data" should be string of html
// context (optional): If specified, the fragment will be created in this context,
// defaults to document
// keepScripts (optional): If true, will include scripts passed in the html string
jQuery.parseHTML = function( data, context, keepScripts ) {
	if ( typeof data !== "string" ) {
		return [];
	}
	if ( typeof context === "boolean" ) {
		keepScripts = context;
		context = false;
	}

	var base, parsed, scripts;

	if ( !context ) {

		// Stop scripts or inline event handlers from being executed immediately
		// by using document.implementation
		if ( support.createHTMLDocument ) {
			context = document.implementation.createHTMLDocument( "" );

			// Set the base href for the created document
			// so any parsed elements with URLs
			// are based on the document's URL (gh-2965)
			base = context.createElement( "base" );
			base.href = document.location.href;
			context.head.appendChild( base );
		} else {
			context = document;
		}
	}

	parsed = rsingleTag.exec( data );
	scripts = !keepScripts && [];

	// Single tag
	if ( parsed ) {
		return [ context.createElement( parsed[ 1 ] ) ];
	}

	parsed = buildFragment( [ data ], context, scripts );

	if ( scripts && scripts.length ) {
		jQuery( scripts ).remove();
	}

	return jQuery.merge( [], parsed.childNodes );
};


/**
 * Load a url into a page
 */
jQuery.fn.load = function( url, params, callback ) {
	var selector, type, response,
		self = this,
		off = url.indexOf( " " );

	if ( off > -1 ) {
		selector = stripAndCollapse( url.slice( off ) );
		url = url.slice( 0, off );
	}

	// If it's a function
	if ( jQuery.isFunction( params ) ) {

		// We assume that it's the callback
		callback = params;
		params = undefined;

	// Otherwise, build a param string
	} else if ( params && typeof params === "object" ) {
		type = "POST";
	}

	// If we have elements to modify, make the request
	if ( self.length > 0 ) {
		jQuery.ajax( {
			url: url,

			// If "type" variable is undefined, then "GET" method will be used.
			// Make value of this field explicit since
			// user can override it through ajaxSetup method
			type: type || "GET",
			dataType: "html",
			data: params
		} ).done( function( responseText ) {

			// Save response for use in complete callback
			response = arguments;

			self.html( selector ?

				// If a selector was specified, locate the right elements in a dummy div
				// Exclude scripts to avoid IE 'Permission Denied' errors
				jQuery( "<div>" ).append( jQuery.parseHTML( responseText ) ).find( selector ) :

				// Otherwise use the full result
				responseText );

		// If the request succeeds, this function gets "data", "status", "jqXHR"
		// but they are ignored because response was set above.
		// If it fails, this function gets "jqXHR", "status", "error"
		} ).always( callback && function( jqXHR, status ) {
			self.each( function() {
				callback.apply( this, response || [ jqXHR.responseText, status, jqXHR ] );
			} );
		} );
	}

	return this;
};




// Attach a bunch of functions for handling common AJAX events
jQuery.each( [
	"ajaxStart",
	"ajaxStop",
	"ajaxComplete",
	"ajaxError",
	"ajaxSuccess",
	"ajaxSend"
], function( i, type ) {
	jQuery.fn[ type ] = function( fn ) {
		return this.on( type, fn );
	};
} );




jQuery.expr.pseudos.animated = function( elem ) {
	return jQuery.grep( jQuery.timers, function( fn ) {
		return elem === fn.elem;
	} ).length;
};




jQuery.offset = {
	setOffset: function( elem, options, i ) {
		var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,
			position = jQuery.css( elem, "position" ),
			curElem = jQuery( elem ),
			props = {};

		// Set position first, in-case top/left are set even on static elem
		if ( position === "static" ) {
			elem.style.position = "relative";
		}

		curOffset = curElem.offset();
		curCSSTop = jQuery.css( elem, "top" );
		curCSSLeft = jQuery.css( elem, "left" );
		calculatePosition = ( position === "absolute" || position === "fixed" ) &&
			( curCSSTop + curCSSLeft ).indexOf( "auto" ) > -1;

		// Need to be able to calculate position if either
		// top or left is auto and position is either absolute or fixed
		if ( calculatePosition ) {
			curPosition = curElem.position();
			curTop = curPosition.top;
			curLeft = curPosition.left;

		} else {
			curTop = parseFloat( curCSSTop ) || 0;
			curLeft = parseFloat( curCSSLeft ) || 0;
		}

		if ( jQuery.isFunction( options ) ) {

			// Use jQuery.extend here to allow modification of coordinates argument (gh-1848)
			options = options.call( elem, i, jQuery.extend( {}, curOffset ) );
		}

		if ( options.top != null ) {
			props.top = ( options.top - curOffset.top ) + curTop;
		}
		if ( options.left != null ) {
			props.left = ( options.left - curOffset.left ) + curLeft;
		}

		if ( "using" in options ) {
			options.using.call( elem, props );

		} else {
			curElem.css( props );
		}
	}
};

jQuery.fn.extend( {
	offset: function( options ) {

		// Preserve chaining for setter
		if ( arguments.length ) {
			return options === undefined ?
				this :
				this.each( function( i ) {
					jQuery.offset.setOffset( this, options, i );
				} );
		}

		var doc, docElem, rect, win,
			elem = this[ 0 ];

		if ( !elem ) {
			return;
		}

		// Return zeros for disconnected and hidden (display: none) elements (gh-2310)
		// Support: IE <=11 only
		// Running getBoundingClientRect on a
		// disconnected node in IE throws an error
		if ( !elem.getClientRects().length ) {
			return { top: 0, left: 0 };
		}

		rect = elem.getBoundingClientRect();

		doc = elem.ownerDocument;
		docElem = doc.documentElement;
		win = doc.defaultView;

		return {
			top: rect.top + win.pageYOffset - docElem.clientTop,
			left: rect.left + win.pageXOffset - docElem.clientLeft
		};
	},

	position: function() {
		if ( !this[ 0 ] ) {
			return;
		}

		var offsetParent, offset,
			elem = this[ 0 ],
			parentOffset = { top: 0, left: 0 };

		// Fixed elements are offset from window (parentOffset = {top:0, left: 0},
		// because it is its only offset parent
		if ( jQuery.css( elem, "position" ) === "fixed" ) {

			// Assume getBoundingClientRect is there when computed position is fixed
			offset = elem.getBoundingClientRect();

		} else {

			// Get *real* offsetParent
			offsetParent = this.offsetParent();

			// Get correct offsets
			offset = this.offset();
			if ( !nodeName( offsetParent[ 0 ], "html" ) ) {
				parentOffset = offsetParent.offset();
			}

			// Add offsetParent borders
			parentOffset = {
				top: parentOffset.top + jQuery.css( offsetParent[ 0 ], "borderTopWidth", true ),
				left: parentOffset.left + jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true )
			};
		}

		// Subtract parent offsets and element margins
		return {
			top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ),
			left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true )
		};
	},

	// This method will return documentElement in the following cases:
	// 1) For the element inside the iframe without offsetParent, this method will return
	//    documentElement of the parent window
	// 2) For the hidden or detached element
	// 3) For body or html element, i.e. in case of the html node - it will return itself
	//
	// but those exceptions were never presented as a real life use-cases
	// and might be considered as more preferable results.
	//
	// This logic, however, is not guaranteed and can change at any point in the future
	offsetParent: function() {
		return this.map( function() {
			var offsetParent = this.offsetParent;

			while ( offsetParent && jQuery.css( offsetParent, "position" ) === "static" ) {
				offsetParent = offsetParent.offsetParent;
			}

			return offsetParent || documentElement;
		} );
	}
} );

// Create scrollLeft and scrollTop methods
jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) {
	var top = "pageYOffset" === prop;

	jQuery.fn[ method ] = function( val ) {
		return access( this, function( elem, method, val ) {

			// Coalesce documents and windows
			var win;
			if ( jQuery.isWindow( elem ) ) {
				win = elem;
			} else if ( elem.nodeType === 9 ) {
				win = elem.defaultView;
			}

			if ( val === undefined ) {
				return win ? win[ prop ] : elem[ method ];
			}

			if ( win ) {
				win.scrollTo(
					!top ? val : win.pageXOffset,
					top ? val : win.pageYOffset
				);

			} else {
				elem[ method ] = val;
			}
		}, method, val, arguments.length );
	};
} );

// Support: Safari <=7 - 9.1, Chrome <=37 - 49
// Add the top/left cssHooks using jQuery.fn.position
// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
// Blink bug: https://bugs.chromium.org/p/chromium/issues/detail?id=589347
// getComputedStyle returns percent when specified for top/left/bottom/right;
// rather than make the css module depend on the offset module, just check for it here
jQuery.each( [ "top", "left" ], function( i, prop ) {
	jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,
		function( elem, computed ) {
			if ( computed ) {
				computed = curCSS( elem, prop );

				// If curCSS returns percentage, fallback to offset
				return rnumnonpx.test( computed ) ?
					jQuery( elem ).position()[ prop ] + "px" :
					computed;
			}
		}
	);
} );


// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
	jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name },
		function( defaultExtra, funcName ) {

		// Margin is only for outerHeight, outerWidth
		jQuery.fn[ funcName ] = function( margin, value ) {
			var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
				extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );

			return access( this, function( elem, type, value ) {
				var doc;

				if ( jQuery.isWindow( elem ) ) {

					// $( window ).outerWidth/Height return w/h including scrollbars (gh-1729)
					return funcName.indexOf( "outer" ) === 0 ?
						elem[ "inner" + name ] :
						elem.document.documentElement[ "client" + name ];
				}

				// Get document width or height
				if ( elem.nodeType === 9 ) {
					doc = elem.documentElement;

					// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],
					// whichever is greatest
					return Math.max(
						elem.body[ "scroll" + name ], doc[ "scroll" + name ],
						elem.body[ "offset" + name ], doc[ "offset" + name ],
						doc[ "client" + name ]
					);
				}

				return value === undefined ?

					// Get width or height on the element, requesting but not forcing parseFloat
					jQuery.css( elem, type, extra ) :

					// Set width or height on the element
					jQuery.style( elem, type, value, extra );
			}, type, chainable ? margin : undefined, chainable );
		};
	} );
} );


jQuery.fn.extend( {

	bind: function( types, data, fn ) {
		return this.on( types, null, data, fn );
	},
	unbind: function( types, fn ) {
		return this.off( types, null, fn );
	},

	delegate: function( selector, types, data, fn ) {
		return this.on( types, selector, data, fn );
	},
	undelegate: function( selector, types, fn ) {

		// ( namespace ) or ( selector, types [, fn] )
		return arguments.length === 1 ?
			this.off( selector, "**" ) :
			this.off( types, selector || "**", fn );
	},
	holdReady: function( hold ) {
		if ( hold ) {
			jQuery.readyWait++;
		} else {
			jQuery.ready( true );
		}
	}
} );

jQuery.isArray = Array.isArray;
jQuery.parseJSON = JSON.parse;
jQuery.nodeName = nodeName;




// Register as a named AMD module, since jQuery can be concatenated with other
// files that may use define, but not via a proper concatenation script that
// understands anonymous AMD modules. A named AMD is safest and most robust
// way to register. Lowercase jquery is used because AMD module names are
// derived from file names, and jQuery is normally delivered in a lowercase
// file name. Do this after creating the global so that if an AMD module wants
// to call noConflict to hide this version of jQuery, it will work.

// Note that for maximum portability, libraries that are not jQuery should
// declare themselves as anonymous modules, and avoid setting a global if an
// AMD loader is present. jQuery is a special case. For more information, see
// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon

if ( typeof rjs.define === 'function' && rjs.define.amd ) {
	rjs.define( "jquery", [], function() {
		return jQuery;
	} );
}




var

	// Map over jQuery in case of overwrite
	_jQuery = window.jQuery,

	// Map over the $ in case of overwrite
	_$ = window.$;

jQuery.noConflict = function( deep ) {
	if ( window.$ === jQuery ) {
		window.$ = _$;
	}

	if ( deep && window.jQuery === jQuery ) {
		window.jQuery = _jQuery;
	}

	return jQuery;
};

// Expose jQuery and $ identifiers, even in AMD
// (#7102#comment:10, https://github.com/jquery/jquery/pull/557)
// and CommonJS for browser emulators (#13566)
if ( !noGlobal ) {
	window.jQuery = window.$ = jQuery;
}




return jQuery;
} );

rjs.define('build-config',[],function () { return {
  "environment": "ap-se",
  "useLegacyTlsUrls": false,
  "enableDWS": false,
  "urls": {
    "remoteApplicationUpdates": "https://ap-southeast-2-1-clients.signagelive.com/brightsign/series6/",
    "dateTimeServer": "http://ap-southeast-2-1-time-api.signagelive.com/api/localtime/?tzName=",
    "playerApi": "https://ap-southeast-2-1-player-api.signagelive.com/v1.0/client",
    "legacyPlayerApi": "https://undefined/v1.0/client",
    "playerDataApi": "https://ap-southeast-2-1-player-data-api.signagelive.com/v1.0/client",
    "legacyPlayerDataApi": "https://undefined/v1.0/client",
    "screenshotUploadApi": "https://ap-southeast-2-1-screenshot-api.signagelive.com/v1.0/client",
    "legacyScreenshotUploadApi": "https://undefined/v1.0/client",
    "webBasedInterrupts": "https://ap-southeast-2-1-wbt-api.signagelive.com",
    "legacyWebBasedInterrupts": "https://undefined",
    "rssProxyUrl": "https://undefined",
    "legacyTlsRssProxyUrl": "https://undefined",
    "contentUrl": "https://ap-southeast-2-1-static.signagelive.com",
    "legacyTlsContentUrl": "https://undefined",
    "widgetUrl": "https://undefined",
    "legacyTlsWidgetUrl": "https://undefined",
    "m2mUrl": "https://undefined",
    "legacyTlsM2mUrl": "https://undefined"
  }
}; });
rjs.define('properties/App.Settings',["build-config"], function(BuildConfig) {
    // Default to SD card directory. 
    // If the device uses another storage method (USB storage, for example), we:
        // 1. Directly update the value of storageDir for properties on the fileSystem object
        // 2. Add a new property (storageDir) to the fileSystem object itself, as some parts of the code build a path string, rather than reference a pre-built path
    var storageDir = "storage/sd";

    return {
        application: {
            // Configure application paths
            fileSystem: {
                root: "",
                storageDirectory: "storage/sd",
                applicationDataDirectory: storageDir + "/data",
                mediaDirectory: storageDir + "/media",
                logDirectory: storageDir + "/log",
                widgetDirectory: storageDir + "/widgets",
                integrationsWidgetSubdirectory: "sl_integrations",
                dataDirectory: storageDir + "/data",
                appDirectory: storageDir + "/app",
                tempDirectory: storageDir + "/temp",
                systemFiles: {
                    settingsFilePath: storageDir + "/data/settings.json",
                    propertiesFilePath: storageDir + "/data/properties.json",
                    lastConfigFilePath: storageDir + "/data/last_config.json",
                    clientIdFilePath: storageDir + "/data/clientId.json",
                    clientKeyFilePath: storageDir + "/data/clientKey.json",
                    appVersionFilePath: storageDir + "/data/app.version.json",
                    fileListFilePath: storageDir + "/data/files.json",
                    proofOfPlayFilePath: storageDir + "/data/proofofplay.json",
                    proofOfPlayUploadFilePath: storageDir + "/data/proofofplayupload.json",
                    screenshotFileListFilePath: storageDir + "/data/screenshotfilelist.json",
                    networkingFilePath: storageDir + "/data/network.json",
                    failedRTERequestsPath: storageDir + "/data/failedRTERequests.json",
                    sandboxUnsupportedUrlsPath: storageDir + "/data/sandboxUnsupportedUrls.json"
                }
            },
            criticalSections: {
                videosPlaying: 0
            },
            dateTimeServer: BuildConfig.urls.dateTimeServer,
            proxyUrl: BuildConfig.urls.rssProxyUrl,
            defaultNTPServer: 'ntp://time.brightsignnetwork.com',
            // Enable / Disable support for client features
            features: {
                canSyncDateTime: false,
                syncDateTimeFrequency: 0,
                canDownloadFiles: true,
                canSwitchScreenOnOff: true,
                maxConcurrentVideos: 2,
                canPerformApplicationUpdates: true,
                canPerformFirmwareUpdates: true,
                canvasBehindLayout: false,
                canSendSystemInformation: false,
                canListenForRemoteKeyPresses: false,
                showWebsiteLoadingAnimation: false,
                canPlayInterrupts: true,
                supportsProofOfPlay: true,
                canTakeScreenshots: true,
                deleteOldFilesOnStartup: false,
                deleteOldFilesOnContentUpdate: true,
                checkAvailableStorageQuotaBeforeMediaDownload: true,
                canListenForWebBasedInterrupts: true,
                canReboot: true,
                canConfigureNetworking: true,
                canDisplay4kContent: false,
                canDisplay8kContent: false,
                supportsSeamlessTransitions: false,
                canDisplayVideoInput: false,
                supportsHashCRC32: true,
                deviceSeries: null,
                supportsMultipleOutputs: false,
                maxSupportedOutputs: 1,
                canTakeoverContent: false,
                canSetScreenMode: true
            },
            // Signagelive API Settings
            api: {
                url: BuildConfig.urls.playerApi,
                appId: '8135a1e2',
                appKey: '36f115ac4a1149c48f267b7d1f7970ad',
                clientId: null,
                clientKey: null,
                authToken: null,
                authTokenExpiry: null,
                brand: 'Signagelive'
            },
            // Signagelive Proof of Play API Settings
            playerdataapi: {
                url: BuildConfig.urls.playerDataApi,
            },
            screenshotuploadapi: {
                url: BuildConfig.urls.screenshotUploadApi,
            },
            remoteApplicationUpdates: {
                location: BuildConfig.urls.remoteApplicationUpdates,
                configFileName: 'bs_config.xml',
                frequency: 600 // In Seconds
            },
            webBasedInterrupts: {
                url: BuildConfig.urls.webBasedInterrupts,
                configurationCheckInterval: 15,
                pusherSettings: null,
                cluster: 'ap4'
            },
            ssl: {
                maxSupportedTLSVersion: 1.2,
                maxSupportedTLSVersionString: "TLS 1.2",
                sslCheckAPIUrl: "https://www.howsmyssl.com/a/check"
            },
            // Settings for additional modules
            modules: {},
            // Remote Firmware Update Settings
            remoteFirmwareUpdates: {},
            dataSyncServices: {
                remoteFileTypeName: 'RemoteFileDTO',
                schemaQuery: "{ __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } } fragment FullType on __Type { kind name description fields(includeDeprecated: false) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType {  kind  name  ofType { kind name ofType { kind name ofType {  kind  name } } } } } } } }",
                dataFileName: "data.json",
                unmodifiedDataFileName: "unmodified-data.json",
                remoteFileSubDirectoryName: 'RemoteFiles'
            }
        },
        user: {
            // Set to true if you would like to show a debug output trace overlay in front of the media player to aid with debugging
            showTraceOutput: false,
            loggingLevel: 0,
            settingsLoaded: false,
            mySettings: {
                ismuted: true,
                showTrace: false,
                rotation: {
                    displayName: 'No rotation',
                    angle: 0,
                    id: 0
                },
                timeZone: {
                    displayName: '(GMT) Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London',
                    bsid: 'GMTBST',
                    id: 'Europe/London'
                }
            }
        }
    };
});
/*
 * The DateTime plugin returns a singleton object that allows the caller to get the current system time, and parse a number of common
 * Javascript datetime string formats.
 */
rjs.define('system/DateTime',["properties/App.Settings"],function(Settings) {
    function DateTimePlugin() {
        var self = this;
 
        var systemTimeClass = require("@brightsign/systemtime");
        var systemTime = new systemTimeClass();

        self.setDateTime = function(dt) {

        };

        function ensure4PositionHex(hex) {
            if (hex.length == 1)
                hex = '000' + hex;

            if (hex.length == 2)
                hex = '00' + hex;

            if (hex.length == 3)
                hex = '0' + hex;

            return hex;
        }

        function ensure2PositionHex(hex) {
            if (hex.length == 1)
                hex = '0' + hex;
            return hex;
        }

        function getCurrentJSDateTime() {
            return new Date();
        }

        self.updateTimeZone = function(tzModel) 
        {
            var dfd = $.Deferred();

            logger.debug("Updating timezone to " + tzModel.bsid);

            systemTime.setTimeZone(tzModel.bsid).then(function(){
                logger.debug("Timezone updated");
				dfd.resolve(true);
			}).catch(function(error)
			{
                logger.error("Error setting timezone: " + error.message);			
                dfd.reject();   
			});
        
            return dfd;
        }

        self.syncDateTime = function(tzId){
            var dfd = $.Deferred();

            setTimeout(function() {
                //Not required on the BrightSign
                dfd.resolve();
            }, 100);

            return dfd;
        }


        self.now = function() {
            return getCurrentJSDateTime();
        };

        self.parse = function(string) {
            /*
             *Parses a date time string in the following formats to a Javascript datetime
             *
             *2013-06-03
             *2013-06-03T11:00:00
             *2013-06-04T09:07:29Z
             *9999-12-30T23:59:59.9999
             *2013-06-04T12:30:05.2926Z
             */

            var year = "1970";
            var month = "01";
            var day = "01";
            var hours = "00";
            var minutes = "00";
            var seconds = "00";
            var milliseconds = "0000";

            // has a date and a time been passed? - e.g. 2013-06-03T11:00:00
            var dateTimeComponents = splitDateTimeString(string);
            if (dateTimeComponents.length == 2) {
                var dateComponents = splitDateString(dateTimeComponents[0]);
                year = dateComponents[0];
                month = dateComponents[1];
                month = parseInt(month, 10) - 1;
                day = dateComponents[2];

                // we have a string in at least format - 2013-06-03T11:00:00
                var timeComponents = splitTimeString(dateTimeComponents[1]);
                hours = timeComponents[0];
                minutes = timeComponents[1];
                seconds = timeComponents[2];

                // check the seconds component as it may have milliseconds or a timezone attached to it
                if (seconds.length > 2) {
                    var secondsComponents = splitSecondsString(seconds);

                    // do we have a string in the format (for seconds only):
                    // 29Z or 59.9999 or 05.2926Z
                    // note that 00 is already handled by the check for a length greater than 2 places
                    if (secondsComponents.length == 2) {
                        // the format is either 59.9999 or 05.2926Z
                        // the first two places are the seconds
                        seconds = secondsComponents[0];
                    } else {
                        // the format is 29Z
                        // use the first two places as the seconds and ignore the timezone (SSSP does not interpret timezone)
                        seconds = seconds.substring(0, 2);
                    }
                }

            } else {
                // only a date e.g 2013-06-03
                var dateComponents = splitDateString(dateTimeComponents[0]);
                year = dateComponents[0];
                month = parseInt(dateComponents[1], 10) - 1;
                day = dateComponents[2];
            }

            return new Date(year, month, day, hours, minutes, seconds, milliseconds);
        };

        function splitDateTimeString(string) {
            return string.split('T');
        }

        function splitDateString(string) {
            return string.split('-');
        }

        function splitTimeString(string) {
            return string.split(':');
        }

        function splitSecondsString(string) {
            return string.split(".");
        }

    }

    Date.prototype.getStringRepresentation = function() {
        var month = this.getMonth() + 1;
        if (month < 10)
            month = "0" + month;

        var day = this.getDate();
        if (day < 10)
            day = "0" + day;

        var hour = this.getHours();
        if (hour < 10)
            hour = "0" + hour;

        var minutes = this.getMinutes();
        if (minutes < 10)
            minutes = "0" + minutes;

        var seconds = this.getSeconds();
        if (seconds < 10)
            seconds = "0" + seconds;

        return this.getFullYear() + "-" + month + "-" + day + "T" + hour + ":" + minutes + ":" + seconds + "Z";
    };

    Date.prototype.toStringLocalTime = function() {
        var month = this.getMonth() + 1;
        if (month < 10)
            month = "0" + month;

        var day = this.getDate();
        if (day < 10)
            day = "0" + day;

        var hour = this.getHours();
        if (hour < 10)
            hour = "0" + hour;

        var minutes = this.getMinutes();
        if (minutes < 10)
            minutes = "0" + minutes;

        var seconds = this.getSeconds();
        if (seconds < 10)
            seconds = "0" + seconds;

        var milliseconds = this.getMilliseconds();
        if(milliseconds < 10)
            milliseconds = "00" + milliseconds;
        else if (milliseconds < 100)
            milliseconds = "0" + milliseconds;

        return this.getFullYear() + "-" + month + "-" + day + "T" + hour + ":" + minutes + ":" + seconds + "." + milliseconds;
    };

    Date.prototype.addDays = function(n) {
        var newDate = new Date(this);
        newDate.setDate(newDate.getDate() + n);
        return newDate;
    };
    /*
     *Adds the number of hours to the specified date and returns a NEW date object
     */
    Date.prototype.addHours = function(h) {
        var newDate = new Date(this);
        newDate.setHours(newDate.getHours() + h);
        return newDate;
    };

    Date.prototype.addMinutes = function(n) {
        var newDate = new Date(this);
        newDate.setMinutes(newDate.getMinutes() + n);
        return newDate;
    };

    Date.prototype.addSeconds = function(n) {
        var newDate = new Date(this);
        newDate.setSeconds(newDate.getSeconds() + n);
        return newDate;
    };

    Date.prototype.addMilliseconds = function(n) {
        var newDate = new Date(this);
        newDate.setMilliseconds(newDate.getMilliseconds() + n);
        return newDate;
    };
    /*
     * Returns the difference in milliseconds between the two dates
     */
    Date.prototype.subtract = function(dt) {
        return this - dt;
    };
    /*
     * Accepts a time in the format HH:mm:ss and returns a NEW date object
     *
     */
    Date.prototype.setTime = function(time) {
        var timeParts = time.split(':');
        var newDate = new Date(this);
        newDate.setHours(timeParts[0]);
        newDate.setMinutes(timeParts[1]);
        newDate.setSeconds(timeParts[2]);

        return newDate;
    };

    Date.prototype.toLocaleStringOrig = Date.prototype.toLocaleString;
    Date.prototype.toLocaleString = function()
    {
        var str = "";
        try{
            str = this.toLocaleStringOrig();
        }  catch(err)
        {
            str = this.toISOString();
        }     
        return str;
    }

    return new DateTimePlugin();
});
rjs.define('utils/logger',["system/DateTime"], function(DateTime) {
    var logger = {
        levels: {
            DEBUG: 0,
            INFO: 1,
            WARNING: 2,
            ERROR: 3,
            FATAL: 4
        },
        level: 0,
        enableTrace: false,
        traceCounter: 0,
        traceCounterMax: 50,
        debug: function(msg) {
            if (this.level == this.levels.DEBUG) {
                msg = DateTime.now().toLocaleString() + " DEBUG   " + msg;
                this.trace(msg);

                console.log(msg);
            }
        },
        info: function(msg) {
            if (this.level <= this.levels.INFO) {
                msg = DateTime.now().toLocaleString() + " INFO    " + msg;
                this.trace(msg);

                console.info(msg);
            }
        },
        warning: function(msg) {
            if (this.level <= this.levels.WARNING) {
                msg = DateTime.now().toLocaleString() + " WARNING " + msg;
                this.trace(msg);

                console.warn(msg);
            }
        },
        error: function(msg) {
            if (this.level <= this.levels.ERROR) {
                msg = DateTime.now().toLocaleString() + " ERROR   " + msg;
                this.trace(msg);

                console.error(msg);
            }
        },
        fatal: function(msg) {
            if (this.level <= this.levels.FATAL) {
                msg = DateTime.now().toLocaleString() + " FATAL   " + msg;
                this.trace(msg);

                console.error(msg);
            }
        },
        trace: function(msg) {
            if (this.enableTrace) {

                if (this.traceCounter == this.traceCounterMax) {
                    $('#trace span:first').remove();
                    this.traceCounter = (this.traceCounterMax - 1);
                }
                $('#trace').append('<span>' + msg + '<br/></span>');
                this.traceCounter++;
            }
        }
    };

    window.logger = logger;

    return logger;
});
rjs.define('system/io/FileSystem',["properties/App.Settings"], function(Settings) {
    /**
     * [FileSystem description]
     */
    function FileSystem() {
        var self = this;

        /**
         * Initialize IndexedDB database (Deferred method)
         * @param  {[type]} opts [description]
         * @return {[type]}      [description]
         */
        self.initialize = function(opts) {
            var dfd = $.Deferred();

            setTimeout(function() {
                dfd.resolve();
            }, 50);

            return dfd;
        };

    }

    var fs = new FileSystem();
    return fs;
});
rjs.define('system/io/Directory',["system/io/FileSystem"], function(FileSystem) {
    /**
     * [Directory description]
     */
    function Directory() {
        var self = this;
        const fs = require('fs');
        const device_info = new BSDeviceInfo();
        const storagePath = device_info.model.toUpperCase() === "LGUV5N" ? "flash" : "sd";
        const rimraf = require(`/storage/${storagePath}/node_modules/rimraf`);

        /**
         * [exists description]
         * @param  {[type]} path [description]
         * @return {[type]}      [description]
         */
        self.exists = function(path) {
            return false;
        };

        /**
         * [createDirectory description]
         * @param  {[type]} path [description]
         * @return {[type]}      [description]
         */
        self.createDirectory = function(path) {
            var dfd = $.Deferred();

            fs.mkdir(path, (err) => {
                if (err && err.errno !== -17) { // directory already exists
                    logger.error(`CREATE DIR: Error: ${JSON.stringify(err, undefined, 2)}`);
                    dfd.reject();
                } else {
                    logger.debug(`CREATE DIR: ${path} successful`);
                    dfd.resolve();
                }
            });

            return dfd;
        };

        // Allow us to delete a widget directory
        self.deleteDirectory = function(path) {
            var dfd = $.Deferred();

            // fs.rmdir(path, (err) => {
            //     if (err) {
            //         logger.error(`DEL DIR: Error: ${JSON.stringify(err, undefined, 2)}`);
            //         dfd.reject();
            //     } else {
            //         logger.debug(`DEL DIR: ${path} successful`);
            //         dfd.resolve();
            //     }
            // });

            rimraf(path, (err) => {
                if (err) {
                    logger.error(`DEL DIR: Error: ${JSON.stringify(err, undefined, 2)}`);
                    dfd.reject();
                } else {
                    logger.debug(`DEL DIR: ${path} successful`);
                    dfd.resolve();
                }
            });


            return dfd;
        };

        self.listFiles = function(path) {
            var dfd = $.Deferred();

            logger.debug('List files for: ' + path);
            var files = [];

            fs.readdir(path, (err, files) => {
                if (err) {
                    logger.error(JSON.stringify(err, undefined, 2));
                    dfd.reject();
                } else {
                    files.forEach(file => {
                        files.push(file);
                    });

                    dfd.resolve(files);
                }
            });

            return dfd;
        };

        /**
         * [delete description]
         * @param  {[type]} path [description]
         * @return {[type]}      [description]
         */
        self.delete = function(path) {
            return false;
        };

        /**
         * [getFiles description]
         * @param  {[type]} path [description]
         * @return {[type]}      [description]
         */
        self.getFiles = function(path) {

        };
    }

    return new Directory();
});
rjs.define('system/io/File',["system/io/FileSystem", "system/io/Directory", "properties/App.Settings"], function(FileSystem, Directory, Settings) {
    function File() {
        const device_info = new BSDeviceInfo();
        const storagePath = device_info.model.toUpperCase() === "LGUV5N" ? "flash" : "sd";
        
        const fs = require(`/storage/${storagePath}/node_modules/graceful-fs`);
        const CRC32Stream = require(`/storage/${storagePath}/node_modules/crc32-stream`).CRC32Stream;
        const yauzl = require(`/storage/${storagePath}/node_modules/yauzl`);
        const path = require('path');

        // CREATE FILE
        var self = this;
        self.create = function(path) {
            logger.debug(`FILE.CREATE: File: ${path}`);
            var dfd = $.Deferred();

            self.write(path, '').done(() => {
                dfd.resolve();
            }).fail(() => {
                dfd.reject();
            });

            return dfd;
        };

        // WRITE FILE
        self.write = function(path, data) {
            logger.debug(`FILE.WRITE: ${path}`);
            var dfd = $.Deferred();

            fs.open(path, 'w', (err, fd) => {
                if (err) {
                    logger.error(`FILE.WRITE: Error opening file for writing: ${JSON.stringify(err, undefined, 2)}`);
                    dfd.reject();
                } else {

                    fs.writeFile(fd, data, (err) => {
                        if (err) {
                            logger.error(`FILE.WRITE: Error: ${JSON.stringify(err, undefined, 2)}`);
                            dfd.reject();
                        } else {
                            logger.debug(`FILE.WRITE: ${path} written successfully`);       
                            //sync to the file system
                            fs.fdatasyncSync(fd);

                            fs.close(fd, (err) => {
                                //Error closing file??
                                dfd.resolve();
                            });
                        }    
                    });
                }
            });

            return dfd;
        };

        // READ FILE
        self.read = function(path) {
            logger.debug(`FILE.READ: ${path}`);
            var dfd = $.Deferred();

            fs.readFile(path, (err, data) => {
                if (err) {
                    
                    if (err.code == 'ENOENT') {
                        logger.info("File not found");
                    } else {
                        logger.error(`FILE.READ: Error: ${JSON.stringify(err, undefined, 2)}`);
                    }

                    dfd.reject();
                } else {
                    logger.debug(`FILE.READ: ${path} success`);
                    dfd.resolve(data);
                }
            });

            return dfd;
        };

        self.copy = function(source, dest) {
            logger.debug(`FILE.COPY: ${source} to ${dest}`);
            var dfd = $.Deferred();

            // Setup streams and handle events
            var rs = fs.createReadStream(source)
                .on("error", (err) => {
                    if (err.code == 'ENOENT') {
                        logger.info(`FILE.COPY: File does not exist: ${JSON.stringify(err, undefined, 2)}`);
                    } else {
                        logger.error(`FILE.COPY: Error: ${JSON.stringify(err, undefined, 2)}`);
                    }
                    dfd.reject();
                });

            var ws = fs.createWriteStream(dest)
                .on("error", () => {
                    logger.error(`FILE.COPY: Error: ${JSON.stringify(err, undefined, 2)}`);
                    dfd.reject();
                })
                .on("close", () => {
                    logger.debug(`FILE.COPY: Success`);
                    dfd.resolve();
                });

            // Pipe read stream to write stream
            rs.pipe(ws);

            return dfd;
        };

        // MOVE FILE
        self.move = function(source, dest) {
            logger.debug(`FILE.MOVE: ${source} to ${dest}`);
            var dfd = $.Deferred();

            self.copy(source, dest)
            .done(function() {
                self.delete(source)
                    .done(function() {
                        logger.debug("FILE.MOVE: Success.");
                        dfd.resolve();
                    }).fail(function() {
                        logger.error("FILE.MOVE: Could not delete source file.");
                        dfd.reject();
                    });
            }).fail(function() {
                logger.error("Unable to copy file. This is likely because the file does not exist.");
                dfd.resolve();
            });

            return dfd;
        };

        // APPEND TO FILE
        self.append = function(path, data) {
            var dfd = $.Deferred();

            logger.debug(`FILE.APPEND: ${path}`);
            var dfd = $.Deferred();

            fs.appendFile(path, data, (err) => {
                if (err) {
                    logger.error(`FILE.APPEND: Error: ${JSON.stringify(err, undefined, 2)}`);
                    dfd.reject();
                } else {

                    logger.debug(`FILE.APPEND: ${path} written successfully`);
                    dfd.resolve();

                }
            });


            return dfd;
        };

        // CHECK FILE EXISTS
        //To-Do - make async at some stage
        self.exists = function(path) {
            var result = false;

            try {
                var stats = fs.statSync(path);

                if(stats.isFile())
                    result = true;                

            } catch (err) {
                result = false;
            }

            return result;
        };

        self.addToLocalFileList = function(path) {
            logger.info("addToLocalFileList is deprecated");
            return true;
        };

        // CHECK FILE EXISTS - using fs.stat - used to check for start file in widgets
        self.existsFull = function(path) {
            var dfd = $.Deferred();
            fs.stat(path, function(err, stat) {
                if (err == null) {
                    logger.debug('File exists');
                    dfd.resolve(true);
                } else if (err.code == 'ENOENT') {
                    // file does not exist
                    logger.debug('File does not exist');
                    dfd.resolve(false);
                } else {

                    dfd.reject();
                }
            });
            return dfd;
        };


        // DELETE FILE
        self.delete = function(path) {
            var dfd = $.Deferred();

            fs.unlink(path, (err) => {
                if (err) {
                    //Error deleting file - likely it does not exist
                    logger.warning(`FILE.DELETE: Error: ${JSON.stringify(err, undefined, 2)}`);
                    dfd.reject();
                } else {
                    logger.debug(`FILE.DELETE: ${path} success`);
                    dfd.resolve();
                }
            });

            return dfd;
        };

        // Yauzl-based unzip function for cross-firmware compatibility
        self.unzip = function (zipPath, targetPath) {
        console.log("Unzip: " + zipPath + " to: " + targetPath);
        var dfd = $.Deferred();
        var extractedFiles = [];

        yauzl.open(zipPath, {lazyEntries: true}, function(err, zipfile) {
            if (err) {
            dfd.reject(err);
            return;
            }

            zipfile.readEntry();
            zipfile.on("entry", function(entry) {
            if (/\/$/.test(entry.fileName)) {
                zipfile.readEntry();
            } else {
                zipfile.openReadStream(entry, function(err, readStream) {
                if (err) {
                    dfd.reject(err);
                    return;
                }
                
                var filePath = path.join(targetPath, entry.fileName);
                var dirName = path.dirname(filePath);
                
                // Create parent directory if file is in a subdirectory
                fs.mkdir(dirName, {recursive: true}, function(err) {
                    if (err && err.code !== 'EEXIST') {
                    console.error("Error creating directory:", err);
                    }
                    
                    var writeStream = fs.createWriteStream(filePath);
                    readStream.pipe(writeStream);
                    
                    writeStream.on('close', function() {
                    console.log("Extracted: " + entry.fileName);
                    extractedFiles.push({path: entry.fileName});
                    zipfile.readEntry();
                    });
                    
                    writeStream.on('error', function(err) {
                    console.error("Error writing file:", err);
                    zipfile.readEntry(); // Continue to next entry even on error
                    });
                });
                });
            }
            });
            
            zipfile.on("end", function() {
            console.log("Files unzipped!");
            dfd.resolve(extractedFiles);
            });
            
            zipfile.on("error", function(err) {
            dfd.reject(err);
            });
        });
        
        return dfd;
        };


        self.deleteLocal = function(path) {
            if (path.startsWith('/')) {
                path = path.substring(1);
            }

            self.delete(path);
        };

        // Reset PoP
        self.resetProofOfPlay = function(sourceLogURI, uploadLogURI) {
            var dfd = $.Deferred();
            logger.info("PoP: Reset Proof of Play");
            logger.info("PoP: Checking if upload log already exists");

            if (sourceLogURI.startsWith('/')) {
                sourceLogURI = sourceLogURI.substring(1);
            }

            if (uploadLogURI.startsWith('/')) {
                uploadLogURI = uploadLogURI.substring(1);
            }

            try {
                self.existsFull(uploadLogURI).done(function(uploadLogExists) {
                    if (uploadLogExists) {
                        logger.info("PoP: Upload log already exists. Appending new log data to existing upload log file.");
                        self.read(sourceLogURI)
                            .done(function(sourcePoPReport) {
                                logger.info("PoP: Source report data loaded: " + sourcePoPReport);
                                self.append(uploadLogURI, sourcePoPReport)
                                    .done(function() {
                                        logger.info("PoP: Data appended to existing upload log successfully");
                                        self.delete(sourceLogURI)
                                            .done(function() {
                                                logger.info("PoP: Source log deleted.");
                                                logger.info("PoP: Successfully reset proof of play.");
                                                dfd.resolve();
                                            }).fail(function() {
                                                logger.error("PoP: Could not delete file PoP source log.");
                                                dfd.reject();
                                            });
                                    })
                                    .fail(function() {
                                        logger.error("PoP: Unable to append new data to upload log file.");
                                        dfd.reject();
                                    });
                            })
                            .fail(function() {
                                logger.error("PoP: Unable to read data from source log file.");
                                dfd.reject();
                            });
                    } else {
                        logger.info("PoP: Does not exist. Copying current log to create new upload log.");
                        self.copy(sourceLogURI, uploadLogURI)
                            .done(function() {
                                logger.info("PoP: Upload log created.");
                                self.delete(sourceLogURI)
                                    .done(function() {
                                        logger.info("PoP: Source log deleted.");
                                        logger.info("PoP: Successfully reset proof of play.");
                                        dfd.resolve();
                                    }).fail(function() {
                                        logger.error("PoP: Could not delete file PoP source log.");
                                        dfd.reject();
                                    });
                            }).fail(function() {
                                logger.warning("Unable to copy PoP data to upload file while attempting to reset proof of play. This is likely because no PoP data has been stored.");
                                dfd.resolve();
                            });
                    }
                });
            } catch (error) {
                logger.error("PoP: Exception thrown resetting PoP: " + error);
                dfd.reject();
            }
            return dfd;
        };

        //CRC32
        self.crc32 = function (filePath) {
            logger.debug('Get CRC32 for file: ' + filePath);
            var dfd = $.Deferred();
        
            const source = fs.createReadStream(filePath);
            const checksum = new CRC32Stream();
                
            checksum.on('end', function(err) 
            {
                dfd.resolve(checksum.hex());
                source.close();
            });
                
            // either pipe it
            source.pipe(checksum);
            checksum.resume();
           
            return dfd;
        };
    }

    var file = new File();
    return file;
});
rjs.define('system/plugins/DownloadPlugin',[], function() {
    function DownloadPlugin() {
        var self = this;
        const device_info = new BSDeviceInfo();
        const storagePath = device_info.model.toUpperCase() === "LGUV5N" ? "flash" : "sd";
        
        const fs = require(`/storage/${storagePath}/node_modules/graceful-fs`);

        // Download file - Uses Fetch API streaming
        self.downloadFile = function(remote, local) {
            logger.debug(`DOWNLOAD: ${remote} to ${local}`);
            var dfd = $.Deferred();

            try {
                // Perform the request and consume response stream
                var ws = fs.createWriteStream(local);
                fetch(remote).then(response => {
                        // If response is for example a 500 then response.ok will be false
                        // Throw an error so we can catch, log and reject the error
                        if (!response.ok) {
                            throw Error(response.statusText);
                        }

                        // If response is OK then we can start to consume the stream
                        return consume(ws, response.body.getReader(), dfd, remote, local);
                    })
                    .catch((err) => {
                        logger.error('Error downloading file: ' + err);
                        ws.end();
                        dfd.reject(remote, local);
                    });

            } catch (e) {
                logger.error(JSON.stringify(e, undefined, 2));
                dfd.reject(remote, local);
            }

            return dfd;
        };
    }

    const consume = (ws, responseReader, dfd, remote, local) => {
        return responseReader.read().then(result => {
            // Done streaming?
            if (result.done) {

                ws.end(); // Close stream

      
                 dfd.resolve(remote, local);
                // Not done
            } else {
                // We must convert the UInt8Array returned by ReadableStream to a Node Buffer (which is available globally)
                var buffer = null;
                var uInt8Array = result.value;
                const device_info = new BSDeviceInfo();
                const storagePath = device_info.model.toUpperCase() === "LGUV5N" ? "flash" : "sd";
                const semver = require(`/storage/${storagePath}/node_modules/semver`);

                //Firmware 8+ uses a higher node version so we need to use Buffer.from
                if(semver.gte(process.version,"10.0.0"))
                {  
                    buffer = Buffer.from(uInt8Array);
                } else {
                    buffer = new Buffer(uInt8Array);
                }


                // Wrire buffer to file using our write stream
                ws.write(buffer, (err) => {
                    uInt8Array = null;
                    buffer = null;
                    // Error - reject
                    if (err) {
                        logger.error(err);
                        ws.end();
                        dfd.reject(remote, local);
                    }
                    // No error - continue to consume data and write to file
                    else {
                        return consume(ws, responseReader, dfd, remote, local);
                    }
                });
            }
        });
    };

    return DownloadPlugin;
});
rjs.define('system/network/NetworkInterface',["properties/App.Settings", "system/plugins/DownloadPlugin", "system/io/File"], (Settings, DownloadPlugin, File) => {
	/*
		Network Interface - Provides access to Network information
	*/
	function NetworkInterface() {
		var self = this;
		var cachedNetworkData = null;
		var dataCachedTime = null;
		var APIInterface = null;
        var networkChangeRequired = false;

	    //var NetworkConfigClass = require("@brightsign/networkconfiguration");


		self.registerAPIInterface = function(interface) {
			APIInterface = interface;
		};

		function getNetworkData() {
			var dfd = $.Deferred();
			var now = new Date().getTime();

			//If we haven't gotten the data before OR it is over a minute old
			if (cachedNetworkData == null || (cachedNetworkData != null && now - dataCachedTime > 60000)) {
				$.ajax({
					url: "http://localhost:8080/get-network-info",
					type: "GET",
					//dataType: "xml",
					async: true,
				})
					.done(function (data, textStatus, jqXHR) {
						var networkData = JSON.parse(data);
						logger.debug(data);

						dataCachedTime = new Date().getTime();

						var registryClass = require("@brightsign/registry");
						var registry = new registryClass();

						registry.read("networking").then(function (reg) {
							if (reg.use_wifi === "true") {
								cachedNetworkData = networkData.wifi;
								dfd.resolve(networkData.wifi);
							} else {
								cachedNetworkData = networkData.ethernet;
								dfd.resolve(networkData.ethernet);
							}
						});
					})
					.fail(function (jqXHR, textStatus, errorThrown) {
						dfd.resolve(null);
					});
			} else {
				setTimeout(function () {
					dfd.resolve(cachedNetworkData);
				}, 100);
			}

			return dfd;
		}
		
		function wifiDataIsValid(wifiData)
		{
			var isValid = true;

			if(wifiData.enabled==false)
				isValid = false;
			else if (wifiData.ssid === undefined || wifiData.ssid === null)
				isValid = false;
			else if (wifiData.ipaddress === undefined || wifiData.ipaddress === null)
				isValid = false;
			else if (wifiData.ipaddress == "")
				isValid = false;

			return isValid;
		}

		// To be Implemented
		self.getMAC = function() {
			var dfd = $.Deferred();

			getNetworkData().done(function(data)
			{
				dfd.resolve(data.macaddress);
			});

			return dfd;

		};

		// To be Implemented
		self.getActiveNetworkConnectionType = function() {

			var dfd = $.Deferred();

			getNetworkData().done(function(data)
			{
				if (data.ssid === undefined || data.ssid === null) {
					dfd.resolve("ethernet");
				} else {
					dfd.resolve("wifi");
				}
 			});

			return dfd;
		};

		// To be Implemented
		self.getIP = function() {
			var dfd = $.Deferred();

			getNetworkData().done(function(data)
			{
				dfd.resolve(data.ipaddress);		
			});

			return dfd;
		};

		// To be Implemented
		self.getSubnetMask = function() {
			var dfd = $.Deferred();

			getNetworkData().done(function(data)
			{
				dfd.resolve(data.subnetmask);		
			});

			return dfd;
		};

		// To be Implemented
		self.getDefaultGateway = function() {
			var dfd = $.Deferred();

			getNetworkData().done(function(data)
			{
				dfd.resolve(data.defaultgateway);		
			});

			return dfd;
		};

		// To be Implemented
		self.getDNS = function() {
			var dfd = $.Deferred();
			
			getNetworkData().done(function(data)
			{
				dfd.resolve(data.dnsservers[0]);	
			});

			return dfd;
		};

		// To be Implemented
		self.isDHCP = function() {
			var dfd = $.Deferred();
		
			getNetworkData().done(function(data)
			{
				dfd.resolve(data.dhcp);		
			});

			return dfd;
		};	

        // Some custom network configuration values are not returned as part of the returned BS object in the get-network-info API call,
        // so here we are storing them persistently using the registry, so we can compare against the incoming network.json to check for changes.
        self.updateRegistry = function (registrySection, keysToCheck, networkConfig, bsNetworkData) {
			var dfd = $.Deferred();
			var registryClass = require("@brightsign/registry");
			var registry = new registryClass();
			var registryUpdates = {};

			// Helper function to convert camelCase to snake case (in keeping with BrightSign registry key format)
			function camelCaseToSnakeCase(key) {
				if (key === "wifiSSID") { // Special case for "wifiSSID"
					return "wifi_ssid";
				} else {
					return key.replace(/([A-Z])/g, "_$1").toLowerCase();
				}
			}

			// Helper function to convert booleans to strings and vice versa (BrightSign registry won't accept writing of bools)
			function toggleBoolsAndStrings(value) {
				if (typeof value === "boolean") {
					return value ? "true" : "false"; // Convert boolean to string
				} else if (typeof value === "string") {
					if (value.toLowerCase() === "true") {
						return true; // Convert string to boolean
					} else if (value.toLowerCase() === "false") {
						return false; // Convert string to boolean
					} else {
						return value; // Return the string as is
					}
				} else {
					return value; // Return the value as is
				}
			}

			registry
				.read(registrySection)
				.then(function (reg) {
					// Loop through the keys and compare the registry with the incoming config
					keysToCheck.forEach(function (key) {
						var registryKey = camelCaseToSnakeCase(key);
						var configValue = toggleBoolsAndStrings(networkConfig[key]);
						var registryValue = reg[registryKey];

						// Add the current registry values onto the data object as they are, so we can use for later comparisons
						bsNetworkData[key] = toggleBoolsAndStrings(reg[registryKey]);

						// Compare values and prepare for update if different.
						/* Note: using the loose inequality operator '!=' ensures we catch all value changes EXCEPT where:
						 ** The key is not found in the registry (undefined), and the incoming value is 'null'.
						 ** This is ideal as we don't want that case to be considered as a change.
						 ** Note: Setting a reg value to 'null' just serves to delete the entry anyway. see: https://brightsign.atlassian.net/wiki/spaces/DOC/pages/370678545/registry
						 */
						if (registryValue != configValue) {
							registryUpdates[registryKey] = configValue; // Update with coerced value
							logger.debug(`Updating '${key}' value (registry key: '${registryKey}')`);
							networkChangeRequired = true; // Set the flag to true if an update is needed
						} else {
							logger.debug(
								`Stored '${key}' value (registry key: '${registryKey}') and incoming value are the same. No registry update required.`
							);
						}
					});

					// If there are updates, write and flush them to the registry
					if (Object.keys(registryUpdates).length > 0) {
						registry
							.write(registrySection, registryUpdates)
							.then(function () {
								logger.debug("Write Successful");
								registry
									.flush()
									.then(function () {
										logger.debug("Registry flushed successfully");
										dfd.resolve(); // Resolve when done
									})
									.catch(function (error) {
										console.error("Error flushing registry:", error);
										dfd.reject(error);
									});
							})
							.catch(function (error) {
								console.error("Error writing to registry:", error);
								dfd.reject(error);
							});
					} else {
						logger.debug("No updates needed.");
						dfd.resolve(); // Resolve if no updates are needed
					}
				})
				.catch(function (error) {
					console.error("Error reading registry:", error);
					dfd.reject(error);
				});

			return dfd;
		};
        

        self.SetNetworkingConfiguration = function(networkConfig) {
            var dfd = $.Deferred();
        
            if (networkConfig.networkConfigured == false) {
                setTimeout(function() {
                    logger.info("Network not configured");
                    dfd.resolve();
                }, 100);
            } else {
                // Configure the networking
                getNetworkData().done(function(data) {
                    var downloadCertificateDfd = $.Deferred();
                    var customNetworkConfigParams = [
                        "use8021x",
                        "wifiIdentity",
                        "wifiPassphrase",
                        "pkcsPass",
                        "x509Pass",
                        "useWifi",
                        "wifiSSID",
                    ];

                    self.updateRegistry("networking", customNetworkConfigParams, networkConfig, data).done(function() {
                    
                        if (networkConfig.use8021x) { // We want to use 802.1x auth
                            if (networkConfig.certType === "PKCS#12" || networkConfig.certType === "X.509") {                 // We want to use 802.1x with a client cert.

                                var saveLocation = Settings.application.fileSystem.storageDirectory + "/";                          // Base filepath for saving the certificates and keys.
                                var remoteCertURL = networkConfig.remoteCertUrl;                                              // String representing a URL to download the certificate from.
                                var certFileName = networkConfig.certFileName;                                                // File name to use when saving the certificate.
                                var certFilePath = saveLocation + certFileName;                                               // Full local filepath for the certificate e.g. "testuser-UsernameCn-client-signed-cert.crt".
                                var certFileNeeded = !File.exists(certFilePath);                                              // Indicates whether the certificate is present on the SD root.

                                if (networkConfig.certType === "X.509") {
                                    var remoteKeyURL = networkConfig.remoteKeyUrl;                                            // String representing a URL to download the key from.
                                    var keyFileName = networkConfig.keyFileName;                                              // File name to use when saving the X.509 key.
                                    var keyFilePath = saveLocation + keyFileName;                                             // Full local filepath for the key e.g. "testuser-UsernameCn-client-signed-key.txt".
                                    var keyFileNeeded = (networkConfig.certType === "X.509" && !File.exists(keyFilePath));    // We're using X.509, but don't have the key.
                                }
                
                                if (!certFileNeeded && !keyFileNeeded) { // We've got the certificate and key (if needed)
                                    downloadCertificateDfd.resolve();
                                } else if (certFileNeeded && !keyFileNeeded) { // We're just missing the certificate
                                    downloadFile(remoteCertURL, certFilePath)
                                    .then(function() {
                                        logger.debug("Successfully downloaded certificate");
                                        networkChangeRequired = true;
                                        downloadCertificateDfd.resolve();
                                    })
                                    .fail(function(e) {
                                        logger.debug("Failed to download certificate: " + e.message + ". Retrying...");
                                        setTimeout(function() {
                                            self.SetNetworkingConfiguration(networkConfig);
                                            return;
                                        }, 5000);
                                    });
                                } else if (!certFileNeeded && keyFileNeeded) { // We've got the certificate, we just need the key
                                    downloadFile(remoteKeyURL, keyFilePath)
                                    .then(function() {
                                        logger.debug("Successfully downloaded key");
                                        networkChangeRequired = true;
                                        downloadCertificateDfd.resolve();
                                    })
                                    .fail(function(e) {
                                        logger.debug("Failed to download key: " + e.message + ". Retrying...");
                                        setTimeout(function() {
                                            self.SetNetworkingConfiguration(networkConfig);
                                            return;
                                        }, 5000);
                                    });
                                } else {
                                    downloadFile(remoteCertURL, certFilePath)
                                    .then(function() {
                                        return downloadFile(remoteKeyURL, keyFilePath);
                                    })
                                    .then(function() {
                                        logger.debug("Successfully downloaded certificate");
                                        networkChangeRequired = true;
                                        downloadCertificateDfd.resolve();
                                    })
                                    .fail(function(e) {
                                        logger.debug("Failed to download certificate: " + e.message + ". Retrying...");
                                        setTimeout(function() {
                                            self.SetNetworkingConfiguration(networkConfig);
                                            return;
                                        }, 5000);
                                    });
                                }
                            } else { // We want to use 802.1x without a client cert (PEAP/MS-CHAP).
                                logger.debug("Using 802.1x with PEAP/MS-CHAPv2");
                                downloadCertificateDfd.resolve();
                            }
                        } else { // We don't want to use 802.1x auth
                            downloadCertificateDfd.resolve();
                        }
            
                        downloadCertificateDfd.then(function() {
                            if (networkConfig.timeServerUrl.includes("time.brightsignetwork.com")) {
                                networkConfig.timeServerUrl = networkConfig.timeServerUrl.replace(
                                    "time.brightsignetwork.com",
                                    "time.brightsignnetwork.com"
                                );
                            }

                            if (data.useWifi) { // Wifi is currently in use...
                                if (!networkConfig.useWifi) { // ...and we want to switch to using Ethernet
                                    networkChangeRequired = true;
                                } else if (data.wifiSSID != networkConfig.wifiSSID) { // We want to continue using Wifi but use a different SSID...
                                        networkChangeRequired = true;
                                }
                            } else { // Ethernet is currently in use...
                                if (networkConfig.useWifi) { // ...and we want to switch to using Wifi
                                    networkChangeRequired = true;
                                }
                            }

                            if (networkConfig.useDHCP != data.dhcp) {
                                networkChangeRequired = true;
                            } else if (!networkConfig.useDHCP && networkConfig.ipAddress != data.ipaddress) {
                                networkChangeRequired = true;
                            } else if (!networkConfig.useDHCP && networkConfig.subnetMask != data.subnetmask) {
                                networkChangeRequired = true;
                            } else if (networkConfig.proxy != data.proxy) {
                                networkChangeRequired = true;
                            } else if (!networkConfig.useDHCP && networkConfig.defaultGateway != data.defaultgateway) {
                                networkChangeRequired = true;
                            } else if (networkConfig.timeServerUrl != data.timeserver) {
                                networkChangeRequired = true;
                            }
            
                            // To-Do - check DNS
                            if (networkChangeRequired) {
                                logger.debug("Network configuration change required");
            
                                logger.debug("Setting to :" + JSON.stringify(networkConfig));
            
                                $.ajax({
                                    traditional: true,
                                    url: "http://localhost:8080/set-network-settings",
                                    type: 'POST',
                                    contentType: "application/json",
                                    data: JSON.stringify(networkConfig),
                                    async: true
                                }).done(function(data, textStatus, jqXHR) {
                                    var networkChangeResult = JSON.parse(data);
            
                                    logger.debug("Network change result: " + networkChangeResult.result);
                                    cachedNetworkData = null;
            
                                    if (networkChangeResult.result)
                                        dfd.resolve();
                                    else
                                        dfd.reject();
            
                                }).fail(function(jqXHR, textStatus, errorThrown) {
                                    logger.debug("Network change request failed: " + textStatus);
                                    dfd.reject();
                                });
            
                            } else {
                                logger.debug("No network configuration change required");
                                dfd.resolve();
                            }
                        });
                    });
                });
            }
            return dfd;
        };

		self.getOnlineStatus = function() {
			var dfd = $.Deferred();
			var onlineStatus = {
				network_connected: window.navigator.onLine,
				internet_access: false
			};
			// When the player first starts up there is a delay starting the API Interface.
			// If this is null then set internet access to true and resolve 
			if (APIInterface == null) {
				onlineStatus.internet_access = onlineStatus.network_connected;
				return dfd.resolve(onlineStatus);
			}

			if (lastAPIConnectionIsWithinThePast5Minutes()) {
				onlineStatus.internet_access = true;
				return dfd.resolve(onlineStatus);
			}
			// Attempt a health check
			else {
				var healthCheckDfd = APIInterface.healthCheck();
				if (healthCheckDfd) {
					healthCheckDfd.always(function() {
						if (lastAPIConnectionIsWithinThePast5Minutes()) {
							onlineStatus.internet_access = true;
							return dfd.resolve(onlineStatus);
						}
					});
				}
				else {
					return dfd.resolve(onlineStatus);
				}
			}
		};

		function lastAPIConnectionIsWithinThePast5Minutes() {
			var currentTime = new Date();
			var threshold = new Date(currentTime.getTime() - 5*60000); // 5 minutes ago
			return APIInterface != null && APIInterface.lastConnectionTime != null && APIInterface.lastConnectionTime > threshold;
		}

        function downloadFile(remoteFileUrl, localFileUrl) {
            logger.debug("Downloading certificate file from: " + remoteFileUrl);

            var dfd = $.Deferred();

            new DownloadPlugin().downloadFile(remoteFileUrl, localFileUrl)
                .done(function (remoteFileUrl, localFileUrl) {
                    logger.debug("Download of " + remoteFileUrl + " successfull.");
                    logger.debug("Certificate saved to: " + localFileUrl);
                    dfd.resolve();
                })
                .fail(function () {
                    logger.debug("Error downloading certificate file");
                    try {
                        File.delete(localFileUrl);
                    } catch (e) {
                        console.error("Error whilst removing the update file: " + e.message);
                    }
                    dfd.reject("Failed to download update file");
                });
            return dfd;
        }
	}

	return new NetworkInterface();
});

rjs.define('models/NetworkModel',["properties/App.Settings"], function (Settings) {
	function NetworkModel(data) {
		var self = this;

		self.networkConfigured = false;
		self.useDHCP = null;
		self.ipAddress = null;
		self.subnetMask = null;
		self.defaultGateway = null;
		self.dns1 = null;
		self.dns2 = null;
		self.useWifi = null;
		self.proxy = null;
		self.wifiSSID = null;
		self.wifiIdentity = null;
		self.wifiPassphrase = null;
		self.wifiSecurityType = null;
		self.timeServerUrl = null;
        self.use8021x = null;
        self.certType = null;
        self.remoteCertUrl = null;
        self.certFileName = null;
        self.remoteKeyUrl = null;
        self.keyFileName = null;
        self.pkcsPass = null;
        self.x509Pass = null;

		if (data) {
			self.networkConfigured = true;
			self.useDHCP = data.use_dhcp;
			self.ipAddress = data.ipv4_address;
			self.subnetMask = data.subnet_mask;
			self.defaultGateway = data.default_gateway;
			self.dns1 = data.dns1;
			self.dns2 = data.dns2;
			self.useWifi = data.use_wifi;

			if (data.proxy_server) {
				self.proxy = data.proxy_server;
			} else {
				self.proxy = "";
			}

			self.wifiSSID = data.wifi_network_name;
			self.wifiPassphrase = data.wifi_pass_phrase;
			self.wifiIdentity = data.wifi_identity;
			self.wifiSecurityType = data.wifi_security_type;

			if (data.time_server) {
				self.timeServerUrl = data.time_server;
			} else {
				self.timeServerUrl = Settings.application.defaultNTPServer;
			}

			self.use8021x = data.use_8021x;
			self.certType = data.cert_type;
			self.remoteCertUrl = data.remote_cert_url;
			self.certFileName = data.cert_file_name;
			self.remoteKeyUrl = data.remote_key_url;
			self.keyFileName = data.key_file_name;
			self.pkcsPass = data.pkcs_pass;
			self.x509Pass = data.x509_pass;
		}
	}

	return NetworkModel;
});

rjs.define('system/power/PowerInterface',[], function() {
	/**
	 * PowerInterface provides access to low level power functions and information
	 */
	function PowerInterface() {
		var self = this;
		var screenStatus = "on";
        self.screenOn = true;
		        
        var systemClass = require("@brightsign/system");
        var system = new systemClass();
        
        var videoOutputClass = require("@brightsign/videooutput");
        var videoOutputHDMI;

        // LGUV5N does not support CEC?
        var device_info = new BSDeviceInfo();
        if (device_info.model.toUpperCase() !== "LGUV5N") {
            var cecClass = require("@brightsign/cec");
            var cec = new cecClass();
        }

        self.switchScreenOn = function () {
            var dfd = $.Deferred();

            logger.debug("Switching screen on");

            setPowerSaveMode(false).then(function() {
                try {
                    var buffer = [];
                    buffer[0] = 0x40;
                    buffer[1] = 0x0D;

                    cec.send(buffer)
                    .then(function() {
                        console.log("PowerInterface - Successfully switched screen on");
                        dfd.resolve();
                    })
                    .catch (function(error) {
                        // This seems to fire with an error code of 133 even though it is successful?
                        console.log("PowerInterface - Error turning screen on: " + JSON.stringify(error));
                        dfd.resolve();
                    });

                    screenStatus = "on";
                    self.screenOn = true;
                } catch (e) {
                    // CEC Transmitter Not Implemented
                }

                dfd.resolve();
            });

            return dfd;
        };

        self.switchScreenOff = function () {
            var dfd = $.Deferred();

            logger.debug("Switching screen off");

            setPowerSaveMode(true).then(function() {
                try {
                    var buffer = [];
                    buffer[0] = 0x40;
                    buffer[1] = 0x36;

                    cec.send(buffer)
                    .then(function() {
                        console.log("PowerInterface - Successfully switched screen off");
                        dfd.resolve();
                    })
                    .catch (function(error) {
                        // This seems to fire with an error code of 133 even though it is successful?
                        console.log("PowerInterface - Error turning screen off: " + JSON.stringify(error));
                        dfd.resolve();
                    });

                    screenStatus = "off";
                    self.screenOn = false;
                } catch (e) {
                    // CEC Transmitter Not Implemented
                }

                dfd.resolve();
            });

            return dfd;
        };

        function setPowerSaveMode(powerSetting) {
            var dfd = $.Deferred();
            // Get model specific details so we know how many connected outputs are available
            var device_info = new BSDeviceInfo();
            // TODO: Extend this to only iterate over enabled HDMI displays 
            switch(device_info.model.toLowerCase())
            {
                case "xc2055":
                case "xt2145":
                    videoOutputHDMI = [new videoOutputClass("hdmi-1"), new videoOutputClass("hdmi-2")];
                    break;
                case "xc4055":
                    videoOutputHDMI = [new videoOutputClass("hdmi-1"), new videoOutputClass("hdmi-2"), new videoOutputClass("hdmi-3"), new videoOutputClass("hdmi-4")];
                    break;
                default:
                    // Assume single output device
                    videoOutputHDMI = [new videoOutputClass("hdmi")];
                    break;
            }

            videoOutputHDMI.forEach(function(output) {
                output.setPowerSaveMode(powerSetting);
            });
            
            setTimeout(function() {
                dfd.resolve();
            }, 250);
            return dfd;
        };

        // To be Implemented
        self.getScreenOnOffStatus = function() {
            var dfd = $.Deferred();

            setTimeout(function() {
                dfd.resolve(screenStatus);
            }, 100);

            return dfd;
        };

        self.reboot = function() {
            var dfd = $.Deferred();

            try {
                system.reboot();
                dfd.resolve();
            } catch (err) {
                logger.error("Error rebooting: " + err.message);
                dfd.reject();
            }

            return dfd;
        };
    }

    // The object is a singleton as there is only a single plugin added to the application
    return new PowerInterface();
});
rjs.define('models/TimeZoneCollection',[],function() {
	var timezones = [{
		displayName : '(GMT-12:00) International Date Line West',
		bsid : 'GMT-12',
		id : 'Etc/GMT+12'
	}, {
		displayName : '(GMT-11:00) Midway Island, Samoa',
		bsid : 'SST',
		id : 'Pacific/Apia'
	}, {
		displayName : '(GMT-10:00) Hawaii',
		bsid : 'HST',
		id : 'Pacific/Honolulu'
	}, {
		displayName : '(GMT-09:00) Alaska',
		bsid : 'AKST',
		id : 'America/Anchorage'
	}, {
		displayName : '(GMT-08:00) Tijuana, Baja California',
		bsid : 'PST2',
		id : 'America/Santa_Isabel'
	}, {
		displayName : '(GMT-08:00) Pacific Time (US and Canada)',
		bsid : 'PST',
		id : 'America/Los_Angeles'
	}, {
		displayName : '(GMT-07:00) Arizona',
		bsid : 'MST1',
		id : 'America/Phoenix'
	}, {
		displayName : '(GMT-07:00) Chihuahua, La Paz, Mazatlan',
		bsid : 'MST2',
		id : 'America/Chihuahua'
	}, {
		displayName : '(GMT-07:00) Mountain Time (US and Canada)',
		bsid : 'MST',
		id : 'America/Denver'
	}, {
		displayName : '(GMT-06:00) Central Time (US and Canada)',
		bsid : 'CST',
		id : 'America/Chicago'
	}, {
		displayName : '(GMT-06:00) Guadalajara, Mexico City, Monterrey',
		bsid : 'GMT-6',
		id : 'America/Mexico_City'
	}, {
		displayName : '(GMT-06:00) Central America',
		bsid : 'GMT-6',
		id : 'America/Guatemala'
	}, {
		displayName : '(GMT-06:00) Saskatchewan',
		bsid : 'GMT-6',
		id : 'America/Regina'
	}, {
		displayName : '(GMT-05:00) Eastern Time (US and Canada)',
		bsid : 'EST',
		id : 'America/New_York'
	}, {
		displayName : '(GMT-05:00) Bogota, Lima, Quito, Rio Branco',
		bsid : 'GMT-5',
		id : 'America/Bogota'
	}, {
		displayName : '(GMT-05:00) Indiana (East)',
		bsid : 'EST',
		id : 'America/Indiana/Indianapolis'
	}, {
		displayName : '(GMT-04:30) Caracas',
		bsid : 'GMT-4:30',
		id : 'America/Caracas'
	}, {
		displayName : '(GMT-04:00) Santiago',
		bsid : 'POSIX:CLT4CLST,M10.2.0/4:00,M3.2.0/3:00',
		id : 'America/Santiago'
	}, {
		displayName : '(GMT-04:00) La Paz',
		bsid : 'GMT-4',
		id : 'America/La_Paz'
	}, {
		displayName : '(GMT-04:00) Manaus',
		bsid : 'GMT-4',
		id : 'America/Cuiaba'
	}, {
		displayName : '(GMT-04:00) Atlantic Time (Canada)',
		bsid : 'AST',
		id : 'America/Halifax'
	}, {
		displayName : '(GMT-03:30) Newfoundland',
		bsid : 'NST',
		id : 'America/St_Johns'
	}, {
		displayName : '(GMT-03:00) Montevideo',
		bsid : 'GMT-3',
		id : 'America/Montevideo'
	}, {
		displayName : '(GMT-03:00) Georgetown',
		bsid : 'GMT-3',
		id : 'America/Cayenne'
	}, {
		displayName : '(GMT-03:00) Greenland',
		bsid : 'POSIX:WGT3WGST,M3.5.0/1:00,M10.5.0/1:00',
		id : 'America/Godthab'
	}, {
		displayName : '(GMT-03:00) Brasilia',
		bsid : 'BRT',
		id : 'America/Sao_Paulo'
	}, {
		displayName : '(GMT-03:00) Buenos Aires',
		bsid : 'GMT-3',
		id : 'America/Argentina/Buenos_Aires'
	}, {
		displayName : '(GMT-02:00) Mid-Atlantic',
		bsid : 'GMT-2',
		id : 'Etc/GMT+2'
	}, {
		displayName : '(GMT-01:00) Cape Verde Is.',
		bsid : 'GMT-1',
		id : 'Atlantic/Cape_Verde'
	}, {
		displayName : '(GMT-01:00) Azores',
		bsid : 'AZOT',
		id : 'Atlantic/Azores'
	}, {
		displayName : '(GMT) Monrovia, Reykjavik',
		bsid : 'WET',
		id : 'Atlantic/Reykjavik'
	}, {
		displayName : '(GMT) Casablanca',
		bsid : 'WET',
		id : 'Africa/Casablanca'
	}, {
		displayName : '(GMT) Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London',
		bsid : 'GMTBST',
		id : 'Europe/London'
	}, {
		displayName : '(GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna',
		bsid : 'CET',
		id : 'Europe/Berlin'
	}, {
		displayName : '(GMT+01:00) West Central Africa',
		bsid : 'GMT+1',
		id : 'Africa/Lagos'
	}, {
		displayName : '(GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague',
		bsid : 'CET',
		id : 'Europe/Budapest'
	}, {
		displayName : '(GMT+01:00) Brussels, Copenhagen, Madrid, Paris',
		bsid : 'CET',
		id : 'Europe/Paris'
	}, {
		displayName : '(GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb',
		bsid : 'CET',
		id : 'Europe/Warsaw'
	}, {
		displayName : '(GMT+02:00) Beirut',
		bsid : 'EET',
		id : 'Asia/Beirut'
	}, {
		displayName : '(GMT+02:00) Minsk',
		bsid : 'EET',
		id : 'Asia/Nicosia'
	}, {
		displayName : '(GMT+02:00) Harare, Pretoria',
		bsid : 'GMT+2',
		id : 'Africa/Johannesburg'
	}, {
		displayName : '(GMT+02:00) Amman',
		bsid : 'EET',
		id : 'Asia/Amman'
	}, {
		displayName : '(GMT+02:00) Jerusalem',
		bsid : 'POSIX:IST-2IDT,J91/1:00,J274/1:00',
		id : 'Asia/Jerusalem'
	}, {
		displayName : '(GMT+02:00) Windhoek',
		bsid : 'GMT+2',
		id : 'Africa/Windhoek'
	}, {
		displayName : '(GMT+02:00) Athens, Bucharest, Istanbul',
		bsid : 'EET',
		id : 'Europe/Bucharest'
	}, {
		displayName : '(GMT+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius',
		bsid : 'EET',
		id : 'Europe/Kiev'
	}, {
		displayName : '(GMT+02:00) Cairo',
		bsid : 'GMT+2',
		id : 'Africa/Cairo'
	}, {
		displayName : '(GMT+03:00) Moscow, St. Petersburg, Volgograd',
		bsid : 'MSK',
		id : 'Europe/Moscow'
	}, {
		displayName : '(GMT+03:00) Baghdad',
		bsid : 'GMT+3',
		id : 'Asia/Baghdad'
	}, {
		displayName : '(GMT+03:00) Tbilisi',
		bsid : 'GMT+3',
		id : 'Asia/Tbilisi'
	}, {
		displayName : '(GMT+03:00) Kuwait, Riyadh',
		bsid : 'GMT+3',
		id : 'Asia/Riyadh'
	}, {
		displayName : '(GMT+03:00) Nairobi',
		bsid : 'GMT+3',
		id : 'Africa/Nairobi'
	}, {
		displayName : '(GMT+03:30) Tehran',
		bsid : 'POSIX:IRT-3:30IRST,J79/0:00,J265/0:00',
		id : 'Asia/Tehran'
	}, {
		displayName : '(GMT+04:00) Port Louis',
		bsid : 'GMT+4',
		id : 'Indian/Mauritius'
	}, {
		displayName : '(GMT+04:00) Yerevan',
		bsid : 'GMT+4',
		id : 'Asia/Yerevan'
	}, {
		displayName : '(GMT+04:00) Abu Dhabi, Muscat',
		bsid : 'GMT+4',
		id : 'Asia/Dubai'
	}, {
		displayName : '(GMT+04:00) Baku',
		bsid : 'GMT+4',
		id : 'Asia/Baku'
	}, {
		displayName : '(GMT+04:30) Kabul',
		bsid : 'GMT+4:30',
		id : 'Asia/Kabul'
	}, {
		displayName : '(GMT+05:00) Islamabad, Karachi',
		bsid : 'GMT+5',
		id : 'Asia/Karachi'
	}, {
		displayName : '(GMT+05:00) Ekaterinburg',
		bsid : 'GMT+5',
		id : 'Asia/Yekaterinburg'
	}, {
		displayName : '(GMT+05:00) Tashkent',
		bsid : 'GMT+5',
		id : 'Asia/Tashkent'
	}, {
		displayName : '(GMT+05:30) Chennai, Kolkata, Mumbai, New Delhi',
		bsid : 'GMT+5:30',
		id : 'Asia/Kolkata'
	}, {
		displayName : '(GMT+05:30) Sri Jayawardenepura',
		bsid : 'GMT+5:30',
		id : 'Asia/Colombo'
	}, {
		displayName : '(GMT+05:45) Kathmandu',
		bsid : 'NPT',
		id : 'Asia/Kathmandu'
	}, {
		displayName : '(GMT+06:00) Astana, Dhaka',
		bsid : 'GMT+6',
		id : 'Asia/Almaty'
	}, {
		displayName : '(GMT+06:00) Almaty, Novosibirsk',
		bsid : 'GMT+6',
		id : 'Asia/Novosibirsk'
	}, {
		displayName : '(GMT+06:30) Yangon (Rangoon)',
		bsid : 'GMT+6:30',
		id : 'Asia/Rangoon'
	}, {
		displayName : '(GMT+07:00) Bangkok, Hanoi, Jakarta',
		bsid : 'GMT+7',
		id : 'Asia/Bangkok'
	}, {
		displayName : '(GMT+07:00) Krasnoyarsk',
		bsid : 'GMT+7',
		id : 'Asia/Krasnoyarsk'
	}, {
		displayName : '(GMT+08:00) Perth',
		bsid : 'AWST1',
		id : 'Australia/Perth'
	}, {
		displayName : '(GMT+08:00) Irkutsk, Ulaan Bataar',
		bsid : 'GMT+8',
		id : 'Asia/Irkutsk'
	}, {
		displayName : '(GMT+08:00) Kuala Lumpur, Singapore',
		bsid : 'GMT+8',
		id : 'Asia/Singapore'
	}, {
		displayName : '(GMT+08:00) Beijing, Chongqing, Hong Kong, Urumqi',
		bsid : 'GMT+8',
		id : 'Asia/Shanghai'
	}, {
		displayName : '(GMT+08:00) Taipei',
		bsid : 'GMT+8',
		id : 'Asia/Taipei'
	}, {
		displayName : '(GMT+09:00) Osaka, Sapporo, Tokyo',
		bsid : 'JST',
		id : 'Asia/Tokyo'
	}, {
		displayName : '(GMT+09:00) Yakutsk',
		bsid : 'GMT+9',
		id : 'Asia/Yakutsk'
	}, {
		displayName : '(GMT+09:00) Seoul',
		bsid : 'GMT+9',
		id : 'Asia/Seoul'
	}, {
		displayName : '(GMT+09:30) Darwin',
		bsid : 'ACST1',
		id : 'Australia/Darwin'
	}, {
		displayName : '(GMT+09:30) Adelaide',
		bsid : 'ACST',
		id : 'Australia/Adelaide'
	}, {
		displayName : '(GMT+10:00) Brisbane',
		bsid : 'AEST1',
		id : 'Australia/Brisbane'
	}, {
		displayName : '(GMT+10:00) Guam, Port Moresby',
		bsid : 'GMT+10',
		id : 'Pacific/Port_Moresby'
	}, {
		displayName : '(GMT+10:00) Canberra, Melbourne, Sydney',
		bsid : 'AEST',
		id : 'Australia/Sydney'
	}, {
		displayName : '(GMT+10:00) Hobart',
		bsid : 'AEST',
		id : 'Australia/Hobart'
	}, {
		displayName : '(GMT+10:00) Vladivostok',
		bsid : 'GMT+10',
		id : 'Asia/Vladivostok'
	}, {
		displayName : '(GMT+11:00) Magadan, Solomon Is., New Caledonia',
		bsid : 'GMT+11',
		id : 'Pacific/Guadalcanal'
	}, {
		displayName : '(GMT+12:00) Fiji, Kamchatka, Marshall Is.',
		bsid : 'CHAST',
		id : 'Pacific/Fiji'
	}, {
		displayName : '(GMT+12:00) Auckland, Wellington',
		bsid : 'NZST',
		id : 'Pacific/Auckland'
	}, {
		displayName : '(GMT+13:00) Nukualofa',
		bsid : 'GMT+13',
		id : 'Pacific/Tongatapu'
	}];
	
	return timezones.reverse();
});
rjs.define('system/SystemInitializer',["system/io/FileSystem", "properties/App.Settings", "system/io/File", "system/network/NetworkInterface", "models/NetworkModel", "system/DateTime", "system/power/PowerInterface", "models/TimeZoneCollection"], function(FileSystem, Settings, File, NetworkInterface, NetworkModel, DateTime, PowerInterface, TimeZoneCollection) {
    function SystemInitializer() {
        var self = this;

        self.initialize = function() {
            var dfd = $.Deferred();

            // Perform any initialization that needs to be done very early in the startup process here, such as file system initialization
            FileSystem.initialize()
                .done(function() {
                    logger.debug("Filesystem initialized");

                    var networkingDfd = $.Deferred();
                    //Load any networking information

                    if (File.exists(Settings.application.fileSystem.systemFiles.networkingFilePath)) {
                        File.read(Settings.application.fileSystem.systemFiles.networkingFilePath).done(function(networkConfigurationData) {
                            logger.info("System Initializer: Network configuration file present. Configuring network.");
                            var networkJSON = JSON.parse(networkConfigurationData);    
                            
                            var updateNetworkJson = false;

                            if (networkJSON.time_server.includes("time.brightsignetwork.com")) {
                                networkJSON.time_server = networkJSON.time_server.replace("time.brightsignetwork.com", "time.brightsignnetwork.com");
                                updateNetworkJson = true;
                            }

                            var currentConfiguration = new NetworkModel(networkJSON);
                            if (!currentConfiguration.timeServerUrl) {
                                currentConfiguration.timeServerUrl = Settings.application.defaultNTPServer; // No idea what the point of this code is?
                            }

                            // does the data file have the incorrect spelling for the time server?
                            if (updateNetworkJson) {
                                logger.info('network.json requires an update as the time server is incorrectly spelt')

                                // update network.json
                                File.write(Settings.application.fileSystem.systemFiles.networkingFilePath, JSON.stringify(networkJSON)).always(function() {
                                    logger.debug("Calling SetNetworkingConfiguration");

                                    NetworkInterface.SetNetworkingConfiguration(currentConfiguration).done(function() {
                                        logger.info("System Initializer: Network configured");
                                        networkingDfd.resolve();
                                    }).fail(function() {
                                        logger.info("System Initializer: Network configuration did not succeed.");
                                        networkingDfd.resolve();
                                    });
                                });
                            } else {
                                logger.info("Time server spelling fix is not required.");
                                
                                NetworkInterface.SetNetworkingConfiguration(currentConfiguration).done(function() {
                                    logger.info("System Initializer: Network configured");
                                    networkingDfd.resolve();
                                }).fail(function() {
                                    logger.info("System Initializer: Network configuration did not succeed.");
                                    networkingDfd.resolve();
                                });

                            }
                        }).fail(function() {
                            logger.info("System Initializer: No network configured.");
                            networkingDfd.resolve();
                        });
                    } else {
                        logger.info("System Initializer: No network configured.");
                        networkingDfd.resolve();
                    }

                    //Once we have checked for networking, we need to check the config to see if we need to reset the timezone
                    networkingDfd.always(function() {
                        File.read(Settings.application.fileSystem.systemFiles.settingsFilePath).done(function(settingsData) {

                            var settings = null;

                            settings = JSON.parse(settingsData);

                            if (settings != null) {
                                var configuredTimezone = settings.timeZone;

                                if (configuredTimezone.bsid) {

                                    $.ajax({
                                        traditional: true,
                                        url: "http://localhost:8080/get-timezone",
                                        type: 'GET',
                                        contentType: "text/plain",
                                        async: true
                                    }).done(function(data, textStatus, jqXHR) {
                                        logger.debug("Timezone received");

                                        var result = JSON.parse(data);
                                        logger.debug("Timezone result: " + data);

                                        if (result.result === configuredTimezone.bsid) {
                                            dfd.resolve();
                                        } else {
                                            DateTime.updateTimeZone(configuredTimezone).done(function() {
                                                //If timezone update we have to reboot.
                                                //Reboot
                                                PowerInterface.reboot();
                                            }).fail(function() {
                                                dfd.resolve();
                                            });
                                        }
                                    }).fail(function(jqXHR, textStatus, errorThrown) {
                                        logger.error("Error sending getting timezone from device: " + textStatus);
                                        dfd.resolve();
                                    });

                                } else {
                                    dfd.resolve();
                                }
                            } else {
                                dfd.resolve();
                            }
                        }).fail(function() {
                            dfd.resolve();
                        });
                    });


                });

            // Adds the capability to receive messages from brightscript
            // At the moment all we do is display them in the console.
            var bsMessage = new BSMessagePort();
            bsMessage.onbsmessage = function(msg) {
                $.each(msg.data, function(i, e) {
                    logger.debug('BRIGHTSIGN_EVENT_RECEIVED: ' + e);
                });
            };

            return dfd;
        };
    }

    return SystemInitializer;
});
rjs.define('properties/App.Properties',[],function() { 
return { 
	appName: "Signagelive",  
	appVersion:  "3.2",  
	appBuildNumber: 219   
};  
});  

rjs.define('system/io/FileInfo',["system/io/File"], function(File) {
    /**
     * A File Info class that also includes a method for deletion of the file
     * @param {[type]} path [description]
     */
    function FileInfo(path) {

        var self = this;
        self.path = path;
        self.name = '';

        // Get the filename
        if (self.path != null || self.path != undefined) {
            self.name = self.path.substring(self.path.lastIndexOf('/') + 1);
        }

        /**
         * Delete the file
         * @return {[type]} [description]
         */
        self.delete = function() {
            var dfd = $.Deferred();

            File.delete(self.path)
                .done(function() {
                    logger.debug("FileInfo: Deleted File: " + self.path);
                    dfd.resolve(true);
                })
                .fail(function() {
                    logger.error("FileInfo: Error: Unable to delete file: " + self.path);
                    dfd.reject();
                });

            return dfd;
        };
    }

    return FileInfo;
});
rjs.define('system/io/DirectoryInfo',["system/io/FileInfo","system/io/Directory"], function(FileInfo, Directory) {
    /**
     * Used to get information about a directory (including the files in it)
     * @param {[type]} path [description]
     */
    function DirectoryInfo(path) {
        const fs = require('fs');

        var self = this;
        self.path = path;

        self.directories = [];
        self.files = [];

        /**
         * Initialize the class so that it gets a full list of its files
         * @return {[type]} [description]
         */
        self.initialize = function() {

            var dfd = $.Deferred();

            fs.readdir(self.path, (err, files) => {
                if (err) {
                    logger.error(JSON.stringify(err, undefined, 2));
                    dfd.reject();
                } else {
                    files.forEach(file => {
                        var filePath = `${self.path}/${file}`;
                        self.files.push(new FileInfo(filePath));
                    });

                    dfd.resolve();
                }
            });

            return dfd;

        };

        self.createSubDirectory = function(name) {
			var fullPath = self.path + "/" + name;
			return Directory.createDirectory(fullPath);
		}

    }

    return DirectoryInfo;
});
rjs.define('models/ConnectivityModel',[],function() {
	function ConnectivityModel(data) {
		var self = this;
		self.configurationCheckTime = data.configuration_check_time;
		self.configurationCheckInterval = data.configuration_check_interval;

		// convert minutes to milliseconds
		if (self.configurationCheckInterval != null)
			self.configurationCheckInterval = data.configuration_check_interval * 60000;

		self.configurationIntervalScheme = data.configuration_interval_scheme;

		self.healthCheckInterval = Math.max(data.health_check_interval * 60000, 60000);

		self.diagnosticsUpdateTime = data.diagnostics_update_time;
		self.diagnosticsUpdateInterval = data.diagnostics_update_interval;
		
		// convert minutes to milliseconds
		if (self.diagnosticsUpdateInterval != null)
			self.diagnosticsUpdateInterval = data.diagnostics_update_interval * 60000;		
		
		self.diagnosticsUpdateIntervalScheme = data.diagnostics_interval_scheme;

		self.scheduledRebootsEnabled = data.scheduled_reboots_enabled;
		self.systemRebootTime = data.system_reboot_time;
		self.instantReboot = data.instant_reboot;

	}

	return ConnectivityModel;
});

rjs.define('models/MediaTypeConstants',[],function(){
	return {
		VIDEO:"video",
		IMAGE:"image",
		WEB:"web",
		IPTV:"iptv",
		CAPTURE:"capture",
		RSS:"rss",
		WIDGET:"widget",
		FEED:"feed"
	};
});
rjs.define('models/ScheduleModel',["system/DateTime"],function(DateTime) {
	function ScheduleModel(data) {
		var self = this;
		self.layoutId = data.layout_id;
		self.start = DateTime.parse(data.start);
		//TODO - do properly
		//self.end = self.start.addDays(90);
		self.end = DateTime.parse(data.end);
		self.layout = null;
	}

	return ScheduleModel;
});

rjs.define('models/ScheduledPlaylistModel',["system/DateTime"],function(DateTime) {
	function ScheduledPlaylistModel(data) {
		var self = this;
		self.externalId = data.id;
		self.playlistid = data.playlist_id;
		self.start = DateTime.parse(data.start);
		self.end = DateTime.parse(data.end);
		
		self.playlist = null;
	}

	return ScheduledPlaylistModel;
});

rjs.define('models/ZoneModel',["models/ScheduledPlaylistModel"],function(ScheduledPlaylistModel) {
	function ZoneModel(data, parentLayoutId) {
		var self = this;
		self.externalId = data.id;
		self.x = data.x;
		self.y = data.y;
		self.width = data.width;
		self.height = data.height;
		self.zIndex = data['z-index'];

		if (parentLayoutId !== undefined) {
			self.parentLayoutId = parentLayoutId;
		}
		
		if(self.zIndex === undefined)
		{
			self.zIndex = 0;
		}
		
		self.type = data.type;
		self.backgroundAssetId = data.background_asset_id;
		self.backgroundAsset = null;		
		
		self.includeInProofOfPlay = data.include_in_proof_of_play;
		
		self.seperatorAsset = null;
		
		self.configurationParameters = data.configuration_parameters;		

		self.defaultMediaAssetId = data.default_media_asset_id;
		self.defaultMediaAsset = null;
		self.defaultPlaylistId = data.default_playlist_id;
		self.defaultPlaylist = null;
		
		self.scheduledPlaylists = [];

		$(data.scheduled_playlists).each(function(index, value) {
			var scheduledPlaylist = new ScheduledPlaylistModel(value);
			self.scheduledPlaylists.push(scheduledPlaylist);
		});
	}

	return ZoneModel;
});

rjs.define('models/LayoutModel',["models/ZoneModel"], function(ZoneModel) {
	function LayoutModel(data) {
		var self = this;
		self.externalId = data.id;
		self.name = data.name;
		self.isDeployed = data.is_deployed;
		self.width = data.width;
		self.height = data.height;
		self.backgroundColor = data.background_color;
		self.backgroundAssetId = data.background_asset_id;
		self.includeInProofOfPlay = data.include_in_proof_of_play;

		self.backgroundAsset = null;
		self.zones = [];

		$(data.zones).each(function(index, value) {
			var zone = new ZoneModel(value, self.externalId);
			self.zones.push(zone);
		});
	}

	return LayoutModel;
});

rjs.define('models/ValidityModel',["system/DateTime"], function(DateTime) {
	function ValidityModel(data) {
		var self = this;
		if (data.start != null)
			self.start = DateTime.parse(data.start);

		if (data.end != null)
			self.end = DateTime.parse(data.end);

		self.startTime = data.start_time;
		self.endTime = data.end_time;

		self.days = data.days;

		self.isCurrentlyValid = function(date) {

			if(!date)
				date = DateTime.now();

			var dateRangeValid = isDateRangeValid(date);
			var timeRangeValid = isTimeRangeValid(date);
			var dayValid = isDayValid(date);
			var valid = (dateRangeValid && timeRangeValid && dayValid);

			return valid;
		}
		function isDateRangeValid(date) {
			var valid = false;
			if (self.start != null & self.end != null) {
				if (date >= self.start && date < self.end) {
					valid = true;
				}
			} else {
				valid = true;
			}

			return valid;
		}

		function isTimeRangeValid(date) {
			var valid = false;

			if (self.startTime != null & self.endTime != null) {
				var startDT = new Date(date).setTime(self.startTime);
				var endDT = new Date(date).setTime(self.endTime);

				// handle overnight validity (i.e. end is before the start)
				if (endDT < startDT)
					endDT = endDT.addDays(1);

				if (date >= startDT && date < endDT) {
					valid = true;
				}
			} else {
				valid = true;
			}
			return valid;
		}

		function isDayValid(date) {
			if (self.days == null)
				return true;
				
			var valid = false;			

			var dayNumber = date.getDay();
			switch(dayNumber) {
				case 0:
					// Sunday
					valid = ((self.days & 1) == 1);
					break;
				case 1:
					// Monday
					valid = ((self.days & 2) == 2);
					break;
				case 2:
					// Tuesday
					valid = ((self.days & 4) == 4);
					break;
				case 3:
					// Wednesday
					valid = ((self.days & 8) == 8);
					break;
				case 4:
					// Thursday
					valid = ((self.days & 16) == 16);
					break;
				case 5:
					// Friday
					valid = ((self.days & 32) == 32);
					break;
				case 6:
					// Saturday
					valid = ((self.days & 64) == 64);
					break;
			}

			return valid;
		}

	}

	return ValidityModel;
});

rjs.define('models/PlaylistMediaAssetActionType',[],function(){
	return {
		CALL_URL:"CallUrl",
		WRITE_DATE_TO_FILE:"WriteDataToFile"
	}
});
rjs.define('models/PlaylistMediaAssetActionModel',["models/PlaylistMediaAssetActionType"], function(PlaylistMediaAssetActionType) {
	function PlaylistMediaAssetActionModel(data) {
		var self = this;
		self.externalId = data.id;
		self.type = null;
		self.url = data.url;
		self.filename = data.filename;
		self.data = data.data;
		
		switch (data.type)
		{
			case "CallUrl":
				self.type = PlaylistMediaAssetActionType.CALL_URL;
				break;
			case "WriteDataToFile":
				self.type = PlaylistMediaAssetActionType.WRITE_DATA_TO_FILE;
				break;
		}
	}

	return PlaylistMediaAssetActionModel;
});

rjs.define('models/PlaylistMediaAssetModel',["models/ValidityModel", "models/PlaylistMediaAssetActionModel"], function(ValidityModel, PlaylistMediaAssetActionModel) {
	function PlaylistMediaAssetModel(data) {
		var self = this;
		self.externalId = data.id;
		self.mediaAssetId = data.media_asset_id;
		self.duration = data.duration * 1000;
		self.includeInProofOfPlay = data.include_in_proof_of_play;
		self.actions = null;
        self.popAssetID = data.pop_asset_id;
        self.usePMADurationForFeeds = false;
        self.feed = null;

        if(data.meta_data != null)
        {
            if(data.meta_data["userdefinedduration"] === "true")
            {
                self.usePMADurationForFeeds = true;
            }
        }        

		if (data.actions != null)
		{
			self.actions = [];

			$.each(data.actions, function(index, action)
			{
				self.actions[index] = new PlaylistMediaAssetActionModel(action);
			});
		}
		
		self.validity = null;
		if (data.validity != null)
			self.validity = new ValidityModel(data.validity);

		self.mediaAsset = null;

		self.isCurrentlyValid = function(date)
        {
			if (self.mediaAsset.hasFailed)
			{
                logger.debug('Asset has failed - failure to play');
                return false;
            }
            if (self.validity == null)
            {
                logger.debug('No validity on PMA, checking mediaasset');
				return self.mediaAsset.isCurrentlyValid(date);
            } else
            {
                return self.validity.isCurrentlyValid(date);
            }
		};

        self.getValidPeriods = function(startDate, endDate)
        {
            var periods = [];

            logger.info('Calculating validity periods for asset');

            // no valid - the asset is therefore valid the entire time specified
            if (self.validity == null) {
                logger.info('No validity set - play all the time');
                periods.push({
                    start : startDate,
                    end : endDate
                });
            } else {
                var validityStart = (self.validity.start == null) ? startDate : self.validity.start;
                var validityEnd = (self.validity.end == null) ? endDate : self.validity.end;

                // calc the periods the asset will recur
                if (self.validity.days == null) {
                    // not recurrence therefore valid all day every day for the validity period
                    periods.push({
                        start : validityStart,
                        end : validityEnd
                    });
                } else {
                    // the asset has recurrence - calculate the periods it will play for the specified period

                    // calculate the recurrence for each day
                    var dateToCheck = startDate;
                    while (dateToCheck <= endDate) {

                        // is the asset valid on the specified day?
                        var isValidOnDayOfWeek = false;
                        var dayNumber = dateToCheck.getDay();
                        switch(dayNumber) {
                            case 0:
                                // Sunday
                                isValidOnDayOfWeek = ((self.validity.days & 1) == 1);
                                break;
                            case 1:
                                // Monday
                                isValidOnDayOfWeek = ((self.validity.days & 2) == 2);
                                break;
                            case 2:
                                // Tuesday
                                isValidOnDayOfWeek = ((self.validity.days & 4) == 4);
                                break;
                            case 3:
                                // Wednesday
                                isValidOnDayOfWeek = ((self.validity.days & 8) == 8);
                                break;
                            case 4:
                                // Thursday
                                isValidOnDayOfWeek = ((self.validity.days & 16) == 16);
                                break;
                            case 5:
                                // Friday
                                isValidOnDayOfWeek = ((self.validity.days & 32) == 32);
                                break;
                            case 6:
                                // Saturday
                                isValidOnDayOfWeek = ((self.validity.days & 64) == 64);
                                break;
                        }

                        // if the asset is valid on the day of the week - calculate the exact times
                        if (isValidOnDayOfWeek) {
                            if (self.validity.startTime != null & self.validity.endTime != null) {
                                var calculatedRecurrenceStart = dateToCheck.setTime(self.validity.startTime);
                                var calculatedRecurrenceEnd = dateToCheck.setTime(self.validity.endTime);

                                // handle overnight validity (i.e. end is before the start)
                                if (calculatedRecurrenceEnd < calculatedRecurrenceStart)
                                    calculatedRecurrenceEnd = calculatedRecurrenceEnd.addDays(1);

                                // adjust date time to ensure that assets don't play outside of the overal validity
                                calculatedRecurrenceStart = (calculatedRecurrenceStart < validityStart) ? validityStart : calculatedRecurrenceStart;
                                calculatedRecurrenceEnd = (calculatedRecurrenceEnd > validityEnd) ? validityEnd : calculatedRecurrenceEnd;

                                // add to the list of times the
                                periods.push({
                                    start : calculatedRecurrenceStart,
                                    end : calculatedRecurrenceEnd
                                });
                            }
                        }

                        // move to the next day!
                        dateToCheck = dateToCheck.addDays(1);
                    }
                }
            }

            return periods;
        };
	}

	return PlaylistMediaAssetModel;
});

rjs.define('models/PlaylistModel',["system/DateTime", "models/MediaTypeConstants", "models/PlaylistMediaAssetModel"], function(DateTime, MediaTypeConstants, PlaylistMediaAssetModel) {
	function PlaylistModel(data)
	{
		var self = this;
		self.externalId = 0;
		self.name = '';
		self.playlistMediaAssets = [];
		self.externalId = data.id;
		self.name = data.name;
		self.includeInProofOfPlay = data.include_in_proof_of_play;
		self.startOnHour = data.start_on_hour;

		$(data.media_assets).each(function(index, value) {
			var playlistMediaAsset = new PlaylistMediaAssetModel(value);
			self.playlistMediaAssets.push(playlistMediaAsset);
		});
		
		self.isAllVideos = function() {
			var isAllVideos = true;

			for (var i = 0; i < self.playlistMediaAssets.length; i++) {
				var plma = self.playlistMediaAssets[i];
				var ma = plma.mediaAsset;

				if (ma.contentType == MediaTypeConstants.VIDEO) {
					isAllVideos = isAllVideos & true;
				} else {
					isAllVideos=false;
					break;
				}
			}

			return isAllVideos;
		};

		self.getGaplessPlaylist = function() {
			// set the next validity event high so that the checks are always less than this
			var nextValidityEvent = DateTime.now().addDays(365);

			// calculate the validity events for the next 7 days
			var startOfPeriod = DateTime.now();
			var endOfPeriod = startOfPeriod.addDays(7);

			// for each playlist media asset
			for (var i = 0; i < self.playlistMediaAssets.length; i++)
			{
				var plma = self.playlistMediaAssets[i];
				var validPeriods = plma.getValidPeriods(startOfPeriod, endOfPeriod);
				
				// and it's validity periods
				for (var j = 0; j < validPeriods.length; j++)
				{
					// find the next event - this could be the start or end of any given asset
					if (validPeriods[j].start > DateTime.now() && validPeriods[j].start < nextValidityEvent)
						nextValidityEvent = validPeriods[j].start;

					if (validPeriods[j].end > DateTime.now() && validPeriods[j].end < nextValidityEvent)
						nextValidityEvent = validPeriods[j].end;
				}
			}

			var plmas = [];
			// get the assets that are valid until the calculated date
			for (var i = 0; i < self.playlistMediaAssets.length; i++)
			{
				var plma = self.playlistMediaAssets[i];
				var validPeriods = plma.getValidPeriods(startOfPeriod, nextValidityEvent);
				
				// and it's validity periods
				for (var j = 0; j < validPeriods.length; j++) {
					// find the next event - this could be the start or end of any given asset
					if (validPeriods[j].start <= DateTime.now() && validPeriods[j].end >= nextValidityEvent)
						plmas.push(plma);
				}
			}

			return {
				playlistMediaAssets : plmas,
				endDate : nextValidityEvent
			};
		};
	}

	return PlaylistModel;
});

rjs.define('models/MimeTypeMediaTypeMappings',["models/MediaTypeConstants"], function(MediaTypeConstants) {
	function MimeTypeMediaTypeMappings() {
		var self = this;
		self.getMediaType = function(mime) {
			switch(mime) {
				case "image/png":
				case "image/jpg":
				case "image/bmp":
				case "image/svg+xml":
				case "image/tiff":
					return MediaTypeConstants.IMAGE;
				case "video/x-ms-wmv":
				case "video/avi":
				case "video/divx":
				case "video/mp4":
				case "video/mp2t":
				case "video/m4v":
				case "video/x-m4v":
				case "video/x-matroska":
				case "video/quicktime":
				case "video/mpeg":
					return MediaTypeConstants.VIDEO;
				case "application/rss+xml":
					return MediaTypeConstants.RSS;
				case "application/vnd.signagelive.iptv":
					return MediaTypeConstants.IPTV;
				case "text/html":
					return MediaTypeConstants.WEB;
				case "application/vnd.signagelive.capture":
					return MediaTypeConstants.CAPTURE;
				case "application/widget":
                    return MediaTypeConstants.WIDGET;
                case "application/vnd.signagelive.feed":
					return MediaTypeConstants.FEED;
			}
		};

		self.getMimeTypeTypeByFileExt = function(ext)
        {
            switch (ext)
            {
                // Images
                case "tif":
                case "tif":
                    return "image/tiff";
                case "png":
                    return "image/png";
                case "jpg":
                case "jpeg":
                    return "image/jpeg";
                case "gif":
                    return "image/gif";
                case "bmp":
                case "bm":
                    return "image/bmp";
                case "svg":
                    return "image/svg+xml";
                
                // Videos
                case "mp4":
                    return "video/mp4";
                case "webm":
                    return "video/webm";
                case "mpg":
                case "mpeg":
                    return "video/mpeg";
                case "avi":
                    return "video/avi";
                
                // Text
                case "js":
                    return "text/javascript";
                case "css":
                    return "text/css";
                case "html":
                case "htm":
                case "htmls":
                    return "text/html";
                case "xml":
                    return "text/xml";
                default:
                    return "text/plain";

                case "json":
                    return "application/json";
                case "txt":
                    return "text/plain";
           }
        };
	}

	return new MimeTypeMediaTypeMappings();
});
rjs.define('models/RealTimeEventsModel',[], function() {
    function RealTimeEventsModel(data) {
        var self = this;
        self.id = data.id;
        self.name = data.name;
        self.events = data.events;

        // the number of seconds to keep this RTE before it is discarded - defualt 5 days (for now)
        self.cacheExpiry = 432000;

        if (data.cacheExpiry) {
            self.cacheExpiry = data.cacheExpiry;
        }
    }

    return RealTimeEventsModel;
});
rjs.define('models/MediaAssetModel',["models/MimeTypeMediaTypeMappings", "models/MediaTypeConstants", "models/ValidityModel", "models/RealTimeEventsModel"], function(MimeTypeMediaTypeMappings, MediaTypeConstants, ValidityModel, RealTimeEventsModel) {
    function MediaAssetModel(data, globalRealTimeEvents) {
        var self = this;
        self.externalId = data.id;
        self.name = data.name;
        self.mimeType = data.type;
        self.contentType = MimeTypeMediaTypeMappings.getMediaType(self.mimeType);
        self.remoteUrl = data.url;
        self.localFileName = data.local_file_name;
        self.hash = data.hash;
        self.hashType = data.hash_type;
        self.size = data.size;
        self.isDownloadable = (self.contentType == MediaTypeConstants.IMAGE || self.contentType == MediaTypeConstants.VIDEO || self.contentType == MediaTypeConstants.RSS || self.contentType == MediaTypeConstants.WIDGET);
        self.isExternallyDelivered = (self.contentType == MediaTypeConstants.RSS);
        self.hasFailed = false;
        self.numberOfFailures = 0;
        self.includeInProofOfPlay = data.include_in_proof_of_play;
        self.popAssetID = data.pop_asset_id;
        self.metaData = data.meta_data;
        self.duration = data.duration * 1000;
        self.integrationIds = data.integrations;
        self.integrations = []; // These are set at the ContentModel level

        self.realTimeEvents = [];

        //Fix has due to broken CRC32 on server
        if(self.hash && self.hash.length < 8)
        {
            var pad = "";

            //Add enough 0s to make an 8 digit CRC32
            while((pad.length + self.hash.length) != 8)
            {
                pad = pad + "0";
            }

            self.hash = pad + self.hash;
        }

        if (data.real_time_events != null && data.real_time_events != undefined) {

            if (data.real_time_events.global != null && data.real_time_events.global != undefined) {

                // need to get the RTE object from global configuration
                $(data.real_time_events.global).each(function(index, globalRealTimeEventId) {

                    // find the id on the global list
                    $(globalRealTimeEvents).each(function(index, globalRealTimeEvent) {
                        if (globalRealTimeEventId == globalRealTimeEvent.id) {
                            self.realTimeEvents.push(globalRealTimeEvent);
                        }
                    });
                });
            }

            // add custom RTEs
            if (data.real_time_events.custom != null && data.real_time_events.custom != undefined) {
                $(data.real_time_events.custom).each(function(index, customRTEConfig) {
                    var customRTE = new RealTimeEventsModel(customRTEConfig);
                    self.realTimeEvents.push(customRTE);
                });
            }
        }

        self.validity = null;
        if (data.validity != null)
            self.validity = new ValidityModel(data.validity);

        self.isCurrentlyValid = function(date) {
            if (self.validity == null)
                return true;
            else
                return self.validity.isCurrentlyValid(date);
        };
    }

    return MediaAssetModel;
});
rjs.define('models/TriggerModel',[],function() {
	function TriggerModel(data) {
		var self = this;
		self.type = data.type;
		self.triggerKey = data.configuration_parameters.ActionKey;
		self.cancelKey = data.configuration_parameters.CancelKey;

		self.serialDeviceId = data.serial_device_id;	
	}

	return TriggerModel;
});

rjs.define('models/ActionModel',[],function() {
	function ActionModel(data) {
		var self = this;
		self.layoutId = data.layout_id;
		self.duration = data.duration;
		
		self.layout = null;
	}

	return ActionModel;
});

rjs.define('models/InterruptModel',["models/TriggerModel","models/ActionModel"],function(TriggerModel,ActionModel) {
	function InterruptModel(data) {
		var self = this;
		self.name = data.name;
		self.trigger = new TriggerModel(data.trigger);
		self.action = new ActionModel(data.action);
		
	}

	return InterruptModel;
});

rjs.define('models/RssItemModel',[], function() {
	function RssItemModel(data) {
		var self = this;
		self.title = '';

		try {
			self.title = $(data).children('title').first().text();
		} catch(e) {
			logger.error('Error loading item: ' + e.message);
		}
	}

	return RssItemModel;
});

rjs.define('models/RssChannelModel',["properties/App.Settings", "models/RssItemModel"], function(Settings, RssItemModel) {
	function RssChannelModel(data) {
		var self = this;

		self.title = $(data).children('rss').children('channel').children('title').text();
		try{
			self.lastBuildDate = Date.parse($(data).children('rss').children('channel').children('lastBuildDate').text());
		} catch(err){}

		try{
			self.pubDate =  Date.parse($(data).children('rss').children('channel').children('pubDate').text());
		} catch(err){}

		self.items = [];
		$(data).find('item').each(function(index, item) {
			var rssItem = new RssItemModel(item);
			if (item.title != '') {
				self.items.push(rssItem);
			}
		});
	}

	RssChannelModel.load = function(file) {
		var rssChannel = null;
		try {
			// NOTE - need to use the AJAX call to load the xml as there have been instances where byte order mark is the first characted retured in the
			// xml jquery.parseXML does not handle this whereas ajax does.
			$.ajax({
				url : file,
				type : 'GET',
				dataType : "xml",
				async : false
			}).done(function(data, textStatus, jqXHR) {
				logger.debug('RSS loaded ' + data);
				if (data != null) {
					rssChannel = new RssChannelModel(data);
				}
			}).fail(function(jqXHR, textStatus, errorThrown) {
				logger.error('Error loading RSS feed');
			});

		} catch(e) {
			logger.error('Error loading RSS Feed ' + e.message);
		}

		return rssChannel;
	};

	return RssChannelModel;
});

rjs.define('gui/PlayUrlHelper',["properties/App.Settings"], function(Settings) {
    function PlayUrlHelper() {
        var self = this;

        self.getMediaAssetPlayUrl = function(mediaAsset) {
            var dfd = $.Deferred();

            var path = "media/" + mediaAsset.localFileName;

       
            dfd.resolve(path);
            
            return dfd;
        };

        self.getFileUrlForVirtualPath = function(virtualPath)
		{
			var dfd = $.Deferred();			
      var path = Settings.application.fileSystem.storageDirectory;

			dfd.resolve(virtualPath.replace(path, "file://"));

			return dfd;
		};
    }

    var helper = new PlayUrlHelper();
    return helper;
});
rjs.define('models/RssChannelCache',["properties/App.Settings", "models/MediaTypeConstants", "models/RssChannelModel", "gui/PlayUrlHelper"], function(Settings, MediaTypeConstants, RssChannel, PlayUrlHelper) {
	function RssChannelCache() {
		var self = this;

		self.loadRssAssetIntoCache = function(asset) {
			if (asset.contentType != MediaTypeConstants.RSS)
				return;

			PlayUrlHelper.getMediaAssetPlayUrl(asset)
			.done(function(localFilePath) {
				try {
					// NOTE - need to use the AJAX call to load the xml as there have been instances where byte order mark is the first characted retured in the
					// xml jquery.parseXML does not handle this whereas ajax does.
					$.ajax({
						url : localFilePath,
						type : 'GET',
						dataType : "xml"
					}).done(function(data, textStatus, jqXHR) {
						logger.debug('RSS loaded ' + data);
						if (data != null) {
							var rssChannel = new RssChannel(data);
							self[asset.externalId] = rssChannel;
						}
					}).fail(function(jqXHR, textStatus, errorThrown) {
						logger.error('Error loading RSS feed');
					});

				} catch(e) {
					logger.error('Error loading RSS Feed ' + e.message);
				}
			});
		};
	}

	var cache = new RssChannelCache();
	return cache;
});

rjs.define('models/FeedMediaAssetModel',["models/ValidityModel", "models/PlaylistMediaAssetActionModel"], function(ValidityModel, PlaylistMediaAssetActionModel) {
	function FeedMediaAssetModel(data) {
		var self = this;
		self.externalId = data.id;
		self.mediaAssetId = data.media_asset_id;
		self.duration = data.duration * 1000;
		self.includeInProofOfPlay = data.include_in_proof_of_play;
		self.actions = null;
        self.popAssetID = data.pop_asset_id;
        self.grouping = data.grouping;
        self.feed = null;

		if (data.actions != null)
		{
			self.actions = [];

			$.each(data.actions, function(index, action)
			{
				self.actions[index] = new PlaylistMediaAssetActionModel(action);
			});
		}
		
		self.validity = null;
		if (data.validity != null)
			self.validity = new ValidityModel(data.validity);

		self.mediaAsset = null;

		self.isCurrentlyValid = function()
        {
			if (self.mediaAsset.hasFailed)
			{
                logger.debug('Asset has failed - failure to play');
                return false;
            }
            if (self.validity == null)
            {
                logger.debug('No validity on PMA, checking mediaasset');
				return self.mediaAsset.isCurrentlyValid();
            } else
            {
                return self.validity.isCurrentlyValid();
            }
		};

        self.getValidPeriods = function(startDate, endDate)
        {
            var periods = [];

            logger.info('Calculating validity periods for asset');

            // no valid - the asset is therefore valid the entire time specified
            if (self.validity == null) {
                logger.info('No validity set - play all the time');
                periods.push({
                    start : startDate,
                    end : endDate
                });
            } else {
                var validityStart = (self.validity.start == null) ? startDate : self.validity.start;
                var validityEnd = (self.validity.end == null) ? endDate : self.validity.end;

                // calc the periods the asset will recur
                if (self.validity.days == null) {
                    // not recurrence therefore valid all day every day for the validity period
                    periods.push({
                        start : validityStart,
                        end : validityEnd
                    });
                } else {
                    // the asset has recurrence - calculate the periods it will play for the specified period

                    // calculate the recurrence for each day
                    var dateToCheck = startDate;
                    while (dateToCheck <= endDate) {

                        // is the asset valid on the specified day?
                        var isValidOnDayOfWeek = false;
                        var dayNumber = dateToCheck.getDay();
                        switch(dayNumber) {
                            case 0:
                                // Sunday
                                isValidOnDayOfWeek = ((self.validity.days & 1) == 1);
                                break;
                            case 1:
                                // Monday
                                isValidOnDayOfWeek = ((self.validity.days & 2) == 2);
                                break;
                            case 2:
                                // Tuesday
                                isValidOnDayOfWeek = ((self.validity.days & 4) == 4);
                                break;
                            case 3:
                                // Wednesday
                                isValidOnDayOfWeek = ((self.validity.days & 8) == 8);
                                break;
                            case 4:
                                // Thursday
                                isValidOnDayOfWeek = ((self.validity.days & 16) == 16);
                                break;
                            case 5:
                                // Friday
                                isValidOnDayOfWeek = ((self.validity.days & 32) == 32);
                                break;
                            case 6:
                                // Saturday
                                isValidOnDayOfWeek = ((self.validity.days & 64) == 64);
                                break;
                        }

                        // if the asset is valid on the day of the week - calculate the exact times
                        if (isValidOnDayOfWeek) {
                            if (self.validity.startTime != null & self.validity.endTime != null) {
                                var calculatedRecurrenceStart = dateToCheck.setTime(self.validity.startTime);
                                var calculatedRecurrenceEnd = dateToCheck.setTime(self.validity.endTime);

                                // handle overnight validity (i.e. end is before the start)
                                if (calculatedRecurrenceEnd < calculatedRecurrenceStart)
                                    calculatedRecurrenceEnd = calculatedRecurrenceEnd.addDays(1);

                                // adjust date time to ensure that assets don't play outside of the overal validity
                                calculatedRecurrenceStart = (calculatedRecurrenceStart < validityStart) ? validityStart : calculatedRecurrenceStart;
                                calculatedRecurrenceEnd = (calculatedRecurrenceEnd > validityEnd) ? validityEnd : calculatedRecurrenceEnd;

                                // add to the list of times the
                                periods.push({
                                    start : calculatedRecurrenceStart,
                                    end : calculatedRecurrenceEnd
                                });
                            }
                        }

                        // move to the next day!
                        dateToCheck = dateToCheck.addDays(1);
                    }
                }
            }

            return periods;
        };
	}

	return FeedMediaAssetModel;
});

rjs.define('models/FeedModel',["system/DateTime", "models/MediaTypeConstants", "models/FeedMediaAssetModel"], function(DateTime, MediaTypeConstants, FeedMediaAssetModel) {
	function FeedModel(data)
	{
		var self = this;
		self.externalId = 0;
		self.mediaAssetId = 0;
		self.name = '';
		self.playMode = '';

		self.feedMediaAssets = [];
		self.externalId = data.id;
		self.name = data.name;
		self.mediaAssetId = data.media_asset_id;
		self.playMode = data.play_mode;
		self.includeInProofOfPlay = data.include_in_proof_of_play;
		self.assetsToPlay = 0; //0 means play them all!

		$(data.media_assets).each(function(index, value) {
			var feedMediaAsset = new FeedMediaAssetModel(value);
			self.feedMediaAssets.push(feedMediaAsset);
		});

		if(data.meta_data)
		{
			if(data.meta_data.assets_to_play)
			{
				self.assetsToPlay = parseInt(data.meta_data.assets_to_play);
			}
		}

		if(self.playMode === "reverse")
			self.feedMediaAssets = self.feedMediaAssets.reverse();

		self.isAllVideos = function() {
			var isAllVideos = true;

			for (var i = 0; i < self.feedMediaAssets.length; i++) {
				var plma = self.feedMediaAssets[i];
				var ma = plma.mediaAsset;

				if (ma.contentType == MediaTypeConstants.VIDEO) {
					isAllVideos = isAllVideos & true;
				} else {
					isAllVideos=false;
					break;
				}
			}

			return isAllVideos;
		};

		self.randomize = function()
		{
			logger.debug("Randomizing feed");
			var tempAssetsArray = [];
			var currentGrouping = -1;
			var tempGroupArray = [];

			//Preserve groupings
			$(self.feedMediaAssets).each(function(index, feedMediaAsset) {
				if(currentGrouping != feedMediaAsset.grouping)
				{
					if(tempGroupArray.length > 0)
					{

						if(tempGroupArray.length > 1)
						{		
							//if we have more than 1 asset, randomize the group array before adding it to the list
							randomizeArray(tempGroupArray);
						}
						
						//Push to the array of arrays
						tempAssetsArray.push(tempGroupArray.slice(0));
						tempGroupArray = [];
					}
					currentGrouping = feedMediaAsset.grouping;
				}

				tempGroupArray.push(feedMediaAsset);
			});

			//Add the last group to the array
			if(tempGroupArray.length > 0)
			{
				if(tempGroupArray.length > 1)
				{
					//if we have more than 1 asset, randomize the group array before adding it to the list
					randomizeArray(tempGroupArray);
				}
				
				//Push to the array of arrays
				tempAssetsArray.push(tempGroupArray.slice(0));
			}
	
			//Randomize the array of arrays
			randomizeArray(tempAssetsArray);
			
			//Re-add to feedmediaassets
			self.feedMediaAssets = [];
			$(tempAssetsArray).each(function(index, array) {
				$(array).each(function(index2, feedMediaAsset) {	
					self.feedMediaAssets.push(feedMediaAsset);			
				});			
			});

		}

	}

	function randomizeArray(array)
	{
		for (var i = array.length - 1; i > 0; i--) {
			var j = Math.floor(Math.random() * (i + 1));
			var temp = array[i];
			array[i] = array[j];
			array[j] = temp;
		}
	}

	return FeedModel;
});

rjs.define('models/IntegrationModel',[],function () {
	function IntegrationModel(data) {
		var self = this;
		self.externalId = data.id;
		self.name = data.name;
		self.dataSource = data.dataSource;
		self.integrationId = data.integrationId;
		self.objectId = data.objectId;
		self.dssQuery = data.dssQuery;
		self.pollFrequency = data.pollFrequency;
	}

	return IntegrationModel;
});

rjs.define('models/TakeoverRecurrenceModel',[], function() {
    function TakeoverRecurrenceModel(recurrence) {
        var self = this;
        self.startTime = (recurrence.StartTime) ? recurrence.StartTime : "0:00:00";
        self.endTime = (recurrence.EndTime) ? recurrence.EndTime : "23:59:59";
        self.days = recurrence.Days;
        self.whenToPlay = recurrence.WhenToPlay;
        self.repeatEveryXSeconds = recurrence.RepeatEveryXSeconds;

        self.isActiveOnDate = function (dt) {
            var isActive = false;

            var dayNumber = dt.getDay();
            switch (dayNumber) {
                case 0:
                    // Sunday
                    isActive = ((self.days & 1) == 1);
                    break;
                case 1:
                    // Monday
                    isActive = ((self.days & 2) == 2);
                    break;
                case 2:
                    // Tuesday
                    isActive = ((self.days & 4) == 4);
                    break;
                case 3:
                    // Wednesday
                    isActive = ((self.days & 8) == 8);
                    break;
                case 4:
                    // Thursday
                    isActive = ((self.days & 16) == 16);
                    break;
                case 5:
                    // Friday
                    isActive = ((self.days & 32) == 32);
                    break;
                case 6:
                    // Saturday
                    isActive = ((self.days & 64) == 64);
                    break;
            }

            return isActive;
        };
    };

    return TakeoverRecurrenceModel;
});
rjs.define('models/TakeoverModel',["models/TakeoverRecurrenceModel", "models/PlaylistModel", "system/DateTime"], function(TakeoverRecurrenceModel, PlaylistModel, DateTime) {
    function TakeoverModel(takeover) {
        var self = this;

        self.id = takeover.id; //WAS mediaAssetId
        self.layoutId = takeover.layout_id;
        self.layoutName = takeover.name;
        self.layout = null; //This is populated by the ContentModel
        self.start = DateTime.parse(takeover.start);
        self.end = DateTime.parse(takeover.end);

        // Once the content model updates and layoutId is matched, layout becomes available - takeover.layout is a layoutmodel an also returns zones, in that has the default playlist or media asset ID and structure as well as scheduled. Looks like takeovers are default playlist

        self.playbackMode = "entire_playlist"; //NOTE: Currently, takeovers can only be the entire playlist - we aren't provided a playback mode

        self.recurrence = new TakeoverRecurrenceModel(takeover.recurrence);

        self.getTakeoverDuration = function(assetIndex, dt) {
            return self.getTotalValidDurationForMediaAssets(dt);
        };

        self.getTotalValidDurationForMediaAssets = function(dt) {
            var takeoverDurationInMS = 0;
            self.playlistMAs = self.layout.zones[0].defaultPlaylist.playlistMediaAssets; //Is there a better way to do this (?)
            self.playlistMAs.forEach(function(pma) {
                if(pma.isCurrentlyValid(dt) && pma.mediaAsset.isCurrentlyValid(dt)) {
                    // NOTE: The only difference for the takeover model on Brightsign is here
                    takeoverDurationInMS += (pma.mediaAsset.duration && pma.mediaAsset.duration > 0) ? pma.mediaAsset.duration : pma.duration;
                }
            });
            return (takeoverDurationInMS / 1000);
        };
    
        self.getNextTakeoverEvent = function() {
            if (self.currentAssetIndex == null) { //set if not previously set
                var currentDate = new Date();    
                self.currentScheduleTime = new Date(currentDate.getTime()).setTime(currentDate.getHours() + ':0:0');
            }

            // Calculate start time, duration and end time
            if(self.recurrence.whenToPlay == "play_once_a_day") {
                var startTime = new Date().setTime(self.recurrence.startTime);
            } else {
                var startTime = new Date(self.currentScheduleTime);
            }
            
            var takeoverDurationInSeconds = self.getTakeoverDuration(self.currentAssetIndex, startTime);
            var endTime = new Date(startTime.getTime()).addSeconds(takeoverDurationInSeconds);
    
            // Create Event
            var nextEvent = {
                id: self.id,
                playbackMode: self.playbackMode,
                assetIndex: self.currentAssetIndex,
                startTime: startTime,
                endTime: endTime,
                takeover: self
            };
    
            // Update the current schedule time
            var repeatFrequencyInSeconds = self.recurrence.repeatEveryXSeconds;

            self.currentScheduleTime = new Date(endTime.getTime()).addSeconds(repeatFrequencyInSeconds);
        
            return nextEvent;
        }
    };

    return TakeoverModel;
});
rjs.define('models/ContentModel',["models/MediaTypeConstants", "models/ScheduleModel", "models/LayoutModel", "models/PlaylistModel", "models/MediaAssetModel", "models/InterruptModel", "models/RssChannelCache", "models/FeedModel", "models/IntegrationModel", "models/TakeoverModel"], function(MediaTypeConstants, ScheduleModel, LayoutModel, PlaylistModel, MediaAssetModel, InterruptModel, RssChannelCache, FeedModel, IntegrationModel, TakeoverModel) {
    function ContentModel(data, globalRealTimeEvents, serialDevices) {
        var self = this;
        self.defaultPlaylistId = null;
        self.defaultPlaylist = null;
        self.schedules = [];
        self.interrupts = [];
        self.layouts = [];
        self.playlists = [];
        self.mediaAssets = [];
        self.feeds = [];
        self.integrations = [];
        self.takeovers = [];
        
        $(data.integrations).each(function(index, value) {
			self.integrations.push(new IntegrationModel(value));
		});

        $(data.media_assets).each(function(index, value) {
            var mediaAsset = new MediaAssetModel(value, globalRealTimeEvents);
            self.mediaAssets.push(mediaAsset);

            if (mediaAsset.contentType == MediaTypeConstants.RSS) {
                RssChannelCache.loadRssAssetIntoCache(mediaAsset);
            }

            // If the mediaAsset integrationIds property has any values then
			// find the matching integration model and push it to mediaAsset.integrations.
			$(mediaAsset.integrationIds).each(function(index, integrationId) {
				// Get matching integration
				var matchingIntegrations = $.grep(self.integrations, function(integration) {
					return integration.externalId === integrationId;
				});
				if (matchingIntegrations.length === 1) {
					mediaAsset.integrations.push(matchingIntegrations[0]);
				}
			});
        });

        $(data.feeds).each(function(index, value) {
            var feed = new FeedModel(value);
            self.feeds.push(feed);

            $(feed.feedMediaAssets).each(function(index, feedMediaAsset) {
                // find the media asset
                var mediaAssets = $.grep(self.mediaAssets, function(mediaAsset, index) {
                    return mediaAsset.externalId == feedMediaAsset.mediaAssetId;
                });

                if (mediaAssets.length == 1)
                    feedMediaAsset.mediaAsset = mediaAssets[0];
            });
        });

        $(data.playlists).each(function(index, value) {
            var playlist = new PlaylistModel(value);
            self.playlists.push(playlist);

            $(playlist.playlistMediaAssets).each(function(index, playlistMediaAsset) {
                // find the media asset
                var mediaAssets = $.grep(self.mediaAssets, function(mediaAsset, index) {
                    return mediaAsset.externalId == playlistMediaAsset.mediaAssetId;
                });

                if (mediaAssets.length == 1) {
                    playlistMediaAsset.mediaAsset = mediaAssets[0];

                    //If it is a feed, save the feed on the playlist media asset
                    if (mediaAssets[0].contentType == MediaTypeConstants.FEED) {
                        var feeds = $.grep(self.feeds, function(feed, index) {
                            return feed.mediaAssetId == mediaAssets[0].externalId;
                        });

                        if (feeds.length == 1) {
                            playlistMediaAsset.feed = feeds[0];
                            var feed = feeds[0];
                            $(feed.feedMediaAssets).each(function(index, fma) {
                                fma.feed = feed;
                            });
                        }      
                    }
                }
            });
        });

        self.defaultPlaylistId = data.default_playlist_id;

        // find the default playlist
        var playlists = $.grep(self.playlists, function(playlist, index) {
            return playlist.externalId == self.defaultPlaylistId;
        });
        if (playlists.length == 1)
            self.defaultPlaylist = playlists[0];

        $(data.layouts).each(function(index, value) {
            var layout = new LayoutModel(value);
            self.layouts.push(layout);

            if (layout.backgroundAssetId != null) {
                // find the default media asset
                var mediaAssets = $.grep(self.mediaAssets, function(mediaAsset, index) {
                    return mediaAsset.externalId == layout.backgroundAssetId;
                });
                if (mediaAssets.length == 1)
                    layout.backgroundAsset = mediaAssets[0];
            }

            $(layout.zones).each(function(index, zone) {
                // populate the background image
                var mediaAssets = $.grep(self.mediaAssets, function(mediaAsset, index) {
                    return mediaAsset.externalId == zone.backgroundAssetId;
                });
                if (mediaAssets.length == 1)
                    zone.backgroundAsset = mediaAssets[0];

                if (zone.configurationParameters != null || zone.configurationParameters != undefined) {
                    if (zone.configurationParameters.seperator_asset_id != undefined) {
                        // populate the seperator image
                        var mediaAssets = $.grep(self.mediaAssets, function(mediaAsset, index) {
                            return mediaAsset.externalId == zone.configurationParameters.seperator_asset_id;
                        });
                        if (mediaAssets.length == 1)
                            zone.seperatorAsset = mediaAssets[0];
                    }
                }

                // find the default media asset
                var mediaAssets = $.grep(self.mediaAssets, function(mediaAsset, index) {
                    return mediaAsset.externalId == zone.defaultMediaAssetId;
                });
                if (mediaAssets.length == 1)
                    zone.defaultMediaAsset = mediaAssets[0];

                // find the default playlist
                var playlists = $.grep(self.playlists, function(playlist, index) {
                    return playlist.externalId == zone.defaultPlaylistId;
                });
                if (playlists.length == 1)
                    zone.defaultPlaylist = playlists[0];

                // find the scheduled playlists
                $(zone.scheduledPlaylists).each(function(index, scheduledPlaylist) {
                    var playlists = $.grep(self.playlists, function(playlist, index) {
                        return playlist.externalId == scheduledPlaylist.playlistid;
                    });

                    if (playlists.length == 1)
                        scheduledPlaylist.playlist = playlists[0];
                });
            });
        });

        $(data.schedules).each(function(index, value) {
            var schedule = new ScheduleModel(value);
            self.schedules.push(schedule);

            // find the layouts
            var layouts = $.grep(self.layouts, function(layout, index) {
                return schedule.layoutId == layout.externalId;
            });
            if (layouts.length == 1)
                schedule.layout = layouts[0];
        });

        try {
            $(data.interrupts).each(function(index, value) {
                var interrupt = new InterruptModel(value);
                self.interrupts.push(interrupt);
				// find the layouts
				var layouts = $.grep(self.layouts, function(layout, index) {
					return interrupt.action.layoutId == layout.externalId;
				});
				if (layouts.length == 1)
					interrupt.action.layout = layouts[0];

                //Add the serial device to the interrupt
                if(interrupt.trigger.type=="SerialEvent")
                {
                    var matchingSerialDevices = $.grep(serialDevices, function(device, index) {
                        return interrupt.trigger.serialDeviceId == device.externalId;
                    });
                    if (matchingSerialDevices && matchingSerialDevices.length == 1)
                        interrupt.trigger.serialDevice = matchingSerialDevices[0];
                }
			});
		} catch(e) {
			logger.error("Error whilst loading interrupts: " + e.message);
		}

        $(data.takeovers).each(function(index, value) {
            var takeover = new TakeoverModel(value);
            self.takeovers.push(takeover);

            // find the layouts
            var layouts = $.grep(self.layouts, function(layout, index) {
                return takeover.layoutId == layout.externalId;
            });
            if (layouts.length == 1)
                takeover.layout = layouts[0];
        });
    }

    return ContentModel;
});
rjs.define('models/ApplicationLoggingModel',[],function() {
	function ApplicationLoggingModel(data) {
		var self = this;
		self.on = data.on;
		self.level = data.level;
		self.upload = data.upload;
	}

	return ApplicationLoggingModel;

});

rjs.define('models/ScreenCaptureLoggingModel',[],function() {
	function ScreenCaptureLoggingModel(data) {
		var self = this;

		if (data != null && data != undefined) {
			self.on = data.on;
			self.frequency = data.frequency;
			self.screenshotAtNextContentCheck = data.screenshot_at_next_content_check;
			logger.info("SCREEN CAPTURE SETTINGS: " + data.on + ", " + data.frequency);
		} else {
			logger.info("SCREEN CAPTURE IS NOT ENABLED: " + JSON.stringify(data));
			self.on = false;
			self.frequency = 60;
			self.screenshotAtNextContentCheck = false;
		}

	}

	return ScreenCaptureLoggingModel;

});
rjs.define('models/LoggingModel',["models/ApplicationLoggingModel", "models/ScreenCaptureLoggingModel"], function(ApplicationLoggingModel, ScreenCaptureLoggingModel) {
    function LoggingModel(data) {
        var self = this;

        if (data != null) {
            self.application = new ApplicationLoggingModel(data.application);
            self.screencapture = new ScreenCaptureLoggingModel(data.screencapture);
        }
    }

    return LoggingModel;
});
rjs.define('models/ScreenControlPlanModel',["system/DateTime"],function(DateTime) {
	function ScreenControlPlanModel(data) {
		var self = this;
		self.onTime = data.on;
		self.offTime = data.off;
		self.days = data.days;		
		
		self.isActiveOnDate = function(dt){
			var isActive = false;
			
			var dayNumber = dt.getDay();
			switch(dayNumber) {
				case 0:
					// Sunday
					isActive = ((self.days & 1) == 1);
					break;
				case 1:
					// Monday
					isActive = ((self.days & 2) == 2);
					break;
				case 2:
					// Tuesday
					isActive = ((self.days & 4) == 4);
					break;
				case 3:
					// Wednesday
					isActive = ((self.days & 8) == 8);
					break;
				case 4:
					// Thursday
					isActive = ((self.days & 16) == 16);
					break;
				case 5:
					// Friday
					isActive = ((self.days & 32) == 32);
					break;
				case 6:
					// Saturday
					isActive = ((self.days & 64) == 64);
					break;
			}

			return isActive;
		};
	}

	return ScreenControlPlanModel;
});

rjs.define('models/ScreenControlModel',["system/DateTime", "models/ScreenControlPlanModel"], function(DateTime, ScreenControlPlanModel) {
	function ScreenControlModel(data) {
		var self = this;
		self.plans = [];

		if (data != null) {
			$(data.plans).each(function(index, value) {
				var screenControlPlan = new ScreenControlPlanModel(value);
				self.plans.push(screenControlPlan);
			});
		}

		function getScreenControlSchedule() {
			var schedules = [];

			var scheduleStartDate = DateTime.now().setTime('00:00:00').addDays(-1);
			var scheduleEndDate = scheduleStartDate.addDays(8);

			var currentDateBeingScheduled = scheduleStartDate;

			var lastOffTime = currentDateBeingScheduled;

			// add all of the 'on' periods to the schedule
			while (currentDateBeingScheduled < scheduleEndDate) {

				// get the plans relavent to the date being scheduled?
				// a plan is a 'on' period
				var relaventPlans = $.grep(self.plans, function(plan, index) {
					return plan.isActiveOnDate(currentDateBeingScheduled);
				});

				// sort the plans by time order
				relaventPlans = relaventPlans.sort(function(a, b) {
					if (a.onTime > b.onTime)
						return 1;
					if (a.onTime < b.onTime)
						return -1;
					return 0;
				});

				var now = DateTime.now();

				$(relaventPlans).each(function(i, plan) {

					// calculate the on and off times
					var onTime = currentDateBeingScheduled.setTime(plan.onTime);
					var offTime = currentDateBeingScheduled.setTime(plan.offTime);

					// if the offtime is 'earlier' than the on time it is intended for the next day
					if (offTime < onTime)
						offTime = offTime.addDays(1);

					if (onTime > lastOffTime) {
						schedules.push({
							time : lastOffTime,
							action : 'off'
						});
					}
					schedules.push({
						time : onTime,
						action : 'on'
					});

					lastOffTime = offTime;
				});

				currentDateBeingScheduled = currentDateBeingScheduled.addDays(1);
			}

			// add the last off time as the last command to execute
			schedules.push({
				time : lastOffTime,
				action : 'off'
			});

			return schedules;
		}


		self.nextAction = function() {
			var nextAction = null;
			if (self.plans.length > 0) {

				var schedules = getScreenControlSchedule();

				var now = DateTime.now();

				for (var i = 0; i < schedules.length; i++) {
					if (schedules[i].time > now) {
						nextAction = schedules[i];
						break;
					}
				}
			}
			return nextAction;
		};

		self.isScreenCurrentlyScheduledOn = function() {
			// if there are no screen control plans the screen should be on
			// if there are screen control plans the screen is off anytime there is no valid on period

			var isCurrentlyScheduledOn = false;

			if (self.plans.length > 0) {
				var schedules = getScreenControlSchedule();

				var now = DateTime.now();

				var activeSchedule = null;

				for (var i = 0; i < schedules.length; i++) {
					if (schedules[i].time > now) {

						// the active schedule is the previous one in the array
						activeSchedule = schedules[i - 1];
						break;
					}
				}

				if (activeSchedule != null) {
					isCurrentlyScheduledOn = (activeSchedule.action == 'on');
				}

			} else {
				// no screen control plans - the screen is on all the time by default
				isCurrentlyScheduledOn = true;
			}

			return isCurrentlyScheduledOn;
		};

		self.isScreenCurrentlyScheduledOff = function() {
			return (!self.isScreenCurrentlyScheduledOn());
		};
	}

	return ScreenControlModel;
});

rjs.define('models/ClientDetailsModel',[], function() {
    function ClientDetailsModel(data) {
        var self = this;
        self.activationCode = data.activation_code;
        self.signageliveSerialNumber = data.serial_number;
        self.description = data.description;
        self.address1 = data.address_1;
        self.address2 = data.address_2;
        self.city = data.city;
        self.state = data.state;
        self.zipCode = data.zip_code;
        self.referenceCode1 = data.reference_code_1;
        self.referenceCode2 = data.reference_code_2;
        self.referenceCode3 = data.reference_code_3;
        self.referenceCode4 = data.reference_code_4;
        self.referenceCode5 = data.reference_code_5;
        self.referenceCode6 = data.reference_code_6;
        self.referenceCode7 = data.reference_code_7;
        self.referenceCode8 = data.reference_code_8;
        self.referenceCode9 = data.reference_code_9;
        self.referenceCode10 = data.reference_code_10;
        self.timeZone = data.time_zone;
        self.utcOffset = data.utc_offset;
    }

    return ClientDetailsModel;
});
rjs.define('models/ProofOfPlayConfigurationModel',[],function() {
	function ProofOfPlayConfigurationModel(data) {
		var self = this;
		self.enabled = data.proof_of_play_enabled;
		self.uploadfrequency = data.upload_frequency;
		self.uploadurl = data.upload_url;
		self.maxlogfilesizeinkb = data.max_log_file_size_kb;
		self.mindiskspaceinkb = data.min_disk_space_kb;
		
		logger.debug("Proof of Play Config: " + JSON.stringify(data));
	}

	return ProofOfPlayConfigurationModel;

});
rjs.define('models/SerialDeviceModel',[],function() {
	function SerialDeviceModel(data) {
		var self = this;
        
        self.externalId = data.id;
        self.name = data.name;

        self.config = {};

        self.config.port = data.configuration.port;
        self.config.baudRate = data.configuration.baudRate;
        self.config.dataBits = data.configuration.dataBits;
        self.config.parity = data.configuration.parity;
        self.config.stopBits = data.configuration.stopBits;
        self.config.flowControl = data.configuration.flowControl;
        self.config.protocol = data.configuration.protocol;
        self.config.eol = data.configuration.eol;
	}

	return SerialDeviceModel;
});

rjs.define('models/SynchronizationModel',[], function() {
	function SynchronizationModel(data) {
		var self = this;
		
        self.enabled = false;
        self.configurationParameters = null;
        self.groupId = 0;
        self.waitTimeout = 500;
        self.defaultMaster = null;
        self.groupMembers = null;
		
		if((data === undefined) == false)
		{
			self.enabled = data.enabled;
            self.groupId = data.sync_group_id;
            self.configurationParameters = data.configuration_parameters;
            self.waitTimeout = data.wait_timeout;
            self.defaultMaster = data.default_master;
            self.groupMembers = data.group_members;
		}
	}

	return SynchronizationModel;
});

rjs.define('models/FirmwareUpdateModel',[],function() {
    function FirmwareUpdateModel(data) {
        var self = this;
        self.id = data.id;
        self.version = data.version;
        self.url = data.url;
        self.hash = data.hash;
        self.hashType = data.hash_type;
        self.size = data.size;
        self.filename = null;
        self.version = self.version;
        if (self.url) {
            self.filename = self.url.substring(self.url.lastIndexOf('/')+1);
        }

        self.isValid = function() {
            return self.url != null && self.filename != null && self.version != null;
        };
    }
    return FirmwareUpdateModel;
});
rjs.define('models/RemoteSettingsModel',[],function() {
	function RemoteSettingsModel(data) {
		var self = this;
        // Not implemented
		if (data != null) {
			self.nativePortraitRotation = data.native_portrait_rotation;
		}	
	}
	return RemoteSettingsModel;
});

rjs.define('models/DataSyncServicesModel',[],function () {
	function DataSyncServicesModel(data) {
		var self = this;
		self.dataApiUrl = data.data_api_url;
	}
	return DataSyncServicesModel;
});

rjs.define('models/TakeoverGroupSyncSettingsModel',[], function() {
    function TakeoverGroupSyncSettingsModel(syncSettings) {
        var self = this;

        if(syncSettings !== null) {
            self.broadcastAddress = syncSettings.broadcast_address;
            self.udpPort = syncSettings.udp_port;
            // self.udpSendPort = syncSettings.udp_send_port; //Not available
            self.failsafeTime = syncSettings.failsafe_time;
            self.missedSyncThreshold = syncSettings.missed_sync_threshold;
        }
    };

    return TakeoverGroupSyncSettingsModel;
});
rjs.define('models/TakeoverGroupMemberModel',[], function() {
    function TakeoverGroupMemberModel(data) {
        var self = this;
        
        if(data !== null) {
            self.ids = data;
        }
    };

    return TakeoverGroupMemberModel;
});
rjs.define('models/TakeoverGroupModel',["models/TakeoverGroupSyncSettingsModel", "models/TakeoverGroupMemberModel"], function(TakeoverGroupSyncSettingsModel, TakeoverGroupMemberModel) {
    function TakeoverGroupModel(group) {
        var self = this;
        self.id = group.id;
        self.name = group.name;
        self.syncSettings = new TakeoverGroupSyncSettingsModel(group.sync_settings);
        self.members = new TakeoverGroupMemberModel(group.members);
        self.master = group.master; //TODO:
    };

    return TakeoverGroupModel;
});
rjs.define('models/ConfigurationModel',["system/DateTime", "models/ConnectivityModel", "models/ContentModel", "models/LoggingModel", "models/ScreenControlModel", "models/ClientDetailsModel", "models/ProofOfPlayConfigurationModel", "models/NetworkModel", "models/SerialDeviceModel", "models/SynchronizationModel", "models/RealTimeEventsModel", "models/FirmwareUpdateModel", "models/RemoteSettingsModel","models/DataSyncServicesModel", "models/TakeoverGroupModel"], function(DateTime, ConnectivityModel, ContentModel, LoggingModel, ScreenControlModel, ClientDetailsModel, ProofOfPlayConfigurationModel, NetworkModel, SerialDeviceModel, SynchronizationModel, RealTimeEventsModel, FirmwareUpdateModel, RemoteSettingsModel, DataSyncServicesModel, TakeoverGroupModel) {
    function ConfigurationModel(data) {
        var self = this;

        self.changeId = data.change_id;
        self.lastModified = new Date(data.last_modified);
        self.connectivity = new ConnectivityModel(data.connectivity);

        self.realTimeEvents = [];

        if (data.real_time_events != null && data.real_time_events != undefined) {
            $(data.real_time_events).each(function(index, value) {
                var rte = new RealTimeEventsModel(value);
                self.realTimeEvents.push(rte);
            });
        }

        self.serialDevices = [];
        if (data.serial_devices != null && data.serial_devices != undefined) {
            $(data.serial_devices).each(function(index, value) {
                var dev = new SerialDeviceModel(value);
                self.serialDevices.push(dev);
            });
        }

        self.content = new ContentModel(data.content, self.realTimeEvents, self.serialDevices);
        self.network = new NetworkModel(data.network);
        self.screenControl = new ScreenControlModel(data.screen_control);
        self.synchronization = new SynchronizationModel(data.synchronization);

        self.logging = new LoggingModel(data.logging);
        self.clientDetails = new ClientDetailsModel(data.client_details);

        if (data.proof_of_play != null) {
            self.proofOfPlayConfig = new ProofOfPlayConfigurationModel(data.proof_of_play);
        }

        if (data.firmware_update != null) {
            self.firmwareUpdate = new FirmwareUpdateModel(data.firmware_update);
        }

        if (data.data_sync_services != null) {
			self.dataSyncServices = new DataSyncServicesModel(data.data_sync_services);
		}

		if (data.takeover_group != null) {
			self.takeoverGroup = new TakeoverGroupModel(data.takeover_group);
		}		
	}

	return ConfigurationModel;
});
rjs.define('models/ObjectProcessingQueueModel',[],function() {
	function ObjectProcessingQueueModel(objects) {
		var self = this;

		if (objects != null & objects != undefined)
			self.items = objects;
		else
			self.items = [];
			
		self.processedItems = [];
		self.failedItems = [];

		self.shift = function() {
			return self.items.shift();
		};
		
		self.push=function(item){
			self.items.push(item);
		};

		self.currentItem = function() {
			if (self.items.length == 0)
				return null;
			else
				return self.items[0];
		};
	}

	return ObjectProcessingQueueModel;
});

rjs.define('models/ConnectivityIntervalSchemeModel',[],function() {
	return {
		REGULAR_INTERVAL : "regularinterval",
		TIME_OF_DAY : "timeofday"
	};
}); 
rjs.define('models/PusherSubscriptionModel',[],function() {
	/**
	 * [PusherSubscriptionModel description]
	 * @param {[type]} data [description]
	 */
	function PusherSubscriptionModel(data) {
		var self = this;

		self.lastModifiedDate = new Date(data.lastModifiedDate);
		self.pusherKey = data.pusherKey;
		self.channels = data.channels;
	}

	return PusherSubscriptionModel;
});
rjs.define('models/UserConfigurationModel',["properties/App.Settings", "system/io/File", "models/PusherSubscriptionModel"], function(Settings, File, PusherSubscriptionModel) {
	/**
	 * [UserConfigurationModel description]
	 */
	function UserConfigurationModel() {
		var self = this;

		self.isMuted = false;
		self.showTrace = false;
		self.rotation = {
			displayName : 'No rotation',
			angle : 0,
			id : 0
		};
		
		self.screenConfig;

		self.timeZone = {
			displayName : '(GMT) Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London',
			bsid : 'GMTBST',
			id : 'Europe/London'
		};
		self.interactiveKiosk = {
			enabled : false,
			idleIntervalInSeconds : 15,
			trigger : {
				key : "t",
				type : "KeyboardEvent"
			}
		};
		self.modules = {
			webBasedInterrupts : {
				enabled : false,
				options : {
					pusherSettings : null,
					trigger : null
				}
			},
			interactiveKiosk : {
				enabled : false,
				options : {
					idleIntervalInSeconds : 15,
					trigger : {
						key : "t",
						type : "KeyboardEvent"
					}
				}
			},
			GPIOBasedInterrupts : {
				enabled : false,
				options : {
					trigger : {
						type : "KeyboardEvent"
					}
				}
			},
			RFIDBasedInterrupts : {
				enabled : false,
				options : {}
			}
		};

		self.remoteFirmwareUpdates = {
			enabled : false,
			configured : false
		};

		self.videoTransparency = {
			enabled : false
		};

		// self.videoMode = {
		// 	resolution : "auto",
		// 	colorSpace : "rgb",
		// 	bitDepth : 8
		// };

        //To be used for new installs
		self.ptp = {
			enabled: true,
			domain: "0"
		}

		self.audio = { 
			globalAudioChannel: 'hdmi',
            discreetAudioChannels: ['HDMI-1']
		}

		self.alwaysEmbedWebpages = { 
			enabled: false
		}

		self.gaplessWidgetTransitions = false;

		self.settings = {};
		self.hasSaved = false;

		/**
		 * Initialize
		 * @return {[type]} [description]
		 */
		self.initialize = function() {
			var dfd = $.Deferred();

			// load the data
			try {
				logger.debug('User Configuration Model: Loading user settings data');
				File.read(Settings.application.fileSystem.systemFiles.settingsFilePath).done(function(settingsData) {
					if (settingsData != null) {
						self.settings = JSON.parse(settingsData);
						if (self.settings.showTrace != undefined)
							self.showTrace = self.settings.showTrace;

						if (self.settings.isMuted != undefined)
							self.isMuted = self.settings.isMuted;

						if (self.settings.rotation != undefined)
							self.rotation = self.settings.rotation;
							
						if (self.settings.screenConfig != undefined)
							self.screenConfig = self.settings.screenConfig;

						if (self.settings.timeZone != undefined)
							self.timeZone = self.settings.timeZone;

						if(self.settings.interactiveKiosk != undefined)
							self.interactiveKiosk = self.settings.interactiveKiosk;

						if (self.settings.videoTransparency != undefined)
							self.videoTransparency = self.settings.videoTransparency;

						// if (self.settings.videoMode != undefined)
						// 	self.videoMode = self.settings.videoMode;

						if (self.settings.gaplessWidgetTransitions != undefined)
							self.gaplessWidgetTransitions = self.settings.gaplessWidgetTransitions;

						if (self.settings.ptp != undefined)
							self.ptp = self.settings.ptp;
						
                        if (self.settings.audio != undefined)
							self.audio = self.settings.audio;
						
                        if (self.settings.alwaysEmbedWebpages != undefined)
							self.alwaysEmbedWebpages = self.settings.alwaysEmbedWebpages;

						if (self.settings.modules != undefined)
						{
							self.modules = self.settings.modules;
							$.extend(true, Settings.application.modules, self.modules);
						}

						if (self.settings.remoteFirmwareUpdates != undefined)
						{
							self.remoteFirmwareUpdates = self.settings.remoteFirmwareUpdates;
							$.extend(true, Settings.application.remoteFirmwareUpdates, self.remoteFirmwareUpdates);
						}

						self.hasSaved = true;
					}

					

					dfd.resolve(self);
				}).fail(function(){
					logger.error('User Configuration Model: Could not read settings file. Does it exist?');
					dfd.resolve(self);
				})

			} catch(e) {
				logger.error('User Configuration Model: Error reading user settings ' + e.message);
				dfd.reject();
			}

			return dfd;
		};

		/**
		 * [save description]
		 * @return {[type]} [description]
		 */
		self.save = function() {
			var dfd = $.Deferred();

			try {
				logger.debug('User Configuration Mode: Save User Config');

				// save the settings file to indicate that setup has been completed.
				// TODO iterate the settings to generate the serialized object
				self.settings.isMuted = self.isMuted;
				self.settings.showTrace = self.showTrace;
				self.settings.rotation = self.rotation;
				self.settings.screenConfig = self.screenConfig;
				self.settings.timeZone = self.timeZone;
				self.settings.modules = self.modules;
				self.settings.remoteFirmwareUpdates = self.remoteFirmwareUpdates;
				self.settings.videoTransparency = self.videoTransparency;
				self.settings.interactiveKiosk = self.interactiveKiosk;
				self.settings.gaplessWidgetTransitions = self.gaplessWidgetTransitions;
				self.settings.ptp = self.ptp;
				self.settings.audio = self.audio;
				self.settings.alwaysEmbedWebpages = self.alwaysEmbedWebpages;
				
				$.extend(true, Settings.application.modules, self.modules);
				$.extend(true, Settings.application.remoteFirmwareUpdates, self.remoteFirmwareUpdates);

				File.write(Settings.application.fileSystem.systemFiles.settingsFilePath, JSON.stringify(self.settings)).done(function(){
					self.hasSaved = true;
					dfd.resolve(self);
				}).fail(function(){
					dfd.reject();
				});

			} catch(e) {
				logger.error('User Configuration Model: Error whilst saving user settings ' + e.message);
				dfd.reject();
			}

			return dfd;
		};
	}

	return UserConfigurationModel;
});
rjs.define('controllers/UserConfigurationController',["models/UserConfigurationModel", "properties/App.Settings"], function(UserConfigurationModel, Settings) {
	/**
	 * [UserConfigurationController description]
	 */
	function UserConfigurationController() {
		var self = this;
		var _propertyChangedHandlers = [];
		var _userConfiguration = null;

		/**
		 * [initialize description]
		 * @return {[type]} [description]
		 */
		self.initialize = function() {
			var dfd = $.Deferred();

			new UserConfigurationModel().initialize().done(function(config){
				_userConfiguration = config;
				dfd.resolve();
			}).fail(function(){
				dfd.reject();
			})

			return dfd;
		};

		self.save = function()
		{
			var dfd = $.Deferred();
			
			getUserConfigurationModel().save().done(function(){
				dfd.resolve();
			}).fail(function()
			{
				dfd.reject();
			});

			return dfd;
		}

		/**
		 * [addEventListener description]
		 * @param {Function} callback [description]
		 */
		self.addEventListener = function(callback) {
			_propertyChangedHandlers.push(callback);
		};

		/**
		 * [removeEventListener description]
		 * @param  {Function} callback [description]
		 * @return {[type]}            [description]
		 */
		self.removeEventListener = function(callback) {

		};

		/**
		 * [reload description]
		 * @return {[type]} [description]
		 */
		self.reload = function() {
			_userConfiguration = null;
		};
		
		/**
		 * [hasSaved description]
		 * @return {Boolean} [description]
		 */
		self.hasSaved = function(){
			return getUserConfigurationModel().hasSaved;
		};

		/**
		 * [volumeUp description]
		 * @return {[type]} [description]
		 */
		self.volumeUp = function() {
			getUserConfigurationModel().isMuted = false;

			notifyConsumers('volumeUp', null);
		};

		/**
		 * [volumeDown description]
		 * @return {[type]} [description]
		 */
		self.volumeDown = function() {
			getUserConfigurationModel().isMuted = false;

			notifyConsumers('volumeDown', null);
		};

		/**
		 * [isMuted description]
		 * @return {Boolean} [description]
		 */
		self.isMuted = function() {
			return getUserConfigurationModel().isMuted;
		};
        
        /**
		 * [alwaysEmbedWebpages description]
		 * @return {Boolean} [description]
		 */
		self.alwaysEmbedWebpages = function() {
			return getUserConfigurationModel().alwaysEmbedWebpages;
		};
        
        /**
		 * [toggleAlwaysEmbedWebpages description]
		 * @return {Boolean} [description]
		 */
		self.toggleAlwaysEmbedWebpages = function() {
			if (getUserConfigurationModel().alwaysEmbedWebpages) {
				getUserConfigurationModel().alwaysEmbedWebpages.enabled = !getUserConfigurationModel().alwaysEmbedWebpages.enabled;
            }
		};

		/**
		 * [toggleMute description]
		 * @return {[type]} [description]
		 */
		self.toggleMute = function() {
			if (getUserConfigurationModel().isMuted)
				getUserConfigurationModel().isMuted = false;
			else
				getUserConfigurationModel().isMuted = true;

			notifyConsumers('mute', getUserConfigurationModel().isMuted);
		};

		/**
		 * [rotation description]
		 * @param  {[type]} rotation [description]
		 * @return {[type]}          [description]
		 */
		self.rotation = function(rotation) {
			if (rotation !== undefined) {
				getUserConfigurationModel().rotation = rotation;
				notifyConsumers('rotation', getUserConfigurationModel().rotation);
			}
			return getUserConfigurationModel().rotation;
		};

		self.rotateVideos = function(rotateVideos) {
			if (rotateVideos !== undefined) {
				getUserConfigurationModel().rotateVideos = rotateVideos;
				notifyConsumers('rotateVideos', getUserConfigurationModel().rotateVideos);
			}
			return getUserConfigurationModel().rotateVideos;
		};

		self.screenConfig = function(screenConfig) {
			if (screenConfig !== undefined) {
				getUserConfigurationModel().screenConfig = screenConfig;
				notifyConsumers('screenConfig', getUserConfigurationModel().screenConfig);
			}
			return getUserConfigurationModel().screenConfig;
		};

		/**
		 * [isRotated description]
		 * @return {Boolean} [description]
		 */
		self.isRotated = function() {
			return (getUserConfigurationModel().rotation.angle != 0) ? true : false;
		};

		/**
		 * [rotationAngle description]
		 * @return {[type]} [description]
		 */
		self.rotationAngle = function() {
			var rotationAngle = getUserConfigurationModel().rotation.angle;

			// For existing devices that have a custom array for their rotation angles we have modified the UCC to always return as 0 as S5 rotation is handled differently to S3/S4.
			if(Settings.application.features.deviceSeries == 5) {
				rotationAngle = 0;
			}

			return rotationAngle;
        };
        

		self.videoTransparency = function(transparency) {
			if (transparency !== undefined) 
			{
				getUserConfigurationModel().videoTransparency = transparency;
			}
			return getUserConfigurationModel().videoTransparency;
		};

		self.ptp = function(ptp) {
			if (ptp !== undefined) 
			{
				getUserConfigurationModel().ptp = ptp;
			}
			return getUserConfigurationModel().ptp;
		};

        
		self.audio = function(audio) {
			if (audio !== undefined) 
			{
				getUserConfigurationModel().audio = audio;
			}
			return getUserConfigurationModel().audio;
		};

		self.videoMode = function(mode) {
			if (mode !== undefined) 
			{
				getUserConfigurationModel().videoMode = mode;
			}
			return getUserConfigurationModel().videoMode;
		};

		/**
		 * [showTrace description]
		 * @param  {[type]} showTrace [description]
		 * @return {[type]}           [description]
		 */
		self.showTrace = function(showTrace) {
			if (showTrace !== undefined) {
				getUserConfigurationModel().showTrace = showTrace;
				notifyConsumers('showTrace', getUserConfigurationModel().showTrace);
			}
			return getUserConfigurationModel().showTrace;
		};

		/**
		 * [timeZone description]
		 * @param  {[type]} tz [description]
		 * @return {[type]}    [description]
		 */
		self.timeZone = function(tz) {
			if (tz != undefined) {
				// set the timezone
				getUserConfigurationModel().timeZone = tz;
			}
			return getUserConfigurationModel().timeZone;
		};

		self.interactiveKiosk = function(kioskSettings) {
			if (kioskSettings != undefined) {
				// set the kiosk settings
				getUserConfigurationModel().interactiveKiosk = kioskSettings;
			}
			return getUserConfigurationModel().interactiveKiosk;
		};

		self.gaplessWidgetTransitions = function(gaplessWidgetTransitions) {
			if (gaplessWidgetTransitions !== undefined) {
				getUserConfigurationModel().gaplessWidgetTransitions = gaplessWidgetTransitions;
				notifyConsumers('gaplessWidgetTransitions', getUserConfigurationModel().gaplessWidgetTransitions);
			}
			return getUserConfigurationModel().gaplessWidgetTransitions;
		};

		/**
		 * [interativeKiosk description]
		 * @param  {[type]} tz [description]
		 * @return {[type]}    [description]
		 */
		self.modules = function(modules)
		{
			if (modules != undefined) {
				// set the timezone
				getUserConfigurationModel().modules = modules;
			}
			
			return getUserConfigurationModel().modules;
		}

		/**
		 * [remoteFirmwareUpdates description]
		 * @param  {[type]} tz [description]
		 * @return {[type]}    [description]
		 */
		self.remoteFirmwareUpdates = function(remoteFirmwareUpdates)
		{
			if (remoteFirmwareUpdates != undefined) {
				// set the timezone
				getUserConfigurationModel().remoteFirmwareUpdates = remoteFirmwareUpdates;		
			}
			
			return getUserConfigurationModel().remoteFirmwareUpdates;
		}

		/**
		 * [getUserConfigurationModel description]
		 * @return {[type]} [description]
		 */
		function getUserConfigurationModel() {

			return _userConfiguration;
		}

		/**
		 * [notifyConsumers description]
		 * @param  {[type]} event [description]
		 * @param  {[type]} data  [description]
		 * @return {[type]}       [description]
		 */
		function notifyConsumers(event, data) {

			//getUserConfigurationModel().save();

			$(_propertyChangedHandlers).each(function(i, handler) {
				handler(event, data);
			});
		}

	}

	return new UserConfigurationController();
});

rjs.define('system/SystemInterface',['controllers/UserConfigurationController','system/plugins/DownloadPlugin',"properties/App.Settings", "system/io/File"], function(UserConfigurationController,DownloadPlugin, Settings, File) {
	/*
		SystemInterface provides access to low level System Information
	 */
	function SystemInterface() {

		var self = this;

		// To be implemented
		self.getSystemName = function() {

			var dfd = $.Deferred();
     
		    setTimeout(function() {
		    	logger.warning("System Interface: Error: Get System Name Not Implemented.");
		    	dfd.reject();
		    }, 100);

		    return dfd;
		};

		// To be implemented
		self.getOSName = function() {

			var dfd = $.Deferred();
     
		    setTimeout(function() {
		    	dfd.resolve("BrightSign");
		    }, 100);

		    return dfd;
		};

		// To be implemented
		self.getOSVersion = function() {

			var dfd = $.Deferred();
			
		    setTimeout(function() {
		    	var device_info = new BSDeviceInfo();
				dfd.resolve(device_info.version);
		    }, 100);
			

		    return dfd;
		};

		// To be implemented
		self.getManufacturer = function() {

			var dfd = $.Deferred();
     
		    setTimeout(function() {
				dfd.resolve("BrightSign");
		    }, 100);

		    return dfd;
		};

		// To be implemented
		self.getModel = function() {

			var dfd = $.Deferred();
     
		    setTimeout(function() {
		    	var device_info = new BSDeviceInfo();
				dfd.resolve(device_info.model);
		    }, 100);

		    return dfd;
		};

		// To be implemented
		self.getSystemUpTime = function() {

			var dfd = $.Deferred();
     
		    setTimeout(function() {
		    	var device_info = new BSDeviceInfo();
				dfd.resolve(device_info.deviceUptime);
		    }, 100);

		    return dfd;
		};

		// To be implemented
		self.getTimeZone = function() {

			var dfd = $.Deferred();

			setTimeout(function() {
		    	dfd.resolve(UserConfigurationController.timeZone().id);
		    }, 100);
									
			return dfd;
		};

		// To be implemented
		self.getUTCOffset = function() {

			var dfd = $.Deferred();
     
		    setTimeout(function() {
		    	logger.warning("System Interface: Error: Get UTC Offset Not Implemented.");
		    	dfd.reject();
		    }, 100);

		    return dfd;
		};

		// To be implemented
		self.switchOffOSD = function() {

			var dfd = $.Deferred();
     
		    setTimeout(function() {
		    	logger.warning("System Interface: Error: Switch Off OSD Not Implemented.");
		    	dfd.reject();
		    }, 100);

		    return dfd;
		};

		self.saveLog = function() {

			var dfd = $.Deferred();

			var now = new Date();

			var localFileName =  "log-" + now.toISOString().replace(/:/g,"").replace(".000","") + ".log";
			var localFileUrl = Settings.application.fileSystem.logDirectory + "/" + localFileName;
		
			var downloadPlugin = new DownloadPlugin();
            downloadPlugin.downloadFile("http://0.0.0.0/download_log", localFileUrl).done(function(remoteFile, localFile) {
                logger.info('Log saved to disk');

				dfd.resolve();
            }).fail(function(remoteUrl, localFile) {
                logger.info('Error downloading update file');
                self.updateInProgress = false;

                // remove the partially downloaded file
                try {
                    File.delete(localFile);
                } catch (e) {
                    logger.error('Error whilst removing the update file: ' + e.message);
				}
				
				dfd.reject();
            });

		    return dfd;
		};

		// Check Max Supported TLS Version
		var cachedSSLDetails = null;
		self.getMaxSupportedTLSVersion = function () {
			// If we already have a cached response then just return that
			if (cachedSSLDetails) {
				return $.Deferred().resolve(cachedSSLDetails);
			}
			// New deferred result to retrun
			var dfd = $.Deferred();
			// Start building result object
			var result = {
				"tls_version_string": Settings.application.ssl.maxSupportedTLSVersionString,
				"tls_version": Settings.application.ssl.maxSupportedTLSVersion
			};
			// Otherwise attempt to call out to https://www.howsmyssl.com/a/check to get detailed SSL info
			callSSLAPICheck()
				.done(function (detailedSSLResults) {
					result.detailed_tls_settings_included = true;
					result.detailed_tls_settings = detailedSSLResults;
					// Cache result
					cachedSSLDetails = result;
					// Return detailed results
					logger.info('Returning detailed TLS Info: ' + JSON.stringify(result, undefined, 2));
					dfd.resolve(result);
				})
				.fail(function () {
					result.detailed_tls_settings_included = false;
					result.detailed_tls_settings = null;
					logger.info('Returning basic TLS Info: ' + JSON.stringify(result, undefined, 2));
					dfd.resolve(result);
				});

			return dfd;
		};

		function callSSLAPICheck() {
			var dfd = $.Deferred();
			try {
				logger.info('Calling out to Hows My SSL to check SSL details: ' + Settings.application.ssl.sslCheckAPIUrl);
				// Attempt to get detailed SSL info from HowsMySSL
				$.ajax({
					url: Settings.application.ssl.sslCheckAPIUrl,
					type: 'GET',
					contentType: 'application/json',
					timeout: 60000,
				})
					.done(function (data, textStatus, jqXHR) {
						logger.info('SSL API call successful: ' + textStatus);
						dfd.resolve(data);
					})
					.fail(function (jqXHR, textStatus, errorThrown) {
						logger.error('Error calling out to SSL API: ' + textStatus);
						dfd.reject();
					});
			} catch (e) {
				logger.error('Error calling out to SSL API: ' + e);
				dfd.reject();
			}
			return dfd;
		}

		self.getPlayerType = function () {
			return "BrightSign";
		};

		self.getFirmwareVersionSync = function () {
			var device_info = new BSDeviceInfo();
			return device_info.version;
		}
	}
	
	// The object is a singleton as there is only a single plugin added to the application
	return new SystemInterface();
});

rjs.define('models/SystemReport_SystemModel',["system/DateTime", "system/SystemInterface", "controllers/UserConfigurationController"], function(DateTime, SystemInterface, UserConfigurationController) {
	/*
		System Report Model - Provides a model for System information
		-------------------------------------------------------------------------
	*/
	function SystemReport_SystemModel() {
		var self = this;

		// Defaults
		self.name = "Signagelive HTML5 Player";
		self.operating_system_name = "Unknown";
		self.operating_system_version = "Unknown";
		self.manufacturer = "Unknown";
		self.model = "Unknown";
		self.up_time = 0;
		self.system_time = DateTime.now();
		self.time_zone = UserConfigurationController.timeZone().id;
		self.utc_offset = 0;

		//
		// Attempts to populate the model with actual data using the SystemInterface
		//
		self.generate = function() {
			var dfd = $.Deferred();
			
			// Get system name
			SystemInterface.getSystemName().done(function(sysName){
				self.name = sysName;
			}).always(function(){

				// Get operating system name
				SystemInterface.getOSName().done(function(osName){
					self.operating_system_name = osName;
				}).always(function() {

					// Get operating system version
					SystemInterface.getOSVersion().done(function(osVersion){
						self.operating_system_version = osVersion;
					}).always(function(){

						// Get manufacturer
						SystemInterface.getManufacturer().done(function(manufacturer){
							self.manufacturer = manufacturer;
						}).always(function() {

							// Get Model
							SystemInterface.getModel().done(function(modelName){
								self.model = modelName;
							}).always(function() {

								// Get up time
								SystemInterface.getSystemUpTime().done(function(upTime){
									self.up_time = upTime;
								}).always(function() {

									// Get Time Zone
									SystemInterface.getTimeZone().done(function(timeZone){
										self.time_zone = timeZone;
									}).always(function() {

										// Get UTC Offset
										SystemInterface.getUTCOffset().done(function(utcOffset){
											self.utc_offset = utcOffset;
										}).always(function(){

											// Get System Time (Update because the class may have been instantiated some time ago)
											self.system_time = DateTime.now();

											// Resolve System Info Report Generation
											dfd.resolve();
										});
									});
								});
							});
						});
					});
				});
			});

			return dfd;
		};
	}

	return SystemReport_SystemModel;
});


rjs.define('models/SystemReport_ClientModel',["properties/App.Properties"], function(Properties) {
	/*
		Client Report model - Provides a model for the client part of the system report
		-------------------------------------------------------------------------

	*/
	function SystemReport_ClientModel() {
		var self = this;

		// defaults
		self.version = Properties.appVersion;

		//
		// Generates the report. Only has to resolve itself as the only thing that
		// matters is the appVersion and this is already defined.
		//
		self.generate = function() {
			var dfd = $.Deferred();
			
			setTimeout(function() {
				dfd.resolve();
			}, 100);

			return dfd;
		};
	}

	return SystemReport_ClientModel;
});

rjs.define('system/cpu/CPUInterface',[],function() {
	/*
		CPUInterface provides access to low level CPU Information
	 */
	function CPUInterface() {
		var self = this;
		var os = require('os');

		// To be Implemented
		self.getCPUUsage = function() {

			var dfd = $.Deferred();
			//Grab first CPU Measure
			var startMeasure = cpuAverage();
			//Set delay for second Measure
			setTimeout(function () {

				//Grab second Measure
				var endMeasure = cpuAverage();

				//Calculate the difference in idle and total time between the measures
				var idleDifference = endMeasure.idle - startMeasure.idle;
				var totalDifference = endMeasure.total - startMeasure.total;

				//Calculate the average percentage CPU usage
				var percentageCPU = 100 - ~~(100 * idleDifference / totalDifference);

				dfd.resolve(percentageCPU);

			}, 1000);


		    return dfd;
		};

		// To be Implemented
		self.getCPUSpeed = function() {
		 	var dfd = $.Deferred();

		 	setTimeout(function(){
				var device_info = new BSDeviceInfo();

				switch(device_info.model.toLowerCase())
				{
					case "ls424":
					case "ls423":
						dfd.resolve(873);
						break;
					case "xt243":
					case "xt1143":
					case "xd233":
					case "xd1033":
					case "xt244":
					case "xt1144":
					case "xd234":
					case "xd1034":
						dfd.resolve(1719);
						break;
					case "hd1024":
					case "hd224":
					case "hd1023":
					case "hd223":
					case "ho523":
						dfd.resolve(1503);
						break;
					case "xc4055":
						dfd.resolve(2400);
						break;
					default:
						logger.warning("CPU Interface: Error: Get CPU Speed Not Implemented.");
						dfd.reject();
						break;
				}

		 	}, 100);

		 	return dfd;
		};

		// To be Implemented
		self.getName = function() {
			var dfd = $.Deferred();

		 	setTimeout(function(){
				var cpus = os.cpus();
				if (cpus.length > 0) {
					dfd.resolve(cpus[0].model);
				}
				else {
					dfd.reject();
				}  
		 	}, 100);

		 	return dfd;
		};

		function cpuAverage() {
			
			//Initialise sum of idle and time of cores and fetch CPU info
			var totalIdle = 0, totalTick = 0;
			var cpus = os.cpus();
			
			//Loop through CPU cores
			for (var i = 0, len = cpus.length; i < len; i++) {
			
				//Select CPU core
				var cpu = cpus[i];
			
				//Total up the time in the cores tick
				for (type in cpu.times) {
					totalTick += cpu.times[type];
				}
		
				//Total up the idle time of the core
				totalIdle += cpu.times.idle;
			}
			
			//Return the average Idle and Tick times
			return { idle: totalIdle / cpus.length, total: totalTick / cpus.length };
		}


	}
	
	// The object is a singleton as there is only a single plugin added to the application
	return new CPUInterface();
});

rjs.define('models/SystemReport_CPUModel',["system/cpu/CPUInterface"], function(CPUInterface) {
	/*
		CPU Model - Provides a model for Device CPU information
		-------------------------------------------------------------------------
		This model generates all of its information from the system/cpu/CPUInterface class
		which provides access to lower level cpu information for the device.
	 */
	function SystemReport_CPUModel() {
		var self = this;
		self.name = 'Unknown';
		self.speed = 1000;
		self.usage = 0;

		//
		// Attempts to populate the CPU Model with accurate data
		//
		self.generate = function() {
			var dfd = $.Deferred();

			// Get Usage data
			CPUInterface.getCPUUsage().done(function(cpuUsage) {
				self.usage = cpuUsage;
			}).always(function() {

				// Get CPU Speed
				CPUInterface.getCPUSpeed().done(function(cpuSpeed) {
					self.speed = cpuSpeed;
				}).always(function() {

					// Get CPU Name
					CPUInterface.getName().done(function(name){
						self.name = name;
					}).always(function() {

						// Resolve CPU Info
						dfd.resolve();
					});
				});
			});

			return dfd;
		};
	}

	return SystemReport_CPUModel;
});

rjs.define('system/memory/MemoryInterface',[],function() {
	/*
		MemoryInterface provides access to low level Memory Information
	 */
	function MemoryInterface() {

		var self = this;
		var os = require('os');

		// To be Implemented
		self.getRAMUsage = function() {

			var dfd = $.Deferred();
     
			setTimeout(function() {
                var total = (os.totalmem() / 1024) / 1024;
                var free = (os.freemem() / 1024) / 1024;
                var usage = Math.round(((total - free) / total) * 100);
                dfd.resolve(usage);
            }, 100);

		    return dfd;
		};

		function getInstalledRAM()
		{
			var device_info = new BSDeviceInfo();
			var deviceModel = device_info.model.toLowerCase();

			switch(device_info.model.toLowerCase())
			{
				case "xt243":
                case "xt1143":
					return 0;
				case "xd233":
                case "xd1033": 
					return 0;
				case "xt244":
                case "xt1144":
					return 4096;
				case "xd234":
                case "xd1034":
					return 2048;
                case "hd1024":
                case "h224":
					return 2048;
				case "hd1023":
				case "hd223":
                case "ho523":
					return 0;
				case "hd222":
				case "hd1022":
					return 1024;
				case "xd232":
				case "xd1032": 
				case "xd1132":
					return 0;
                case "ls423":
						return 0;

                case "ls424":
                    return 2048;
				default:
					return 0;
			}
		}

		// To be Implemented
		self.getRAMTotalAvailable = function() {

			var dfd = $.Deferred();
     
		    setTimeout(function() {
                dfd.resolve(Math.floor((os.totalmem() / 1024) / 1024));
            }, 100);

		    return dfd;
		};

	}
	
	// The object is a singleton as there is only a single plugin added to the application
	return new MemoryInterface();
});

rjs.define('models/SystemReport_MemoryModel',["system/memory/MemoryInterface"], function(MemoryInterface) {
	/*
		Memory Model - Provides a model for Device memory information
		-------------------------------------------------------------------------
		This model generates all of its information from the system/memory/MemoryInterface class
		which provides access to lower level memory information for the device.
	 */
	function SystemReport_MemoryModel() {
		var self = this;
		self.total_available = 1024;
		self.total_used = 0;

		//
		// Attempts to populate the model
		//
		self.generate = function() {
			var dfd = $.Deferred();
			
			// Get Total RAM Available
			MemoryInterface.getRAMTotalAvailable().done(function(totalRAM) {

				// Update total available
				self.total_available = totalRAM;

				// Get RAM Usage
				MemoryInterface.getRAMUsage().done(function(ramUsage) {

					// Calculate total used
					self.total_used = Math.round(self.total_available * (ramUsage / 100));

					// Resolve Memory Model
					dfd.resolve();

				}).fail(function() {

					// Reject Memory Model
					dfd.reject();
				});
			}).fail(function() {

				// Reject Memory Model
				dfd.reject();
			});

			return dfd;
		};
	}

	return SystemReport_MemoryModel;
});

rjs.define('system/storage/StorageInterface',[],function() {
    /*
    	StorageInterface provides access to low level storage Information
     */
    function StorageInterface() {

        var self = this;

        // To be implemented
        self.getStorageName = function() {

            var dfd = $.Deferred();

            setTimeout(function() {
                logger.warning("Storage Interface: Error: Get Storage Name Not Implemented.");
                dfd.reject();
            }, 100);

            return dfd;
        };

        // To be implemented
        self.getStorageFormat = function() {

            var dfd = $.Deferred();

            $.ajax({
                url: "http://localhost:8080/get-storage-type",
                type: 'GET',
                async: true
            }).done(function(data, textStatus, jqXHR) {
                var result = JSON.parse(data);
                dfd.resolve(result.result);      
            }).fail(function(jqXHR, textStatus, errorThrown) {
                dfd.reject(textStatus);
            });
 

            return dfd;
        };

        // To be implemented
        self.getTotalStorageAvailable = function() {

            var dfd = $.Deferred();

            setTimeout(function() {
                $.ajax({
                    url: "http://localhost:8080/get-storage-total",
                    type: 'GET',
                    async: true
                }).done(function(data, textStatus, jqXHR) {
                    var result = JSON.parse(data);
                    dfd.resolve(result.result);      
                }).fail(function(jqXHR, textStatus, errorThrown) {
                    dfd.reject(textStatus);
      
               });
            }, 100);

            return dfd;
        };

        // To be implemented
        self.getTotalStorageUsed = function() {

            var dfd = $.Deferred();
            
            setTimeout(function() {
                $.ajax({
                    url: "http://localhost:8080/get-storage-used",
                    type: 'GET',
                    async: true
                }).done(function(data, textStatus, jqXHR) {
                    var result = JSON.parse(data);
                    dfd.resolve(result.result);  
                }).fail(function(jqXHR, textStatus, errorThrown) {
                   dfd.reject(textStatus);
                 
                });
            }, 100);
            
            return dfd;
        };

        // To be implemented
        self.getStorageType = function() {

            var dfd = $.Deferred();

            setTimeout(function() {
                dfd.resolve("External");
            }, 100);

            return dfd;
        };
    }

    // The object is a singleton as there is only a single plugin added to the application
    return new StorageInterface();
});
rjs.define('models/SystemReport_StorageDeviceModel',["system/storage/StorageInterface"], function(StorageInterface){
	/*
		Storage Model - Provides a model for Device storage information
		-------------------------------------------------------------------------
		This model generates all of its information from the system/storage/StorageInterface class
		which provides access to lower level storage information for the device.
	 */
	function SystemReport_StorageDeviceModel(){
		var self = this;
		
		// Defaults
		self.name = 'Internal Storage';
		self.format = 'Unknown';
		self.total_available = 0;
		self.total_used = 0;
		self.type = 'Fixed Internal';

		//
		// Attempts to populate the storage system report model
		//
		self.generate = function() {

			var dfd = $.Deferred();

			// Get storage Name
			StorageInterface.getStorageName().done(function(storageName){
				self.name = storageName;
			}).always(function() {

				// Get Storage format
				StorageInterface.getStorageFormat().done(function(storageFormat){
					self.format = storageFormat;
				}).always(function() {

					// Get total available space
					StorageInterface.getTotalStorageAvailable().done(function(availableSpace) {
						self.total_available = availableSpace;
					}).always(function() {

						// Get total used
						StorageInterface.getTotalStorageUsed().done(function(usedSpace){
							self.total_used = usedSpace;
						}).always(function() {

							StorageInterface.getStorageType().done(function(storageType){
								self.type = storageType;
							}).always(function() {

								// Resolve storage device system report generation
								dfd.resolve();
							});
						});
					});
				});
			});

			return dfd;
		};
	}
	
	return SystemReport_StorageDeviceModel;
});

rjs.define('system/temperature/TemperatureInterface',[],function() {
	/*
		TemperatureInterface provides access to low level Temperature Information
	 */
	function TemperatureInterface() {

		var self = this;

		// To be implemented
		self.getTemperature = function() {

			var dfd = $.Deferred();
     
		    setTimeout(function() {
		    	logger.warning("Temperature Interface: Error: Get Temperature Not Implemented.");
		    	dfd.reject();
		    }, 100);

		    return dfd;
		};


		// To be implemented
		self.getTemperatureUnit = function() {

			var dfd = $.Deferred();
     
		    setTimeout(function(){
		    	logger.warning("Temperature Interface: Error: Get Temperature Unit Not Implemented.");
		    	dfd.reject();
		    }, 100);

		    return dfd;
		};

		// To be implemented
		self.getSensorName = function() {

			var dfd = $.Deferred();
     
		    setTimeout(function(){
		    	logger.warning("Temperature Interface: Error: Get Sensor Name Not Implemented.");
		    	dfd.reject();
		    }, 100);

		    return dfd;
		};
	}
	
	// The object is a singleton as there is only a single plugin added to the application
	return new TemperatureInterface();
});

rjs.define('models/SystemReport_TemperatureModel',["system/temperature/TemperatureInterface"], function(TemperatureInterface) {
	/*
		Temperature Model - Provides a model for Device temperature information
		-------------------------------------------------------------------------
		This model generates all of its information from the system/temperature/TemperatureInterface class
		which provides access to lower level temperature information for the device.
	 */
	function SystemReport_TemperatureModel() {
		var self = this;

		// Defaults
		self.sensor_name = 'Screen';
		self.temperature = 0;
		self.temperature_unit = "celsius";

		//
		// Attempts to populate the temperature model
		//
		self.generate = function() {
			var dfd = $.Deferred();

			// Get Temperature
			TemperatureInterface.getTemperature().done(function(temp) {
				self.temperature = temp;
			}).always(function() {

				// Get Temperature Unit
				TemperatureInterface.getTemperatureUnit().done(function(unit) {
					self.temperature_unit = unit;
				}).always(function(){

					// Get Sensor Name
					TemperatureInterface.getSensorName().done(function(sensorName){
						self.sensor_name = sensorName;
					}).always(function(){

						// Resolve Temperature Model
						dfd.resolve();
					});
				});
			});

			return dfd;
		};
	}

	return SystemReport_TemperatureModel;
});

rjs.define('system/graphics/GraphicsInterface',["controllers/UserConfigurationController"],function(UserConfigurationController) {
	/*
		GraphicsInterface provides access to low level Graphics Information
	 */
	function GraphicsInterface() {

		//See https://docs.brightsign.biz/display/DOC/videomodeconfiguration
		var self = this;
		var VideoModeConfigurationClass = require("@brightsign/videomodeconfiguration");
		var videoConfig = new VideoModeConfigurationClass();
        var VideoOutputClass = require("@brightsign/videooutput");
        var videoOutputHDMI = new VideoOutputClass("hdmi");
		self.screens = [];
		self.availableVideoModes = [];

		self.getGraphicsName = function() {			
			var dfd = $.Deferred();   
			setTimeout(function() {    
				var device_info = new BSDeviceInfo();
				var deviceModel = device_info.model.toLowerCase();
				if(deviceModel.startsWith("hs")||deviceModel.startsWith("ho"))
				{
					dfd.resolve("Internal Graphics");
				} else {
					dfd.resolve("HDMI");
				}
			}, 50);
		    return dfd;
		};

		self.getOutputResolution = function()
		{
			var dfd = $.Deferred();
			var output = {};
	
			videoOutputHDMI.getOutputResolution().then(function(size){
				output.width = size.width;
				output.height = size.height;
				dfd.resolve(output);
			}).catch(function(error)
			{
				var videomode_info = new BSVideoMode();
				output.width = videomode_info.resX;
				output.height = videomode_info.resY;

				dfd.resolve(output);
			});

			return dfd;
		};
		
		// To be Implemented
		self.getGraphicsHorizontalResolution = function() {			
			var dfd = $.Deferred();
     
		    setTimeout(function(){
				var videomode_info = new BSVideoMode();
				dfd.resolve(videomode_info.resX);
		    }, 100);

		    return dfd;
		};

		// To be Implemented
		self.getGraphicsVerticalResolution = function() {

			var dfd = $.Deferred();
     
		    setTimeout(function(){
				var videomode_info = new BSVideoMode();
				dfd.resolve(videomode_info.resY);
		    }, 100);

		    return dfd;
		};

		// To be Implemented
		self.getContrastRGB = function() {

			var dfd = $.Deferred();
     
		    setTimeout(function(){
		    	logger.warning("Graphics Interface: Error: Get Contrast RGB Not Implemented.");
		    	dfd.reject();
		    }, 100);

		    return dfd;
		};

		// To be Implemented
		self.getBrightnessRGB = function() {

			var dfd = $.Deferred();
     
		    setTimeout(function(){
		    	logger.warning("Graphics Interface: Error: Get Brightness RGB Not Implemented.");
		    	dfd.reject();
		    }, 100);

		    return dfd;
		};

		self.setVideoMode = function(settingsVideoMode) {
			
			var videoModeString = "";
			
			var videomode_info = new BSVideoMode();

			if(settingsVideoMode.resolution == "auto")
			{
				//var bestMode = videomode_info.GetBestMode("hdmi");
				//bestmode will be something like 1920x1080x60p or 3840x2160x60p
				videoModeString = "auto";
			}
			else 
			{
				videoModeString = settingsVideoMode.resolution + ":";
				if(settingsVideoMode.colorSpace == "rgb")
					videoModeString += "rgb:auto";
				else
				{
					videoModeString += settingsVideoMode.colorSpace + ":" + settingsVideoMode.bitDepth + "bit";
				}
			}

			var device_info = new BSDeviceInfo();
			var deviceModel = device_info.model.toLowerCase();

			if(deviceModel.startsWith("xt") && videoModeString != "auto")
			{
				var resParts = settingsVideoMode.resolution.split("x");
				var resWidth = parseInt(resParts[0]);
				var resHeight = parseInt(resParts[1]);

				//fullres required for resolutions over 1920x1200
				if(resWidth > 1920 || resHeight > 1200)
					videoModeString += ":fullres:gfxmemlarge";
			}
			
			var dfd = $.Deferred();
			
			var item = {};
			item.videoMode = videoModeString;
			
			$.ajax({
				traditional: true,
				url: "http://localhost:8080/set-video-mode",
				type: 'POST',
				contentType: "application/json",
				data: JSON.stringify(item),
				async: true
			}).done(function(data, textStatus, jqXHR) {
				var result = JSON.parse(data);
				
				logger.debug("Setting video mode to " + videoModeString + ", result = " + result.result);
								
				dfd.resolve(result.result); 
							
			}).fail(function(jqXHR, textStatus, errorThrown) {
				dfd.reject("Error sending set video mode request: " + textStatus);	
			});
			
			return dfd;
		};

		self.getSupportedVideoModes = function()
		{
			var dfd = $.Deferred();

			videoConfig.getAvailableModes().then(
				function(data)
				{
					//var i;
					//for (i = 0; i < data.length; i++) { 
						//logger.debug(data[i].modeName);
					//}

					self.availableVideoModes = data.sort(function(a, b) {
						if(a.width > b.width) {
							return a.width - b.width;
						} else {
							return (a.width + a.height) - (b.width + b.height);
						}
					}).reverse();

					dfd.resolve(data); 
				}).catch(
				function(data) {
					dfd.reject("Error getting video modes: " + JSON.stringify(data));	
			});

			return dfd;
		};

		self.getBestMode = function () {
			var dfd = $.Deferred();

			videoConfig.getBestMode("hdmi").then(function (vm) {
				dfd.resolve(vm);
			});
			
			return dfd;
		};

		self.getScreenModes = function() {
			var dfd = $.Deferred();
			
			videoConfig.getScreenModes().then(function(data) {
				data = data.filter(function(screen) { return screen.outputName !== "TypeC-1"; }); //TypeC is not currently supported therefore we want to remove it from being returned
				self.screens = data;
				dfd.resolve(data);
			});
						
			return dfd;
		};

		self.setScreenModes = function (screenConfig) {
			var dfd = $.Deferred();
			var validConfig = true;
			try {
				var screensEnabled = screenConfig.filter(function(screen) {
					if(screen.enabled) {
						return screen;
					}
				});

				// Check enabled screens and if none are enabled then dont update the screen configuration
				if(screensEnabled.length == 0) { validConfig = false; }
				
				screenConfig.forEach(function (screen, ind) {
					if((screen.enabled == false && screen.outputName !== undefined) || (screen.enabled == true && screen.outputName !== undefined && screen.screenX !== undefined && screen.screenY !== undefined && screen.transform !== undefined && screen.videoMode !== undefined)) {} else {
						validConfig = false;
					}
					// Double check screen.transform state to make sure an invalid transform is not used
					if (screen.transform) {
						transform = (Number(screen.transform) == 270) ? "270" : (Number(screen.transform) == 90) ? "90" : "normal";
						screen.transform = transform;
					}
				});

				if(validConfig) {
					videoConfig.setScreenModes(screenConfig).then(function () {
						logger.info("Screen Config Updated");
						setTimeout(function() {
							dfd.resolve();
						}, 250);
					}).catch(function(e) {
						logger.error(e);
						dfd.reject();
					});
				} else {
					logger.warning("Invalid Configuration");
					dfd.reject();
				}
			} catch (e) {
				dfd.reject(e);
			}

			return dfd;
		};

		self.getDisplayProperties = function() {
			var displayProperties = {};
			var dfd = $.Deferred();
			$.when(self.getGraphicsHorizontalResolution(), self.getGraphicsVerticalResolution()).done(function(width, height) {
				displayProperties.width = width;
				displayProperties.height = height;
				displayProperties.is_native_portait_app = false;
				// NOTE: For S5 - Return screens array data - useful for multiple output displays
				displayProperties.screenConfig = UserConfigurationController.screenConfig();
				// NOTE: Amended for S5: Return primary screen information
				var screenRotationValue = displayProperties.screenConfig.filter(function(screen) {
					if(screen.enabled) {
						return screen;
					}
				});
				displayProperties.rotation = (!screenRotationValue) ? 0 : (screenRotationValue[0].transform !== "normal") ? Number(screenRotationValue[0].transform) : 0;
				displayProperties.video_rotation_supported = true;
				dfd.resolve(displayProperties);
			});
			return dfd;
		};


	}
	
	// The object is a singleton as there is only a single plugin added to the application
	return new GraphicsInterface();
});

rjs.define('models/SystemReport_GraphicsDeviceModel',["system/graphics/GraphicsInterface","controllers/UserConfigurationController"], function(GraphicsInterface, UserConfig) {
	/*
		Graphics Model - Provides a model for Device temperature information
		-------------------------------------------------------------------------
		This model generates all of its information from the system/graphics/GraphicsInterface class
		which provides access to lower level graphics information for the device.
	 */
	function SystemReport_GraphicsDeviceModel() {
		var self = this;

		// Defaults
		self.name = 'Internal Graphics';
		self.current_resolution_horizontal = document.documentElement.clientWidth;
		self.current_resolution_vertical = document.documentElement.clientHeight;

		//
		// Attempts to populate the graphics model
		//
		self.generate = function() {

			var dfd = $.Deferred();

			logger.info('Generate Graphics Information');

			// Get graphics name
			GraphicsInterface.getGraphicsName().done(function(gName){
				self.name = gName;
			}).always(function(){

				GraphicsInterface.getOutputResolution().done(function(outputSize) {
					self.current_resolution_horizontal = outputSize.width;
					self.current_resolution_vertical = outputSize.height;
				}).always(function() {
					if(UserConfig.isRotated())
					{
						var tempRes = self.current_resolution_horizontal;
						self.current_resolution_horizontal = self.current_resolution_vertical;
						self.current_resolution_vertical = tempRes;
					}
					// Resolve graphics system model generation
					dfd.resolve();
				});			
			});

			return dfd;
		};
	};

	return SystemReport_GraphicsDeviceModel;
});

rjs.define('models/SystemReport_NetworkAdapterModel',["system/network/NetworkInterface"], function(NetworkInterface) {
	/*
		Network Adapter Model - Provides a model for Device network information
		-------------------------------------------------------------------------
		This model generates all of its information from the system/network/NetworkInterface class
		which provides access to lower level networking information for the device.
	 */
	function SystemReport_NetworkAdapterModel() {
		var self = this;
		self.type = '';
		self.use_dhcp = false;
		self.ipv4_address = '';
		self.subnet_mask = '';
		self.default_gateway = '';
		self.dns1 = '';
		self.dns2 = '';
		self.mac_address = '';

		//
		// When called attempts to populate the model with informaton from the device
		//
		self.generate = function() {

			var dfd = $.Deferred();

			logger.info('Getting network information');

			try {
				// Get active network connection type
				NetworkInterface.getActiveNetworkConnectionType().done(function(activeType) {
					self.type = activeType;
				}).always(function(){
					// Get the rest of the device details. Could use $.when but using .always() instead - this seemed to work better for SSSP as each call to the SDK occurs one at a time.
				
					// DHCP enabled?
					NetworkInterface.isDHCP().done(function(isDHCP){
						self.use_dhcp = isDHCP;
					}).always(function(){

						// Get device IP
						NetworkInterface.getIP().done(function(ip){
							self.ipv4_address = ip;
						}).always(function() {

							// Get subnet mask
							NetworkInterface.getSubnetMask().done(function(subnetMask) {
								self.subnet_mask = subnetMask;
							}).always(function() {

								// Get default gateway
								NetworkInterface.getDefaultGateway().done(function(defaultGateway){
									self.default_gateway = defaultGateway;
								}).always(function() {

									// Get DNS settings
									NetworkInterface.getDNS().done(function(dns){
										self.dns1 = dns;
									}).always(function() {

										// Get MAC address
										NetworkInterface.getMAC().done(function(mac) {
											self.mac_address = mac;
										}).always(function() {
											
											// Resolve Generation
											dfd.resolve();
										});
									});
								});
							});
						});
					});
				});
			} catch(e) {
				logger.error('Error whilst getting network information: ' + e.message);
				dfd.reject();
			}

			return dfd;
		};
	}

	return SystemReport_NetworkAdapterModel;
});

rjs.define('system/sound/SoundInterface',[],function() {
	/*
		Sound Interface - Access to the SDK methods related to sound and volume control / information
	*/
	function SoundInterface() {
	
		var self = this;

		//
		// Get Volume - Not Implemented in generic App
		//
		self.getVolume = function() {
			var dfd = $.Deferred();

			setTimeout(function(){
				logger.warning("Sound Interface: Error: Get Volume Not Implemented.");
				dfd.reject();
			}, 100);

			return dfd;

		};

		//
		// Set Volume - Not Implemented in generic App
		//
		self.setVolume = function(level) {
			var dfd = $.Deferred();

			if(level >= 0 && level <= 100)
			{

				var item = {};
				item.volume = level;

				$.ajax({
					traditional: true,
					url: "http://localhost:8080/audio-set-volume",
					type: 'POST',
					contentType: "application/json",
					data: JSON.stringify(item),
					async: true
				}).done(function(data, textStatus, jqXHR) {
					dfd.resolve();
				}).fail(function(jqXHR, textStatus, errorThrown) {
					dfd.reject("Error sending set volume request: " + textStatus);	
				});
			} else{
				dfd.reject("Volume must be between 0 and 100")
			}

			return dfd;

		};

		//
		// Get Mute Status - Not Implemented in generic App
		//
		self.getMuteStatus = function() {
			var dfd = $.Deferred();

			setTimeout(function(){
				logger.warning("Sound Interface: Error: Get Mute Status Not Implemented.");
				dfd.reject();
			}, 100);

			return dfd;

		};

		//
		// Set Mute Status - Not Implemented in generic App
		//
		self.setMuteStatus = function(muted) {
			var dfd = $.Deferred();

			var item = {};
			item.muted = muted;

			$.ajax({
				traditional: true,
				url: "http://localhost:8080/audio-mute",
				type: 'POST',
				contentType: "application/json",
				data: JSON.stringify(item),
				async: true
			}).done(function(data, textStatus, jqXHR) {
				dfd.resolve();
			}).fail(function(jqXHR, textStatus, errorThrown) {
				dfd.reject("Error sending mute audio request: " + textStatus);	
			});

			return dfd;

		};
	}

	return new SoundInterface();
});

rjs.define('system/remotecontrol/RemoteControlInterface',[],function() {
    /*
    	RemoteControlInterface provides access to low level remote control Information
     */
    function RemoteControlInterface() {

        var self = this;

        // Supported Key Codes
        self.keys = {
            KEY_INFO: -1, // Not Implemented
            KEY_PLAY: -1, // Not Implemented
            KEY_PAUSE: -1, // Not Implemented
            KEY_RW: -1, // Not Implemented
            KEY_FF: -1, // Not Implemented
            KEY_RED: -1, // Not Implemented
            KEY_VOL_UP: -1, // Not Implemented
            KEY_VOL_DOWN: -1, // Not Implemented
            KEY_MUTE: -1 // Not Implemented
        }

        //
        //	Not Implemented - Not supported in generic app
        //
        self.getRemoteControlStatus = function() {

            var dfd = $.Deferred();

            setTimeout(function() {
                logger.warning("Remote Control Interface: Error: Get Remote Control Status Not Implemented.");
                dfd.reject();
            }, 100);

            return dfd;
        };

        //
        //	Not Implemented - Not Supported in generic app
        //
        self.getButtonLockStatus = function() {

            var dfd = $.Deferred();

            setTimeout(function() {
                logger.warning("Remote Control Interface: Error: Get Button Lock Not Implemented.");
                dfd.reject();
            }, 100);

            return dfd;
        };

        //
        //	Not Implemented - Not Supported in generic app
        //
        self.getSafetyLockStatus = function() {

            var dfd = $.Deferred();

            setTimeout(function() {
                logger.warning("Remote Control Interface: Error: Get Safety Lock Status Not Implemented.");
                dfd.reject();
            }, 100);

            return dfd;
        };

    }

    // The object is a singleton as there is only a single plugin added to the application
    return new RemoteControlInterface();
});
rjs.define('models/SystemReport_SystemSettingsModel',["system/power/PowerInterface", "system/sound/SoundInterface", "system/graphics/GraphicsInterface", "system/remotecontrol/RemoteControlInterface"], function(PowerInterface, SoundInterface, GraphicsInterface, RemoteControlInterface) {
	/*
		System Settings Model - Provides a model for System Settings
		-------------------------------------------------------------------------
		This model uses a number of interfaces to populate its data including:
		- system/power/PowerInterface
		- system/sound/SoundInterface
		- system/graphics/GraphicsInterface
		- system/remotecontrol/RemoteControlInterface

		It originally used system/plugins/LFDPlugin for all of its methods but the data
		and functions exposed in this class have been split out into a number of Interface
		classes to provide a more modular approach to making low level panel functions and information
		available to the application and system reporting model.
	*/
	function SystemReport_SystemSettingsModel() {

		var self = this;

		//
		// Attempts to populate the system settings report model
		//
		self.generate = function() {
			var dfd = $.Deferred();

			// Get Picture Contrast
			GraphicsInterface.getContrastRGB().done(function(data) {
				self.contrast = data;
			}).always(function() {

				// Get picture brightness
				GraphicsInterface.getBrightnessRGB().done(function(data) {
					self.brightness = data;
				}).always(function() {

					// Get volume level
					SoundInterface.getVolume().done(function(data) {
						self.volume = data;
					}).always(function() {

						// Get mute status
						SoundInterface.getMuteStatus().done(function(muteStatus) {
							if (muteStatus)
							{
								self.mute_status = "muted";
							}
							else
							{
								self.mute_stated = "not muted";
							}
						}).always(function() {

							// Get remote control status
							RemoteControlInterface.getRemoteControlStatus().done(function(data) {
								self.remote_control_status = data;
							}).always(function() {

								// Get saftey lock status
								RemoteControlInterface.getSafetyLockStatus().done(function(data) {
									self.saftey_lock_status = data;
								}).always(function() {

									// Get button lock status
									RemoteControlInterface.getButtonLockStatus().done(function(data) {
										self.button_lock_status = data;
									}).always(function() {

										// Get screen on/off status
										PowerInterface.getScreenOnOffStatus().done(function(data) {
											self.screen_status = data;
										}).always(function() {

											// Resolve system settings information generation
											dfd.resolve();
										});
									});
								});
							});
						});
					});
				});
			});

			return dfd;
		};
	}

	return SystemReport_SystemSettingsModel;
});

rjs.define('models/SystemReportModel',['models/SystemReport_SystemModel', 'models/SystemReport_ClientModel', 'models/SystemReport_CPUModel', 'models/SystemReport_MemoryModel', 'models/SystemReport_StorageDeviceModel', 'models/SystemReport_TemperatureModel', 'models/SystemReport_GraphicsDeviceModel', 'models/SystemReport_NetworkAdapterModel', 'models/SystemReport_SystemSettingsModel', 'models/UserConfigurationModel'], function(SystemReport_SystemModel, SystemReport_ClientModel, SystemReport_CPUModel, SystemReport_MemoryModel, SystemReport_StorageDeviceModel, SystemReport_TemperatureModel, SystemReport_GraphicsDeviceModel, SystemReport_NetworkAdapterModel, SystemReport_SystemSettingsModel, UserConfigurationModel) {
	/*
		System Report Model - Provides a model for Signagelive System Reports
		-------------------------------------------------------------------------
		Use the generate method to 
	 */
	function SystemReportModel() {
		var self = this;

		//
		// Generates the system report
		//
		self.generate = function() {
			var dfd = $.Deferred();

			// System report properties
			self.system = null;
			self.client = null;
			self.cpus = [];
			self.memory = null;
			self.storage_devices = [];
			self.temperature = [];
			self.graphics_devices = [];
			self.network_adapters = [];
			self.installed_applications = [];
			self.running_processes = [];
			self.app_settings = {};
			self.system_settings = {};

			// System Report Models
			var _system = new SystemReport_SystemModel();
			var _client = new SystemReport_ClientModel();
			var _cpu = new SystemReport_CPUModel();
			var _memory = new SystemReport_MemoryModel();
			var _storage = new SystemReport_StorageDeviceModel();
			var _temperature = new SystemReport_TemperatureModel();
			var _graphics = new SystemReport_GraphicsDeviceModel();
			var _network = new SystemReport_NetworkAdapterModel();
			var _systemSettings = new SystemReport_SystemSettingsModel();

			// Generate system report
			_system.generate().done(function() {
				self.system = _system;
			}).always(function() {

				// Generate client report
				_client.generate().done(function() {
					self.client = _client;
				}).always(function() {

					// Generate CPU report
					_cpu.generate().done(function() {
						self.cpus.push(_cpu);
					}).always(function() {

						// Generate memory report
						_memory.generate().done(function() {
							self.memory = _memory;
						}).always(function() {

							// Generate Temperature report
							_temperature.generate().done(function() {
								self.temperature.push(_temperature);
							}).always(function() {

								// Generate graphics report
								_graphics.generate().done(function() {
									self.graphics_devices.push(_graphics);
								}).always(function() {

									// Generate network report
									_network.generate().done(function() {
										self.network_adapters = [_network];
									}).always(function() {

										// Generate system settings report
										_systemSettings.generate().done(function() {
											self.system_settings = _systemSettings;
										}).always(function() {

											// Generate storage report
											_storage.generate().done(function() {
												self.storage_devices.push(_storage);
											}).always(function() {

												// Instantiate other reporting bits
												self.app_settings = {};

												self.installed_applications = [];
												self.running_processes = [];

												// Resolve system report generation
												dfd.resolve();
												
											});
										});
									});
								});
							});
						});
					});
				});
			});

			return dfd;
		};
	}

	return SystemReportModel;
});


rjs.define('controllers/UserMessageController',[], function() {
	function UserMessageController() {
		var self = this;

		var _propertyChangedHandlers = [];

		self.addEventListener = function(callback) {
			_propertyChangedHandlers.push(callback);
		};

		self.postMessage = function(msg) {
			notifyConsumers(msg);
		};
		function notifyConsumers(event, data) {

			$(_propertyChangedHandlers).each(function(i, handler) {
				handler(event, data);
			});
		}

	}

	return new UserMessageController();
});

rjs.define('system/IDConverter',[], function() {
    function IDConverter() {
        var self = this;
        var _power = 62;

        if (!String.prototype.padStart) {
            String.prototype.padStart = function padStart(targetLength, padString) {
                targetLength = targetLength >> 0; //truncate if number, or convert non-number to 0;
                padString = String(typeof padString !== 'undefined' ? padString : ' ');
                if (this.length >= targetLength) {
                    return String(this);
                } else {
                    targetLength = targetLength - this.length;
                    if (targetLength > padString.length) {
                        padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
                    }
                    return padString.slice(0, targetLength) + String(this);
                }
            };
        }

        self.getObjectId = function(hex) {
            hex = hex.substring(3);
            return self.toInt(hex);
        }

        self.toInt = function(hex) {
            var returnValue = 0;
            for (var i = 0; i < hex.length; i++) {

                var num1 = 62;
                var num2 = (hex.length - (i + 1));
                var num3 = CharToVal(hex[i]);

                returnValue += Math.pow(num1, num2) * num3;
            }
            return returnValue;
        }

        self.toHex = function(objectTypeId, objectId) {
            try {
                var objectTypeIdHex = _toHex(objectTypeId).padStart(3, '0');
                var objectIdHex = _toHex(objectId).padStart(11, '0');
                return objectTypeIdHex + objectIdHex;
            } catch (e) {
                return e.message;
            }
        }

        function _toHex(value) {
            try {
                var x = parseInt(value % _power, 10);
                var y = parseInt(value / _power, 10);
                if (y > 0)
                    return _toHex(y) + ValToChar(x);
                else {
                    return ValToChar(x);
                }
            } catch (e) {
                log(e.message)
            }

        }

        function ValToChar(value) {
            if (value > 9) {
                var ascii = (65 + (value - 10));
                if (ascii > 90)
                    ascii += 6;
                return String.fromCharCode(ascii);
            } else
                return value.toString();
        }

        function CharToVal(value) {

            var chrval = value.charCodeAt(0);
            if (chrval > 96)
                return (chrval - 71) + 10;
            else if (chrval > 64)
                return (chrval - 65) + 10;
            else
                return parseInt(value, 10);
        }
    }
    return IDConverter;
});
rjs.define('system/serialport/SerialPortInterface',[], function() {
    function SerialPortInterface() {
        var self = this;

        var ports = {};

        self.closePorts = function()
        {
            //Close all the ports
            for (var portName in ports) {
                ports[portName].Close();
                delete ports[portName];
            }            
        };

        //To do - have some kind of process to periodically retry creating the serial port if it fails, in case it is plugged in after bootup
        self.receiveData = function(id, config,onCommandReceived) 
        {
            var portName = "PORT" + config.port;

            createReceivePort(id, portName,config,onCommandReceived);
 
        };

        function createReceivePort(id, portName, config,onCommandReceived) {

            var result = true;

            //If already created as a send port we need to close it and reopen so it can be used for both
            if (ports[portName] && !ports[portName].createdAsReceivePort) {
                ports[portName].Close();
                delete ports[portName];
            }

            //Only create the port if it is not already created
            if (!ports[portName]) {
                try{
                    var serial_input = new BSSerialPort(Number(config.port));
                    serial_input.SetBaudRate(Number(config.baudRate));
                    serial_input.SetDataBits(Number(config.dataBits));
                    serial_input.SetStopBits(Number(config.stopBits));
                    serial_input.SetParity(config.parity);
                    serial_input.SetGenerateLineEvent(true);
    
                    if (config.flowControl == "none") {
                        serial_input.SetFlowControl(false);
                    } else {
                        serial_input.SetFlowControl(true);
                    }
                    serial_input.createdAsReceivePort = true;
    
                    //serial_input.SetInverted(config.inverted);
    
                    serial_input.onserialline = function(e){
                        //logger.debug("### onserialline: " + e.sdata);
                        onCommandReceived(id, Number(config.port),e.sdata);
                    }
    
                    ports[portName] = serial_input;
                } catch(err)
                {
                    logger.error("Error creating serial port: " + err.message);
                    result = false;
                }
            }

            return result;
        }

        self.sendData = function(config, data) {

            if(!config)
            {
                logger.error("Serial config is undefined or null");
                return;
            }

            var portName =  config.name = "PORT" + config.port;

            try {

                if (!ports[portName]) {
                    var serial_output = new BSSerialPort(Number(config.port));
                    serial_output.SetBaudRate(Number(config.baudRate));
                    serial_output.SetDataBits(Number(config.dataBits));
                    serial_output.SetStopBits(Number(config.stopBits));
                    serial_output.SetParity(config.parity);

                    if (config.flowControl == "none") {
                        serial_output.SetFlowControl(false);
                    } else {
                        serial_output.SetFlowControl(true);
                    }

                    serial_output.SetInverted(config.inverted);

                    ports[portName] = serial_output;
                }
                var port = ports[portName];

                port.SendBytes(data);
            } catch (e) {
                logger.error("Error sending Serial Message " + e.message);

                //Close and remove the port so it is reinitialised next time.
                if(ports[portName] && !ports[portName].createdAsReceivePort)
                {
                    ports[portName].Close();
                    delete ports[portName];
                }
            }

        }
    }

    return new SerialPortInterface();
});
rjs.define('system/network/TCPClientInterface',[], function() {
    /*
    	TCP Client Interface - 
    */
    function TCPClientInterface() {
        var self = this;
        const Net = require('net');
        var sockets = {};

        function writeToSocket(client, data, eol, encoding) {
            data.forEach(function(cts) {
                var line = cts;

                try {
                    if (eol && eol.length > 0) {
                        switch (eol) {
                            case "CR":
                                line = line + "\r";
                                break;
                            case "LF":
                                line = line + "\n";
                                break;
                            case "CR+LF":
                                line = line + "\r\n";
                                break;
                            default:
                                //Unrecognised
                                break;
                        }
                    }

                    // Default to utf8. utf8 is the default if unspecified but specifying it for clarity
                    if (encoding == null) {
                        encoding = 'utf8';
                    }

                    switch (encoding) {
                        case 'hex':
                            // Use regex to remove invalid and unnecessary characters for usability
                            // Examples:
                            // Inputs:
                            //      55 AA 00 00 FE FF 01 FF FF FF 01 00 01 00 00 02 01 00 80 D5 5A
                            //      55, AA, 00, 00, FE, FF, 01, FF, FF, FF, 01, 00, 01, 00, 00, 02, 01, 00, 80, D5, 5A
                            //      0x55, 0xAA, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x01, 0x00, 0x80, 0xD5, 0x5A
                            //      [0x55, 0xAA, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x01, 0x00, 0x80, 0xD5, 0x5A]
                            //  Output:
                            //      55AA0000FEFF01FFFFFF010001000002010080D55A
                            line = line.replace(/[^A-Fa-f0-9]+|(?:0[xX])/g, '');
                            break;
                        default:
                            // Nothin to do for other formats right now                         
                            break;
                    }
                    // Send message
                    client.write(line, encoding);

                } catch (err) {
                    logger.error("Error sending line to socket: " + err.message);
                }
            });
        }

        //Takes a configuration and array of strings, to send them via TCP socket
        //Config has port, host and eol
        //Data is an array of strings
        self.sendData = function(config, data, encoding) {
            var dfd = $.Deferred();

            logger.debug("SendData: " + JSON.stringify(data));

            var socketName = config.host + ":" + config.port;

            if (sockets[socketName]) {
                writeToSocket(sockets[socketName], data, config.eol, encoding);

                dfd.resolve();
            } else {
                var client = new Net.Socket();

                client.connect({ port: config.port, host: config.host }, function() {
                    // If there is no error, the server has accepted the request and created a new 
                    // socket dedicated to us.
                    logger.debug('TCP connection established with the server.');

                    sockets[socketName] = client;

                    writeToSocket(sockets[socketName], data, config.eol, encoding);

                    dfd.resolve();
                });

                client.on('error', () => {
                    //Couldn't open socket so we can't send commands
                    logger.error('TCP Socket error - server not listening?');

                    //Close the socket
                    client.end();
                    if(sockets[socketName])
                        delete sockets[socketName];

                    dfd.reject();
                });

                client.on('end', () => {
                    logger.error('TCP Socket closed');
                    delete sockets[socketName];
                });
            }


            return dfd;
        };

        self.closeAll = function() {

            for (var socketName in sockets) {
                sockets[socketName].end();
                delete sockets[socketName];
            }

        };
    }

    return new TCPClientInterface();
});
rjs.define('controllers/RealTimeEventsController',["properties/App.Settings", "system/io/File", "system/IDConverter", "system/serialport/SerialPortInterface", "system/network/TCPClientInterface"], function(Settings, File, IDConverter, SerialPortInterface, TCPClientInterface) {
    function RealTimeEventsController() {
        var self = this;

        var _config = null;
        var _commandsToSend = [];
        var _tcpCommandsToSend = [];
        var _failedRequests = null;

        self.updateConfiguration = function(playerConfiguration) {
            _config = playerConfiguration;

            if (_failedRequests == null) {
                File.read(Settings.application.fileSystem.systemFiles.failedRTERequestsPath)
                    .done(function(failedRequests) {
                        _failedRequests = JSON.parse(failedRequests);
                        for (var failedRequest of _failedRequests) {
                            failedRequest.cacheExpiry = new Date(failedRequest.cacheExpiry);
                        }
                        processFailedRequestQueue();
                    }).fail(function() {
                        _failedRequests = [];

                        processFailedRequestQueue();
                    });
            }
        }

        self.sendMediaAssetDownloaded = function(mediaAsset) {
            if (_config == null) {
                return;
            }
            // send each RTE in turn
            for (var i = 0; i < mediaAsset.realTimeEvents.length; i++) {
                var rte = mediaAsset.realTimeEvents[i];
                configureAndSendRTE(rte, 'mediaAssetDownloaded', {
                    mediaAsset: mediaAsset,
                    clientDetails: _config.clientDetails
                })
            }
        }

        //var commandIndex = 0;
        self.sendMediaAssetStart = function(pma, timestamp) {

            if (_config == null) {
                return;
            }
            // send each RTE in turn
            for (var i = 0; i < pma.mediaAsset.realTimeEvents.length; i++) {
                var rte = pma.mediaAsset.realTimeEvents[i];
                configureAndSendRTE(rte, 'mediaAssetStart', {
                    pma: pma,
                    start: timestamp,
                    clientDetails: _config.clientDetails
                })
            }
        }

        self.sendMediaAssetComplete = function(pma, start, end, startReason, endReason) {
            if (_config == null) {
                return;
            }
            // send each RTE in turn
            for (var i = 0; i < pma.mediaAsset.realTimeEvents.length; i++) {
                var rte = pma.mediaAsset.realTimeEvents[i];
                configureAndSendRTE(rte, 'mediaAssetComplete', {
                    pma: pma,
                    start: start,
                    end: end,
                    startReason: startReason,
                    endReason: endReason,
                    clientDetails: _config.clientDetails
                })
            }
        }

        self.sendMediaAssetRemoved = function(mediaAsset) {
            if (_config == null) {
                return;
            }
            // send each RTE in turn
            for (var i = 0; i < mediaAsset.realTimeEvents.length; i++) {
                var rte = mediaAsset.realTimeEvents[i];
                configureAndSendRTE(rte, 'mediaAssetRemoved', {
                    mediaAsset: mediaAsset,
                    clientDetails: _config.clientDetails
                })
            }
        }

        function configureAndSendRTE(rte, eventName, data) {
            // is this event support on the RTE config?
            if (rte.events[eventName] != null) {

                if (rte.events[eventName].type == "http") {
                    var rteConfig = {}

                    // set the defaults for the request
                    $.extend(true, rteConfig, rte.events[eventName].config);

                    var placeholdersConfig = rte.events[eventName].slplaceholderConfig;
                    delete rteConfig.slplaceholderConfig;

                    // interpolate the player specific data into the configuration
                    interpolatePlayerDataInEventConfig(rteConfig, placeholdersConfig, data);

                    logger.debug('Send RTE ' + rte.name);

                    ajax(rteConfig).fail(function(requestConfig) {
                        // add to the array of failures only on the first failure
                        if (!requestConfig.firstFailure) {
                            requestConfig.firstFailure = new Date();
                            requestConfig.cacheExpiry = requestConfig.firstFailure.addSeconds(rte.cacheExpiry);

                            requestConfig.processed = false;

                            if (_failedRequests == null)
                                _failedRequests = [];
                            _failedRequests.push(requestConfig);
                        }
                    });
                } else if (rte.events[eventName].type == "serial") {
                    // _commandsToSend = [];

                    var rteConfig = {
                        "connection": {
                            "port": "2",
                            "baudRate": "115200",
                            "dataBits": "8",
                            "parity": "none",
                            "stopBits": "1",
                            "flowControl": "none",
                            "protocol": "ASCII",
                            "inverted": false,
                            "eol": "CR+LF",
                            "delay": 50
                        }
                    }

                    // override the default configuration
                    $.extend(true, rteConfig, rte.events[eventName].config);

                    // interpolate the player specific data into the configuration
                    interpolatePlayerDataInEventConfig(rteConfig, rte.events[eventName].slplaceholderConfig, data);


                    // construct the commands
                    for (var i = 0; i < rteConfig.commands.length; i++) {
                        var commandToSend = rteConfig.commands[i];

                        if (rteConfig.connection.eol == "CR+LF" && !commandToSend.startsWith("SLPAUSE("))
                            commandToSend += "\r\n";

                        _commandsToSend.push({ connection: rteConfig.connection, commandToSend: commandToSend });
                    }

                    // start the serial events processing queue if it is not running
                    if (!serialCommandQueueProcessorRunning)
                        processNextItemInSerialCommandQueue();

                } else if (rte.events[eventName].type == "tcp") {
                    _tcpCommandsToSend = [];
                    var containsPauses = false;

                    rte.events[eventName].config.commands.forEach(function(command) {
                        if (command.startsWith("SLPAUSE("))
                            containsPauses = true;

                        _tcpCommandsToSend.push(command);
                    });

                    if (containsPauses == false)
                        TCPClientInterface.sendData(rte.events[eventName].config.connection, _tcpCommandsToSend, rte.events[eventName].config.commandEncodingFormat);
                    else
                        sendTCPCommand(rte.events[eventName].config.connection, rte.events[eventName].config.commandEncodingFormat);
                }
            }
        }

        function sendTCPCommand(tcpConnectionSettings, commandEncodingFormat) {
            if (_tcpCommandsToSend.length > 0) {
                var cts = _tcpCommandsToSend.shift();

                if (cts.startsWith("SLPAUSE(")) {
                    try {
                        var pauseStr = cts.substring(cts.indexOf("(") + 1, cts.indexOf(")"));
                        logger.debug("Pause for: " + parseInt(pauseStr));
                        setTimeout(function() {
                            sendTCPCommand(tcpConnectionSettings, commandEncodingFormat);
                        }, parseInt(pauseStr));
                    } catch (err) {
                        logger.error("Error in tcp RTE pause config: " + cts);
                        sendTCPCommand(tcpConnectionSettings, commandEncodingFormat);
                    }
                } else {
                    //A little wasteful but will only be done if pauses are used
                    TCPClientInterface.sendData(tcpConnectionSettings, [cts]).done(function() {
                        sendTCPCommand(tcpConnectionSettings, commandEncodingFormat);
                    });
                }
            }
        }

        var serialCommandQueueProcessorRunning = false;

        function processNextItemInSerialCommandQueue() {


            serialCommandQueueProcessorRunning = true;

            // are there any other commands to send?
            if (_commandsToSend.length > 0) {
                var nextSerialCommandToSend = _commandsToSend.shift();

                var cts = nextSerialCommandToSend.commandToSend;
                if (cts.startsWith("SLPAUSE(")) {
                    try {
                        var pauseStr = cts.substring(cts.indexOf("(") + 1, cts.indexOf(")"));
                        logger.debug("Pause for: " + parseInt(pauseStr));
                        setTimeout(function() {

                            processNextItemInSerialCommandQueue();
                        }, parseInt(pauseStr));
                    } catch (err) {
                        logger.error("Error in serial RTE pause config: " + cts);

                    }
                } else {
                    sendSerialCommand(nextSerialCommandToSend);
                    setTimeout(function() {
                        processNextItemInSerialCommandQueue();
                    }, parseInt(nextSerialCommandToSend.connection.delay, 10));
                }
            } else {
                serialCommandQueueProcessorRunning = false;
            }
        }

        function sendSerialCommand(commandConfig) {
            var cts = commandConfig.commandToSend;
            var connection = commandConfig.connection;

            SerialPortInterface.sendData(connection, cts);
        }


        function interpolatePlayerDataInEventConfig(eventConfig, placeholdersConfig, data) {
            if (!placeholdersConfig)
                return;

            try {
                // check all the keys at this level of the JSON config object  
                for (var key in eventConfig) {
                    var value = eventConfig[key];
                    if (typeof(value) == "string") {
                        // if it is a string it is possible that it contains a placeholder
                        eventConfig[key] = replaceString(value, placeholdersConfig, data);
                    } else if (Array.isArray(value)) {
                        // check each element of the array
                        for (var arrId = 0; arrId < value.length; arrId++) {
                            // if the element is a string - replace and values otherwise recursively check the element (could be another array or an object)                                                    
                            if (typeof(value[arrId]) == "string") {
                                value[arrId] = replaceString(value[arrId], placeholdersConfig, data);
                            } else {
                                interpolatePlayerDataInEventConfig(value[arrId], placeholdersConfig, data);
                            }
                        }
                    } else if (typeof(value) == "object") {
                        // recursively check the child object
                        interpolatePlayerDataInEventConfig(value, placeholdersConfig, data);
                    }
                }
            } catch (e) {
                logger.error(e.message);
            }
        }

        function replaceString(str, placeholdersConfig, data) {
            var newRequestValue = str;
            // check to see if any off the place holders in the config are in this string - there maybe more than
            for (var i = 0; i < placeholdersConfig.length; i++) {

                var placeholderConfig = placeholdersConfig[i];

                // is the place holder in the string?
                if (str.indexOf(placeholderConfig.name) >= 0) {
                    // yes

                    // is there an explicit value set in the config to insert?
                    if (placeholderConfig.value) {
                        // there is an explicit value 
                        var insertValue = placeholderConfig.value;

                        // is it a known "special value"? 
                        // if so get the formatted value
                        if (placeholderConfig.value == "$now$") {
                            insertValue = formatValue(new Date().toString(), placeholderConfig)
                        }

                        // should we replace the entire property value or just part of it?
                        // we can determine this if the request config only has the key set as it's value
                        if (newRequestValue != placeholderConfig.name) {
                            newRequestValue = newRequestValue.replace(placeholderConfig.name, insertValue);
                        } else {
                            newRequestValue = insertValue;
                        }
                    } else {
                        // no value set - get from the data object
                        // get the value to insert into the request from the data object 

                        if (placeholderConfig.field) {
                            // some data objects specify child properties - dynamically traverse to the correct value
                            var dataKeys = placeholderConfig.field.split('.');
                            var dataValue = data;

                            // fix for widgets where Media Asset Id is in the meta data
                            // data is a PMA
                            if (placeholderConfig.field == 'pma.mediaAsset.externalId' && data.pma.mediaAsset.mimeType == 'application/widget') {
                                dataValue = data.pma.mediaAsset.metaData.media_asset_id;
                            } else if (placeholderConfig.field == 'mediaAsset.externalId' && data.mediaAsset.mimeType == 'application/widget') {
                                dataValue = data.mediaAsset.metaData.media_asset_id;
                            } else {
                                for (var z = 0; z < dataKeys.length; z++) {
                                    dataValue = dataValue[dataKeys[z]];
                                }
                            }

                            dataValue = formatValue(dataValue, placeholderConfig);

                            // insert the new data value into the proposed update to the request object
                            newRequestValue = newRequestValue.replace(placeholderConfig.name, dataValue);

                        }
                    }
                }
            }

            // set the value on the request object
            return newRequestValue;
        }

        function formatValue(newRequestValue, placeholderConfig) {
            var value;

            // format the request data
            switch (placeholderConfig.dataType) {
                case "id":
                    value = formatID(newRequestValue, placeholderConfig.format);
                    break;
                case "string":
                    // do nothing - the config is stored in strings
                    value = newRequestValue;
                    break;

                case "number":
                    value = parseInt(newRequestValue, 10);
                    break;
                case "datetime":
                    value = formatDateTime(new Date(newRequestValue), placeholderConfig.format);
                    break;
                case "boolean":
                    value = getBoolean(newRequestValue);
                    break;
                default:
                    value = newRequestValue;
                    break;
            }
            return value;
        }

        function getBoolean(value) {
            switch (value) {
                case true:
                case "true":
                case 1:
                case "1":
                case "on":
                case "yes":
                    return true;
                default:
                    return false;
            }
        }

        function formatID(value, format) {
            switch (format) {
                case "int":
                    var idConverter = new IDConverter();
                    return idConverter.getObjectId(value);
                    break;
                case "string":
                    return value;
                    break;
                default:
                    return value;
                    break;
            }
        }

        function formatDateTime(dt, format) {
            // convert to the correct date format
            var value = dt;
            switch (format) {
                case "epochSeconds":
                    value = Math.round(dt.getTime() / 1000);
                    break;
                case "ISO8601":
                    var month2Digit = (dt.getMonth() + 1);
                    if (month2Digit < 10)
                        month2Digit = "0" + month2Digit;

                    var dayOfMonth2Digit = (dt.getDate());
                    if (dayOfMonth2Digit < 10)
                        dayOfMonth2Digit = "0" + dayOfMonth2Digit;

                    var hour2Digit = (dt.getHours());
                    if (hour2Digit < 10)
                        hour2Digit = "0" + hour2Digit;

                    var minute2Digit = (dt.getMinutes());
                    if (minute2Digit < 10)
                        minute2Digit = "0" + minute2Digit;

                    var second2Digit = (dt.getSeconds());
                    if (second2Digit < 10)
                        second2Digit = "0" + second2Digit;

                    value = dt.getFullYear() + "-" + month2Digit + "-" + dayOfMonth2Digit + "T" + hour2Digit + ":" + minute2Digit + ":" + second2Digit;
                    break;
            }

            if (format.indexOf("custom") == 0 || format.indexOf("Custom") == 0) {
                var matches = format.match(/\[(.*?)\]/);

                if (matches) {
                    var customFormatter = matches[1];
                    value = customFormatter;

                    // year 4 digits
                    value = value.replace('yyyy', dt.getFullYear());

                    // month 01-12
                    var month2Digit = (dt.getMonth() + 1);
                    if (month2Digit < 10)
                        month2Digit = "0" + month2Digit;
                    value = value.replace('MM', month2Digit);

                    // month 1-12
                    value = value.replace('M', (dt.getMonth() + 1));

                    // day of month 01-31
                    var day2Digit = (dt.getDate());
                    if (day2Digit < 10)
                        day2Digit = "0" + day2Digit;
                    value = value.replace('dd', day2Digit);

                    // day of month 1-31
                    value = value.replace('d', dt.getDate());

                    // 12 hour clock
                    var hour12HourClock = dt.getHours();
                    if (dt.getHours() > 12) {
                        hour12HourClock = dt.getHours() - 12;
                    }
                    if (hour12HourClock == 0)
                        hour12HourClock = 12;

                    var hour12HourClock2Digir = hour12HourClock;
                    if (hour12HourClock2Digir < 10)
                        hour12HourClock2Digir = "0" + hour12HourClock2Digir;

                    // hour 01-12
                    value = value.replace('hh', hour12HourClock2Digir);

                    // hour 1-12
                    value = value.replace('h', hour12HourClock);

                    // 24 hour clock
                    // hour 00-23
                    var hours2Digit = (dt.getHours());
                    if (hours2Digit < 10)
                        hours2Digit = "0" + hours2Digit;
                    value = value.replace('HH', hours2Digit);

                    // hour 0-23
                    value = value.replace('H', dt.getHours());

                    // minutes 00-23
                    var minutes2Digit = (dt.getMinutes());
                    if (minutes2Digit < 10)
                        minutes2Digit = "0" + minutes2Digit;
                    value = value.replace('mm', minutes2Digit);

                    // minutes 0-23
                    value = value.replace('m', (dt.getMinutes()));

                    // seconds 00-23
                    var seconds2Digit = (dt.getSeconds());
                    if (seconds2Digit < 10)
                        seconds2Digit = "0" + seconds2Digit;
                    value = value.replace('ss', seconds2Digit);

                    // seconds 0-23
                    value = value.replace('s', (dt.getSeconds()));

                    // AM/PM
                    var ampm = 'AM';
                    if (dt.getHours() >= 12) {
                        ampm = 'PM';
                    }
                    // minutes 0-23
                    value = value.replace('tt', (ampm));

                    // minutes 0-23
                    value = value.replace('t', ampm.substring(0, 1));
                }
            }
            return value;
        }



        function ajax(config) {
            var dfd = $.Deferred();

            if (config == null) {
                dfd.resolve(config);
            } else {
                var ajaxConfig = $.extend({ cache: false }, config, true);

                try {
                    if (ajaxConfig.data) {
                        ajaxConfig.data = JSON.stringify(ajaxConfig.data);
                    }

                    if (ajaxConfig.headers) {
                        var headersArr = ajaxConfig.headers;
                        var headersObj = {};

                        for (var i = 0; i < headersArr.length; i++) {
                            headersObj[headersArr[i].name] = headersArr[i].value;
                        }

                        ajaxConfig.headers = headersObj;
                    }

                    $.ajax(ajaxConfig).done(function(msg) {
                        logger.debug("Real Time Data sent: " + msg);

                        dfd.resolve(config);

                    }).fail(function(jqXHR, textStatus, errorThrown) {
                        logger.error("Real Time Data error " + jqXHR.status);

                        dfd.reject(config);
                    })
                } catch (e) {
                    logger.error("Real Time Data error " + e.message);

                    dfd.resolve(config);
                }
            }
            return dfd;
        }

        function processFailedRequestQueue() {
            if (_failedRequests.length > 0) {

                processNextItemInFailedRequestQueue(0);
            } else {
                // check again in 1 min for other failures
                setTimeout(processFailedRequestQueue, 60000);
            }
        }

        function processNextItemInFailedRequestQueue(index) {
            // have we got to the end of the queue?
            if (index == _failedRequests.length) {

                // remove all successfully requests from the queue
                _failedRequests = _failedRequests.filter(function(failedRequest) {
                    return (failedRequest.processed == false);
                });

                // write to disk
                File.write(Settings.application.fileSystem.systemFiles.failedRTERequestsPath, JSON.stringify(_failedRequests));

                setTimeout(processFailedRequestQueue, 60000);
            } else {
                var ajaxConfig = _failedRequests[index];

                if (ajaxConfig.processed) {
                    // this request has been processed already - process the next one instanty
                    processNextItemInFailedRequestQueue(index + 1);
                } else {

                    // is the request greater than the validity period?
                    var now = new Date();
                    if (now >= ajaxConfig.cacheExpiry) {
                        // mark it as processed so it is removed from the collection
                        ajaxConfig.processed = true;

                        // process the next item
                        processNextItemInFailedRequestQueue(index + 1);
                    } else {
                        ajax(ajaxConfig).done(function(config) {
                            config.processed = true;
                        }).fail(function(config) {
                            config.processed = false;
                        }).always(function() {
                            // process the next request in 1 second
                            setTimeout(function() {
                                processNextItemInFailedRequestQueue(index + 1);
                            }, 1000);
                        });
                    }
                }
            }
        }
    }

    return new RealTimeEventsController();
});
rjs.define('models/DeviceInfoModel',["properties/App.Properties","system/SystemInterface", "properties/App.Settings"], function(Properties,SystemInterface, Settings) {
	function DeviceInfoModel()
	{
		var self = this;
		self.manufacturer = "";
		self.model = "";
		self.client_version = Properties.appVersion;
        self.firmware_version = "";
        self.client_id =  Settings.application.api.clientId;

        self.generate = function() {
            var dfd = $.Deferred();

            SystemInterface.getOSVersion().done(function(osVersion){
                self.firmware_version = osVersion;
                }).always(function(){

                    // Get manufacturer
                    SystemInterface.getManufacturer().done(function(manufacturer){
                        self.manufacturer = manufacturer;
                    }).always(function() {
                        SystemInterface.getModel().done(function(modelName){
                            self.model = modelName;
                        }).always(function() {
                            dfd.resolve();
                        });
                    });
            });
            
			return dfd;
		};
    }

    return DeviceInfoModel;
});

rjs.define('models/API',[],function(){
	return {
		PLAYER_API:"PlayerAPI",
		SCREENSHOT_UPLOAD:"ScreenshotUploadAPI",
		PLAYER_DATA:"PlayerDataAPI",
        WEB_TRIGGERS:"WebTriggersAPI"
	}
});
rjs.define('core/SLAPIMethodsInterface',["properties/App.Settings","properties/App.Properties", "models/API", "system/DateTime","system/io/File"], function(Settings, Properties, API, DateTime, File) {
    function SLAPIMethodsInterface() {
    
        var self = this;

        function getEndpoint(api) {
            var apiEndpoint = "";
            switch(api) {
                case API.PLAYER_API:
                    apiEndpoint = Settings.application.api.url;
                    break;
                case API.SCREENSHOT_UPLOAD:
                    apiEndpoint = Settings.application.screenshotuploadapi.url;
                    break;
                case API.PLAYER_DATA:
                    apiEndpoint = Settings.application.playerdataapi.url;
                    break;
                case API.WEB_TRIGGERS:
                    apiEndpoint = Settings.application.webBasedInterrupts.url;
                    break;
            }
            return apiEndpoint;
        }

        self.authenticate = function(api) {
            var dfd = $.Deferred();

            logger.info('auth about to be called');
            var jqXHR = self.executeAPICall(api,'authenticate', {
                type: 'POST'
            });

            jqXHR.done(function(data, textStatus, jqXHR) {
                logger.info('authenticated');
                // set the auth token
                Settings.application.api.authToken = data.token;
                Settings.application.api.authTokenExpiry = DateTime.now().addMinutes(30);

                // We are authenticated
                dfd.resolve(true);
            }).fail(function(jqXHR, textStatus, errorThrown) {
                logger.debug('auth failed');
                // log the error
                logAPIError(this.url, this.data, jqXHR.status, errorThrown, jqXHR.responseText);

                dfd.resolve(false);
            });

            return dfd;
        };

        self.executeAPICall = function(api, operationName, settings) {
            try {
                var apiEndpoint = getEndpoint(api);
                // Request defaults
                var type = 'GET';
                var async = true;
                var data = null;
                var contentType = 'application/json';
                var dataType = null;
                var timeout = 60000;

                // Merge/Override passed Settings
                if (settings != undefined) {
                    if (settings.type != undefined) {
                        type = settings.type;
                    }
                    if (settings.async != undefined) {
                        async = settings.async;
                    }
                    if (settings.data != undefined) {
                        if (api == API.PLAYER_DATA && operationName == 'proofofplay') {
                            data = settings.data;
                        } else {
                            data = JSON.stringify(settings.data);
                        }
                    }
                    if (settings.contentType != undefined) {
                        contentType = settings.contentType;
                    }
                    if (settings.dataType != undefined) {
                        dataType = settings.dataType;
                    }
                    if (settings.timeout != undefined) {
                        timeout = settings.timeout;
                    }
                }

                if ((settings == undefined || settings.dataType == undefined) && type != 'HEAD') {
                    dataType = 'json';
                }
                if (api == API.PLAYER_DATA && operationName == 'proofofplay') {
                    contentType = 'text/plain;charset=UTF-8;';
                    dataType = 'text';
                    timeout = 0;
                }

                logger.info(api + ': executeAPICall ' + type + ' ' + operationName + ', URL: ' + apiEndpoint);

                if (api != API.WEB_TRIGGERS) {
                    logger.info(api + ': API Credentials - Client ID: ' + Settings.application.api.clientId + ', Token: ' + Settings.application.api.authToken + ", Expiry: " + Settings.application.api.authTokenExpiry);
                }

                // Do request
                return $.ajax({
                    url: apiEndpoint + "/" + operationName,
                    type: type,
                    contentType: contentType,
                    dataType: dataType,
                    async: async,
                    data: data,
                    timeout: timeout,
                    beforeSend: function(xhr, settings) {
                        xhr.setRequestHeader('X-SIGNAGELIVE-APP-ID', Settings.application.api.appId);
                        xhr.setRequestHeader('X-SIGNAGELIVE-APP-KEY', Settings.application.api.appKey);
                        xhr.setRequestHeader('X-SIGNAGELIVE-CLIENT-ID', Settings.application.api.clientId);
                        xhr.setRequestHeader('X-SIGNAGELIVE-CLIENT-KEY', Settings.application.api.clientKey);
                        xhr.setRequestHeader('X-SIGNAGELIVE-AUTH-TOKEN', Settings.application.api.authToken);
                        if (Settings.application.api.temp && Settings.application.api.temp.setOldClientIdHeader === true) {
                            xhr.setRequestHeader('X-SIGNAGELIVE-OLD-CLIENT-ID', Settings.application.api.temp.oldClientId);
                        }
                        if (Properties.appBranding) {
                            xhr.setRequestHeader('X-SIGNAGELIVE-INSTALL-ID', Properties.appBranding);
                        }
                        else {
                            if (Settings.application.api.brand !== 'Signagelive') {
                                xhr.setRequestHeader('X-SIGNAGELIVE-INSTALL-ID', Settings.application.api.brand);
                            }
                        }
                    },
                    error : function (jqXHR, textStatus,  errorThrown ){
                        logger.error('Error executing api call ' + errorThrown);
                    },
                    complete : function (jqXHR, textStatus){
                        logger.debug('COMPLETE: API: ' + api + ': executeAPICall ' + type + ' ' + operationName + ', URL: ' + apiEndpoint + ', Status: ' + textStatus);
                    }
                });
            } catch (e) {
                logger.error(api + ': Error executing api call ' + e.message);
                return null;
            }
        };


        function logAPIError(requestUrl, requestData, responseStatus, responseMessage, responseText) {
            var logData = {
                url: requestUrl,
                requestData: requestData,
                responseStatus: responseStatus,
                responseMessage: responseMessage,
                responseBody: responseText
            };

            logger.error("PLAYER DATA API: API Error " + JSON.stringify(logData));
        }
       
    }

    return SLAPIMethodsInterface;
});
String.prototype.startsWith||Object.defineProperty(String.prototype,"startsWith",{value:function(t,r){var e=r>0?0|r:0;return this.substring(e,e+t.length)===t}});

rjs.define('controllers/WidgetEventController',["system/io/File", "properties/App.Settings", "properties/App.Properties", "system/SystemInterface", "system/graphics/GraphicsInterface","system/network/NetworkInterface", "system/io/DirectoryInfo", "models/ConfigurationModel"], function(File, Settings, Properties, SystemInterface, GraphicsInterface, NetworkInterface, DirectoryInfo, ConfigurationModel) {
    function WidgetEventController() {

        var self = this;
        self.viewers = [];
		self.layouts = [];

        var readyEventAdditionalWaitInMS = 250; // wait half a second before showing widget
        var readyEventTimeout = null;

        // Setup event handler
        self.initialize = function() {
            logger.info("Widget event controller initialized");
            window.removeEventListener("message", widgetEventHandler);
            window.addEventListener("message", widgetEventHandler);
        };

        self.stop = function(parentLayoutId) {
            clearTimeout(readyEventTimeout);
            self.viewers[parentLayoutId] = [];
        };

        function widgetEventHandler(event) {
            logger.debug('Widget SDK event received');
            if (event && event.data) {
                // Parse data
                var data = {};
                try {
                    if (typeof event.data === "string") {
                        data = JSON.parse(event.data);
                    }
                    else {
                        data = event.data;
                    } 
                } catch (error) {
                    logger.error('Unable to parse event data');
                    return;
                }
                // AgentM
                if (data.name && data.name.includes("Agent")) {
                    return;
                }
                // Check we have a method and if so continue to process request and response
                if (data.method) {
                    logger.info('Widget SDK event: ' + data.method);
                    if (data.params && data.params.constructor === Array) {
                        data.params.unshift(data.windowId); // If array then push windowId to front so it is the first param passed to all methods
                    } else {
                        data.params = [data.windowId]; // If no params are passed then the windowId is our only param
                    }
                    var childIframe = document.getElementById(data.windowId);
                    //var methodToCall = self[data.method];
                    var methodToCall = data.method;
                    // If the player is using an older version of the SDK then it is possible they will just send the callback rather than the success and error callbacks
                    var successCallback = data.successCallback
                    ? data.successCallback
                    : data.callback;
                    if (methodToCall) {
                        processRequestAndResponse(childIframe, methodToCall, data.params, data.messageId, successCallback, data.errorCallback);
                    } else {
                        logger.error('Requested method not supported');
                    }
                } else {
                    // Ignore as this is likely not related to the SL Widget SDK - could be a meldCX postMessage or something else
                    //logger.error('Invalid event data');
                }
            } else {
                logger.error('Invalid event data');
            }
        }

        function processRequestAndResponse(iframe, methodToCall, params, messageId, successCallback, errorCallback) {
            // This code originally used Function.apply() to dynamically pass the params to the method to call.
            // This function is unsupported on webOS 2 so this code was modified to call the implemented methods
            // using a switch statement instead.
            var requestProcessed = null;
            switch (methodToCall)
            {
                case "ready":
                    var windowId = params[0];
                    requestProcessed = self.ready(windowId);
                    break;
                case "getData":
                    var windowId = params[0];
                    var key = params[1];
                    var shared = params[2];
                    requestProcessed = self.getData(windowId, key, shared);
                    break;
                case "setData":
                    var windowId = params[0];
                    var key = params[1];
                    var value = params[2];
                    var shared = params[3];
                    requestProcessed = self.setData(windowId, key, value, shared);
                    break;
                case "playVideo":
                    var windowId = params[0];
                    var uris = params[1];
                    var x = params[2];
                    var y = params[3];
                    var width = params[4];
                    var height = params[5];
                    var loop = params[6];
                    var play4k = params[7];
                    requestProcessed = self.playVideo(windowId, uris, x, y, width, height, loop, play4k);
                    break;
                case "stopVideo":
                    var windowId = params[0];
                    requestProcessed = self.stopVideo(windowId);
                    break;
                case "getPlayerDetails":
                    var windowId = params[0];
                    requestProcessed = self.getPlayerDetails(windowId);
                    break;
                case "getDisplayProperties":
                    var windowId = params[0];
                    requestProcessed = self.getDisplayProperties(windowId);
                    break;
                case "getTLSSupportInfo":
                    var windowId = params[0];
                    requestProcessed = self.getTLSSupportInfo(windowId);
                    break;
                case "requestMediaPlayerMoveToNextAsset":
                    var windowId = params[0];
                    requestProcessed = self.requestMediaPlayerMoveToNextAsset(windowId);
                    break;
                case "getOnlineStatus":
                    var windowId = params[0];
                    requestProcessed = self.getOnlineStatus(windowId);
                    break;
                case "listStoredDataKeys":
                    var windowId = params[0];
                    var shared = params[1];
                    requestProcessed = self.listStoredDataKeys(windowId, shared);
                    break;
                case "removeData":
                    var windowId = params[0];
                    var key = params[1];
                    var shared = params[2];
                    requestProcessed = self.removeData(windowId, key, shared);
                    break;
                case "clearStorage":
                    var windowId = params[0];
                    requestProcessed = self.clearStorage(windowId);
                    break;
                case "getExternalData":
                        var windowId = params[0];
                        var dataSource = params[1];
                        var integrationId = params[2];
                        var objectId = params[3];
                        requestProcessed = self.getExternalData(windowId, dataSource, integrationId, objectId);
                        break;
                default:
                    // Default response - Unsupported
                    requestProcessed = $.Deferred().reject({"reason": "Unsupported method call"});
                    break;
            };
            // Each method returns a deferred object. Once done we post the result back to the content Window
            requestProcessed.done(function(result) {
                if (successCallback) {
                    var responseMessage = {
                        messageId: messageId,
                        method: successCallback,
                        returnValue: result
                    }
                    if (iframe) {
                        iframe.contentWindow.postMessage(JSON.stringify(responseMessage), '*');
                    }
                }
            });
            requestProcessed.fail(function(error) {
                // If using the original version of the SDK there will not be an error callback so just
                // use the success callbak returning the error object
                var callbackToUse = errorCallback
                    ? errorCallback
                    : successCallback;
                if (callbackToUse) {
                    var responseMessage = {
                        messageId: messageId,
                        method: callbackToUse,
                        returnValue: error
                    }
                    if (iframe) {
                        iframe.contentWindow.postMessage(JSON.stringify(responseMessage), '*');
                    }
                }
            });
        }

        self.registerWidgetViewer = function(viewer, parentLayoutId) {
			if (!self.viewers[parentLayoutId]) {
				self.viewers[parentLayoutId] = [];
                self.layouts.push(parentLayoutId);
			}
            var existingViewers = self.viewers[parentLayoutId].filter(function(existingViewer) {
                return existingViewer.id === viewer.id;
            });

            if (existingViewers.length === 0) {
                self.viewers[parentLayoutId].push(viewer);
            }
        };

        self.unregisterWidgetViewer = function(viewer, parentLayoutId) {
			if (!self.viewers[parentLayoutId]) {
				self.viewers[parentLayoutId] = [];
                self.layouts.push(parentLayoutId);
			}

            // Remove the viewer for the layout
            self.viewers[parentLayoutId] = self.viewers[parentLayoutId].filter(function(existingViewer) {
                return existingViewer.id !== viewer.id;
            });
        };      

        self.notifyWidgetsOfExternalDataUpdate = function(mediaAsset, integration, data) {
            var windowsThatNeedToBeNotified = getWindowsByMediaAssetId(mediaAsset.externalId);
            logger.info('Number of Windows to update: ' + windowsThatNeedToBeNotified.length);
            $.each(windowsThatNeedToBeNotified, function(index, window) {
                var responseData = {
                    dataSource: integration.dataSource,
                    integrationId: integration.integrationId,
                    objectId: integration.objectId, //NOTE: or should this be integration name
                    data: data
                };
                sendEventMessage(window.$activeViewer.attr('id'), "onExternalDataUpdate", responseData);
            });
        };

        // IMPLEMENTED EVENTS
        // ------------------------

        // Get data using file store - defaults to a namespace unique to zone and widget but can be stored globally if global is true
        self.getData = function(windowId, key, shared) {
            var window = getWindowById(windowId)
            if (window == null) {
                logger.warning('Widget attempted to get data but no matching viewer found');
                return $.Deferred().reject({"reason": "Viewer not found"});
            }
            // Key to get data. If global is false then this will use isolated storage for this zone and asset.
            // If global is true then it will just store the key as is risking collissions with other widget data
            var fullKey = 'widget_data_' + key;
            if (shared !== true) { // false or undefined
                fullKey += '_' + windowId.substring(0, windowId.length - 2);
            }
            // Figure out path to file
            var widgetBasePath = '';
            if (Settings.application.fileSystem.root !== "") {
                widgetBasePath += Settings.application.fileSystem.root + "/";
            }
            if (Settings.application.fileSystem.widgetDirectory !== "") {
                widgetBasePath += Settings.application.fileSystem.widgetDirectory + '/';
            }
            widgetBasePath += window.mediaAsset.localFileName.replace('.wgt', '') + '/';
            var path = widgetBasePath + fullKey + '.json';
            var dfd = $.Deferred();
            File.read(path)
                .done(function(data) {
                    logger.info('Returning widget data');
                    dfd.resolve(String.fromCharCode.apply(null, data)); //NOTE: Or should this just resolve the data?
                })
                .fail(function() {
                    logger.error('Error getting widget data');
                    dfd.resolve(null);
                });
            return dfd;
        };

        // Store data in file store - defaults to a namespace unique to zone and widget but can be stored globally if global is true
        self.setData = function(windowId, key, value, shared) {
            var window = getWindowById(windowId)
            if (window == null) {
                logger.warning('Widget attempted to store data but no matching viewer found');
                return $.Deferred().reject({"reason": "Viewer not found"});
            }
            // Key to get data. If global is false then this will use isolated storage for this zone and asset.
            // If global is true then it will just store the key as is risking collissions with other widget data
            var fullKey = 'widget_data_' + key;
            if (shared !== true) { // false or undefined
                fullKey += '_' + windowId.substring(0, windowId.length - 2);
            }
            // Figure out path to file
            var widgetBasePath = '';
            if (Settings.application.fileSystem.root !== "") {
                widgetBasePath += Settings.application.fileSystem.root + "/";
            }
            if (Settings.application.fileSystem.widgetDirectory !== "") {
                widgetBasePath += Settings.application.fileSystem.widgetDirectory + '/';
            }
            widgetBasePath += window.mediaAsset.localFileName.replace('.wgt', '') + '/';
            var path = widgetBasePath + fullKey + '.json';
            var dfd = $.Deferred();
            File.write(path, value)
                .done(function() {
                    logger.info('Widget data stored in file');
                    dfd.resolve(true);
                })
                .fail(function(error) {
                    logger.error('Error writing widget data to storage: ' + error);
                    dfd.resolve(false)
                });
            return dfd;
        };

        self.listStoredDataKeys = function(windowId, shared) {
            var window = getWindowById(windowId)
            if (window == null) {
                logger.warning('Widget attempted to store data but no matching viewer found');
                return $.Deferred().reject({"reason": "Viewer not found"});
            }
            // Figure out path to file
            var dfd = $.Deferred();
            var widgetBasePath = '';
            if (Settings.application.fileSystem.root !== "") {
                widgetBasePath += Settings.application.fileSystem.root + "/";
            }
            if (Settings.application.fileSystem.widgetDirectory !== "") {
                widgetBasePath += Settings.application.fileSystem.widgetDirectory + '/';
            }
            widgetBasePath += window.mediaAsset.localFileName.replace('.wgt', '') + '/';

            var widgetDirInfo = new DirectoryInfo(widgetBasePath);
            widgetDirInfo.initialize().always(function() {
                var matchingKeysToReturn = [];
                $(widgetDirInfo.files).each(function(i, file) {
                    var key = file.name.replace('widget_data_', '').replace('.json', '').replace('_' + windowId.substring(0, windowId.length - 2), '');
                    if (file.name.startsWith('widget_data_')) {

                        if (shared === true) {
                            matchingKeysToReturn.push(key);
                        }
                        else {
                            if (file.name.includes(windowId.substring(0, windowId.length - 2))) {
                                matchingKeysToReturn.push(key);
                            }
                        }
                    }
                });
                dfd.resolve(matchingKeysToReturn);
            });
            return dfd;
        };

        self.removeData = function(windowId, key, shared) {
            var window = getWindowById(windowId)
            if (window == null) {
                logger.warning('Widget attempted to remove data but no matching viewer found');
                return $.Deferred().reject({"reason": "Viewer not found"});
            }
            // Key to get data. If global is false then this will use isolated storage for this zone and asset.
            // If global is true then it will just store the key as is risking collissions with other widget data
            var fullKey = 'widget_data_' + key;
            if (shared !== true) { // false or undefined
                fullKey += '_' + windowIdwindowId.substring(0, windowId.length - 2);
            }
            // Figure out path to file
            var widgetBasePath = '';
            if (Settings.application.fileSystem.root !== "") {
                widgetBasePath += Settings.application.fileSystem.root + "/";
            }
            if (Settings.application.fileSystem.widgetDirectory !== "") {
                widgetBasePath += Settings.application.fileSystem.widgetDirectory + '/';
            }
            widgetBasePath += window.mediaAsset.localFileName.replace('.wgt', '') + '/';
            var path = widgetBasePath + fullKey + '.json';
            var dfd = $.Deferred();
            File.delete(path)
                .done(function() {
                    logger.info('Successfully deleted widget data');
                    dfd.resolve(true);
                })
                .fail(function() {
                    logger.error('Error removing widget data');
                    dfd.resolve(false);
                });
            return dfd;
        };

        self.clearStorage = function(windowId) {
            var window = getWindowById(windowId)
            if (window == null) {
                logger.warning('Widget attempted to store data but no matching viewer found');
                return $.Deferred().reject({"reason": "Viewer not found"});
            }
            var dfd = $.Deferred();
            // Figure out path to file
            var widgetBasePath = '';
            if (Settings.application.fileSystem.root !== "") {
                widgetBasePath += Settings.application.fileSystem.root + "/";
            }
            if (Settings.application.fileSystem.widgetDirectory !== "") {
                widgetBasePath += Settings.application.fileSystem.widgetDirectory + '/';
            }
            widgetBasePath += window.mediaAsset.localFileName.replace('.wgt', '') + '/';

            var widgetDirInfo = new DirectoryInfo(widgetBasePath);
            widgetDirInfo.initialize().always(function() {
                var filesToDelete = [];
                $(widgetDirInfo.files).each(function(i, file) {
                    if (file.name.startsWith('widget_data_')) {
                        filesToDelete.push(file);
                    }
                });
                
                // delete each file synchronously
                var deleteDfd = $.Deferred().resolve();
                filesToDelete.forEach(function(fileToDelete) {
                    deleteDfd = deleteDfd.then(function() {
                        return fileToDelete.delete();
                    });
                });

                // report that deletion process has completed
                deleteDfd.done(function() {
                    logger.debug('Old files deleted.');
                    dfd.resolve();
                });
            });
            return dfd;
        };

        // Will 
        self.ready = function(windowId) {
            var dfd = $.Deferred();
            var window = getWindowById(windowId)
            if (window) {
                logger.debug('Widget ready event recieved. Showing widget..');
                readyEventTimeout = setTimeout(function() {
                    window.show();
                }, readyEventAdditionalWaitInMS);
                dfd.resolve();
            }
            else {
                logger.warning('Matching viewer not found');
                dfd.reject({"reason": "Viewer not found"});
            }
            return dfd;
        };

        self.playVideo = function(windowId, uris, x, y, width, height, loop, play4k) {
            var window = getWindowById(windowId)
            if (window) {
                logger.info('Widget play video requested');
                return window.prepareVideo(uris, x, y, width, height, loop, play4k);
            }
            else {
                logger.warning('Matching viewer not found');
                return $.Deferred().reject({"reason": "Viewer not found"});
            }
        };

        self.stopVideo = function(windowId) {
            var window = getWindowById(windowId)
            if (window) {
                logger.info('Widget stop video requested');
                window.stopVideo();
                return $.Deferred().resolve(true);
            }
            else {
                logger.warning('Matching viewer not found');
                return $.Deferred().reject({"reason": "Viewer not found"});
            }
        };

        self.sendWidgetClosingNotification = function(windowId) {
            sendEventMessage(windowId, 'onWidgetClosingNotificationReceived', null);
        }

        self.onVideoTimeUpdate = function(windowId, currentVideo, currentTime) {
            var data = {
                video: currentVideo,
                currentTime: currentTime
            };
            sendEventMessage(windowId, 'onVideoTimeUpdate', data);
        }
        
        self.onVideoComplete = function(windowId, currentVideo) {
            logger.info('Video Complete: ' + currentVideo);
            var data = {
                video: currentVideo,
            };
            sendEventMessage(windowId, 'onVideoComplete', data);
        }

        self.onVideoStopped = function(windowId, currentVideo) {
            logger.info('Video Stopped: ' + currentVideo);
            var data = {
                video: currentVideo,
            };
            sendEventMessage(windowId, 'onVideoStopped', data);
        }

        self.onVideoError = function(windowId, currentVideo, errorMessage) {
            logger.info('Video Error: ' + currentVideo + '. Error: ' + errorMessage);
            var data = {
                video: currentVideo,
                errorMessage: errorMessage
            };
            sendEventMessage(windowId, 'onVideoError', data);
        }

        function sendEventMessage(windowId, eventName, data) {
            var iframe = document.getElementById(windowId);
            var responseMessage = {
                messageId: 'videoEvent',
                method: eventName,
                returnValue: data
            }
            if (iframe) {
                iframe.contentWindow.postMessage(JSON.stringify(responseMessage), '*');
            }
        }

        self.getExternalData = function(windowId, dataSource, integrationId, objectId) {
            // dataSource is always DSS right now so can be ignored
            var window = getWindowById(windowId);
            if (window) {
                // Data dir
                var dataFilePath = '';
                if (Settings.application.fileSystem.root !== "") {
                    dataFilePath += Settings.application.fileSystem.root + "/";
                }
                if (Settings.application.fileSystem.widgetDirectory !== "") {
                    dataFilePath += Settings.application.fileSystem.widgetDirectory + '/';
                }
                dataFilePath += window.mediaAsset.localFileName.replace('.wgt', '') + "/";
                dataFilePath += Settings.application.fileSystem.integrationsWidgetSubdirectory + "/";
                dataFilePath += integrationId + "/";
                dataFilePath += objectId + "/data.json";

                var dfd = $.Deferred();
                File.read(dataFilePath)
                    .then(function(data) {
                        try {
                            var deserializedData = JSON.parse(data);
                            dfd.resolve(deserializedData);
                        }
                        catch(error) {
                            logger.error(error);
                            dfd.reject({"reason": error});
                        }
                    })
                    .catch(function(error) {
                        logger.error(error);
                        dfd.reject({"reason": error});
                    });
                return dfd;

            }
            else {
                logger.warning('Matching viewer not found');
                return $.Deferred().reject({"reason": "Viewer not found"});
            }
        };

        self.getPlayerDetails = function(windowId) {
            var window = getWindowById(windowId);
            if (window) {
                return self._getPlayerDetails();
            }
            else {
                logger.warning('Matching viewer not found');
                return $.Deferred().reject({"reason": "Viewer not found"});
            }
        }

        self._getPlayerDetails = function() {
            var dfd = $.Deferred();
            var details = {};
			try {
                // Read Config file
				File.read(Settings.application.fileSystem.systemFiles.lastConfigFilePath).done(function(data) {
					// Load Config
					var configuration = new ConfigurationModel(JSON.parse(data));
					// Populate configuration data
					if (configuration && configuration.clientDetails)	
					{
                        var clientDetails = configuration.clientDetails;
                        details.configuration_change_id = configuration.changeId;
                        details.serial_number = clientDetails.signageliveSerialNumber;
                        details.site_description = clientDetails.siteDescription;
                        details.address_1 = clientDetails.address1;
                        details.address_2 = clientDetails.address2;
                        details.city = clientDetails.city;
                        details.state = clientDetails.state;
                        details.zip_code = clientDetails.zipCode;
                        details.reference_code_1 = clientDetails.referenceCode1;
                        details.reference_code_2 = clientDetails.referenceCode2;
                        details.reference_code_3 = clientDetails.referenceCode3;
                        details.reference_code_4 = clientDetails.referenceCode4;
                        details.reference_code_5 = clientDetails.referenceCode5;
                        details.reference_code_6 = clientDetails.referenceCode6;
                        details.reference_code_7 = clientDetails.referenceCode7;
                        details.reference_code_8 = clientDetails.referenceCode8;
                        details.reference_code_9 = clientDetails.referenceCode9;
                        details.reference_code_10 = clientDetails.referenceCode10;
                        
                        details.utc_offset = clientDetails.utcOffset;
                        details.client_version = Properties.appVersion;
                        details.client_build_number = Properties.appBuildNumber;
                        details.player_type = SystemInterface.getPlayerType();
                        SystemInterface.getModel().always(function(model) {
                            details.model = model;
                            details.deviceSeries = Settings.application.features.deviceSeries;
                            SystemInterface.getManufacturer().always(function(manufacturer) {
                                details.manufacturer = manufacturer;
                                SystemInterface.getOSVersion().always(function(osVersion){
                                    details.os_version = osVersion;
                                    SystemInterface.getTimeZone().always(function(tz) {
                                        details.time_zone = tz;
                                        details.firmware_version = SystemInterface.getFirmwareVersionSync();
                                        dfd.resolve(details);
                                    });
                                });
                            });
                        });
					}
				})
				.fail(function() {
					logger.error("Error loading device properties. Unable to read config file.");
                    dfd.reject({"reason": "Unable to read player configuration file"});
				});
			} catch(e) {
				logger.error('Error loading device properties: ' + e.message);
				dfd.reject({"reason": "Error loading device properties"});
			}
            return dfd;
        };

        self.getDisplayProperties = function(windowId) {
            var window = getWindowById(windowId);
            if (window) {
                return GraphicsInterface.getDisplayProperties();
            }
            else {
                logger.warning('Matching viewer not found');
                return $.Deferred().reject({"reason": "Viewer not found"});
            }
        };

        self.getTLSSupportInfo = function(windowId) {
            var window = getWindowById(windowId);
            if (window) {
                return SystemInterface.getMaxSupportedTLSVersion();
            }
            else {
                logger.warning('Matching viewer not found');
                return $.Deferred().reject({"reason": "Viewer not found"});
            }
        };

        self.requestMediaPlayerMoveToNextAsset = function(windowId) {
            var window = getWindowById(windowId);
            if (window) {
                return window.requestMediaPlayerMoveToNextAsset();
            }
            else {
                logger.warning('Matching viewer not found');
                return $.Deferred().reject({"reason": "Viewer not found"});
            }
        };

        self.getOnlineStatus = function(windowId) {
            var window = getWindowById(windowId);
            if (window) {
                return NetworkInterface.getOnlineStatus();
            }
            else {
                logger.warning('Matching viewer not found');
                return $.Deferred().reject({"reason": "Viewer not found"});
            }
        };

        function getWindowById(windowId) {
            var viewerId = windowId.substring(0, windowId.length - 2);
			var layoutId = windowId.substring(0, windowId.length - 15);

            var existingViewers = null;

            // Due to the layout transition issues we need to iterate over each parent layout ID that has been initialised
            self.layouts.forEach(function(parentLayoutId, i) {
                if((!existingViewers) || (existingViewers && existingViewers.length == 0)) {
                    existingViewers = self.viewers[parentLayoutId].filter(function(existingViewer) {
                        return existingViewer.id === viewerId;
                    }).filter(function(val) { return val !== 0 });
                }
            });

            var window = (existingViewers.length === 1)
                ? existingViewers[0]
                : null;

            return window;
        }

        function getWindowsByMediaAssetId(maId) {
            var matchingViewers = [];
            var existingViewers = null;

            // Due to the layout transition issues we need to iterate over each parent layout ID that has been initialised
            self.layouts.forEach(function(parentLayoutId, i) {
                    existingViewers = self.viewers[parentLayoutId].filter(function(existingViewer) {
                        return existingViewer.mediaAsset && existingViewer.mediaAsset.externalId === maId;
                    }).filter(function(val) { return val !== 0 });

                    if (existingViewers.length) {
                        matchingViewers.push(existingViewers[0]);
                    }
            });

            return matchingViewers;
        }


    }

    return new WidgetEventController();
});
rjs.define('models/TokenModel',[],function() {
    function TokenModel(token) {
        var self = this;

        // Parse token
        try {
            self.token = JSON.parse(atob(token.split('.')[1]));
        }
        catch (error) {
            logger.error('Error parsing token');
            self.token = null;
        }

        self.hasClaim = function(claimToFind) {
            if (self.token == null || Object.keys(self.token).length === 0 ) {
                return false;
            }

            var matchingClaim = Object.keys(self.token).find(function(claim) {
                return claim === claimToFind;
            });

            return matchingClaim != null;
        };
    }
    return TokenModel;
});
rjs.define('models/TokenClaims',[],function(){
	return {
		CLIENT_ID: "https://www.signagelive.com/userid",
		CLIENT_SECRET: "https://www.signagelive.com/clientSecret",
		APP_ID: "https://www.signagelive.com/applicationid",
		NETRWORK_ID: "https://www.signagelive.com/networkid",
		SERIAL_NO: "https://www.signagelive.com/serialno",

		DSS: "https://www.sigangelive.com/datasyncservicesenabled",
		REMOTE_SCREENSHOTS: "https://www.sigangelive.com/remotescreenshotsenabled",
		PROOF_OF_PLAY: "https://www.sigangelive.com/proofofplayenabled",
		WEB_TRIGGERS: "https://www.sigangelive.com/webtriggersenabled",
	};
});
rjs.define('core/DSSAPIInterface',["properties/App.Settings", "system/io/File", "system/io/DirectoryInfo", "system/plugins/DownloadPlugin", "gui/PlayUrlHelper", "controllers/WidgetEventController", "models/TokenModel", "models/TokenClaims", "models/API", "core/SLAPIMethodsInterface", "system/DateTime"], function(Settings, File, DirectoryInfo, Downloader, PlayUrlHelper, WidgetEventController, TokenModel, TokenClaims, API, SLAPIMethodsInterface, DateTime) {
	function DSSAPIInterface() {
		var self = this;
		self.dssConfig = {};
		self.integrations = [];
		self.syncInProgress = false;
		self.syncTimer = null;
		self.integrationSyncTimers = {};
		self.dssSchema = null;

		var apiInterface = new SLAPIMethodsInterface();

		var REMOTE_FILE_TYPE_NAME = Settings.application.dataSyncServices.remoteFileTypeName;
		var SCHEMA_QUERY = Settings.application.dataSyncServices.schemaQuery;
		var DATA_FILE_NAME = Settings.application.dataSyncServices.dataFileName;
		var UNMODIFIED_DATA_FILE_NAME = Settings.application.dataSyncServices.unmodifiedDataFileName;
		var REMOTE_FILES_SUB_DIR_NAME = Settings.application.dataSyncServices.remoteFileSubDirectoryName;
		
		self.synchroniseIntegrationsForMediaAssets = function(dssConfig, integrations, mediaAssets, resync) {
			var syncDFD = $.Deferred();
			logger.info('synchroniseIntegrationsForMediaAssets() - Resync: ' + resync);
			// Do not recheck schema if this is a resync
			var skipSchemaUpdate = resync;
			// Only need to update the configuration and reset sync timers if this is not a resync (a config update has occurred or first run after startup)
			if (!resync) {
				// Update config and integrations list
				logger.info('Updating stored dss configuration');
				self.dssConfig = dssConfig;
				self.integrations = integrations;
				self.integrationSyncTimers = {};
				if (self.syncInProgress == true) {
					logger.info('Sync in progress....');
					self.syncTimer = setTimeout(function() {
						self.synchroniseIntegrationsForMediaAssets(dssConfig, integrations, mediaAssets);
					}, 5 * 1000);
					return syncDFD.reject();
				}
			}
			self.syncInProgress = true;
			updateDSSSchema(skipSchemaUpdate)
				.then(function() {
					// We can't continue if we do not have a schema at this point
					if (self.dssSchema == null) {
						logger.error('Unable to load dss schema. Can not continue to sync integrations');
						self.syncInProgress = false;
						return syncDFD.reject();
					}
					// Start sync for each integration sequentially
					var allIntegrationsSynced = $.Deferred().resolve();
					// logger.info('Number of integrations to sync: ' + integrations.length);
					$.each(integrations, function(index, integration) {
						logger.info('Checking Integration: ' + integration.objectId);
						// Get all media Assets that reqiore data from this integration.
						// We need to know these so we can write the data to a the directory for each widget.
						// We do this so we can make the data/files more easily available to each widget
						var mediaAssetsThatUseIntegrationData = mediaAssets.filter(function(mediaAsset) {
							return mediaAsset.integrations.some(function(maIntegration) {
								return maIntegration.integrationId === integration.integrationId && maIntegration.objectId === integration.objectId;
							});
						});
						logger.info('Number of media assets using integration: ' + mediaAssetsThatUseIntegrationData.length);
						// Only resync if the integration does not have a sync timer (new) or the sync timer has expired (<= current time)
						var integrationSyncTimer = self.integrationSyncTimers[integration.objectId];
						if (integrationSyncTimer == null || integrationSyncTimer.getTime() <= new Date().getTime()) {
							allIntegrationsSynced = allIntegrationsSynced.then(function() {
								return syncIntegrationForMediaAssets(integration, mediaAssetsThatUseIntegrationData);
							});
						}
						else {
							logger.info('Integration is not scheduled to be resynced at this time.');
						}						
					});
					// When all integrations have synced
					allIntegrationsSynced
						.done(function() {
							logger.info('All integrations have been sucessfully synced');
							syncDFD.resolve();
						})
						.fail(function(err) {
							logger.error('Error syncing integrations: ' + err);
							syncDFD.reject();
						})
						.always(function() {
							// DSS main sync process runs every minute but only syncs each integration as per their schedules
							logger.info('DSS sync process will run again in 1 minute');
							self.syncInProgress = false;
							self.syncTimer = setTimeout(function() {
								self.synchroniseIntegrationsForMediaAssets(dssConfig, integrations, mediaAssets, true);
							}, 60 * 1000);
						})
				});
			return syncDFD;
		}

		// TODO: We need to cache this locally.
		// TODO: We also need to retry this if it fails for whatever reason
		function updateDSSSchema(skipSchemaUpdate) {
			if (skipSchemaUpdate) {
				return $.Deferred().resolve(self.dssSchema);
			}
			var updateSchemaDFD = $.Deferred();
			logger.info('Updating DSS Schema..');
			if (currentTokenHasDSSClaim()) {
				doHTTPSchemaCalloutToDSSAPI()
					.then(function(schemaResponse) {
						self.dssSchema = schemaResponse
						// logger.info('DSS Schema Updated: ' + JSON.stringify(self.dssSchema, undefined, 2));
						logger.info('DSS Schema Updated: ');
						updateSchemaDFD.resolve(self.dssSchema);
					})
					.fail(function(err) {
						logger.error('Error loading Schema.');
						updateSchemaDFD.resolve(self.dssSchema);
					});
			}
			else {
				// We are going to reauth with the player API and check if the new token has the required audience
				logger.info('Attempting to reauthenticate to get token with DSS audience');
				apiInterface.authenticate(API.PLAYER_API)
					.done(function(authenticated) {
						if (authenticated && currentTokenHasDSSClaim()) {
							doHTTPSchemaCalloutToDSSAPI()
								.then(function(schemaResponse) {
									self.dssSchema = schemaResponse
									// logger.info('DSS Schema Updated: ' + JSON.stringify(self.dssSchema, undefined, 2));
									logger.info('DSS Schema Updated: ');
									updateSchemaDFD.resolve(self.dssSchema);
								})
								.fail(function(err) {
									logger.error('Error loading Schema.');
									updateSchemaDFD.resolve(self.dssSchema);
								});
						}
						else {
							logger.warning('Authentication failed or does not include DSS audience');
							updateSchemaDFD.resolve(self.dssSchema);
						}
					});
			}
			return updateSchemaDFD;
		}

		function syncIntegrationForMediaAssets(integration, mediaAssets) {
			var syncIntegrationForMediaAssetsDFD = $.Deferred();
			// TODO: Websockets if supported

			// Do sync using HTTP Polling
			logger.info('Syncing Integration: ' + integration.objectId);
			doHTTPCalloutToDSSAPI(integration)
				.then(function(response) {
	
					var dssResponse = response.data;
					var unmodifiedDSSResponse = JSON.stringify(dssResponse);
					// logger.info('DSS Response: ' + JSON.stringify(dssResponse, undefined, 2));
					// Store data in local file system for each media Asset
					var integrationSyncedForAllMediaAssets = $.Deferred().resolve();
					logger.info('Storing Remote Files for media assets that require access to them..');
					$.each(mediaAssets, function (index, mediaAsset) {
						// Chain the sync process for aech media asset
						integrationSyncedForAllMediaAssets = integrationSyncedForAllMediaAssets.then(function () {
							var mediaAssetIntegrationSyncDFD = $.Deferred();
							logger.info('Checking and storing data for media asset: ' + mediaAsset.name);
							// Determine and create directory
							var widgetBasePath = '';
							if (Settings.application.fileSystem.root !== "") {
								widgetBasePath += Settings.application.fileSystem.root + "/";
							}
							if (Settings.application.fileSystem.widgetDirectory !== "") {
								widgetBasePath += Settings.application.fileSystem.widgetDirectory + '/';
							}
							widgetBasePath += mediaAsset.localFileName.replace('.wgt', '');
							// Create widget integration data sub directories
							var widgetDirInfo = new DirectoryInfo(widgetBasePath);
							var integrationsDirInfo = new DirectoryInfo(widgetDirInfo.path + "/" + Settings.application.fileSystem.integrationsWidgetSubdirectory);
							var integrationIdDirInfo = new DirectoryInfo(widgetDirInfo.path + "/" + Settings.application.fileSystem.integrationsWidgetSubdirectory + "/" + integration.integrationId);
							var integrationNameDirInfo = new DirectoryInfo(widgetDirInfo.path + "/" + Settings.application.fileSystem.integrationsWidgetSubdirectory + "/" + integration.integrationId + "/" + integration.objectId);
							var remoteFilesSubDirectoryInfo = new DirectoryInfo(widgetDirInfo.path + "/" + Settings.application.fileSystem.integrationsWidgetSubdirectory + "/" + integration.integrationId + "/" + integration.objectId + "/" + REMOTE_FILES_SUB_DIR_NAME);
							// Load the widget base dir and then all create (if required) and init all sub dirs
							// Finally write the data to disk
							// And if there are Remote Files store them too
							logger.debug('Init widget dir');
							widgetDirInfo.initialize()
								.then(function () {
									logger.debug('Create sub dir');
									return widgetDirInfo.createSubDirectory(Settings.application.fileSystem.integrationsWidgetSubdirectory)
								})
								.then(function () {
									logger.debug('Init sub dir');
									return integrationsDirInfo.initialize();
								})
								.then(function () {
									logger.debug('Create integration id dir');
									return integrationsDirInfo.createSubDirectory(integration.integrationId);
								})
								.then(function () {
									logger.debug('Init integration id dir');
									return integrationIdDirInfo.initialize();
								})
								.then(function () {
									logger.debug('Create integration object id dir');
									return integrationIdDirInfo.createSubDirectory(integration.objectId);
								})
								.then(function () {
									logger.debug('Init integration object id dir');
									return integrationNameDirInfo.initialize();
								})
								.then(function () {
									logger.debug('Create Remote Files sub dir');
									return integrationNameDirInfo.createSubDirectory(REMOTE_FILES_SUB_DIR_NAME);
								})
								.then(function () {
									logger.debug('Init Remote Files sub dir');
									return remoteFilesSubDirectoryInfo.initialize();
								})
								.then(function () {
									var unmodifiedDataFilePath = integrationNameDirInfo.path + "/" + UNMODIFIED_DATA_FILE_NAME;
									return checkIfTheDSSResponseHasChanged(dssResponse, unmodifiedDataFilePath)
								})
								.then(function (responseHasChanged) {
									if (responseHasChanged) {
										logger.info('Response has changed. Checking schema and downloading any remote files that are required...');
										checkSchemaAndDownloadRemoteFiles(dssResponse, remoteFilesSubDirectoryInfo.path)
											.then(function (filesInUse) {
												logger.debug('Store dss data: ' + JSON.stringify(dssResponse, undefined, 2));
												var dataFilePath = integrationNameDirInfo.path + "/" + DATA_FILE_NAME;
												File.write(dataFilePath, JSON.stringify(dssResponse))
													.then(function () {
														logger.debug('Store umodified DSS data: ' + unmodifiedDSSResponse);
														var unmodifiedDataFilePath = integrationNameDirInfo.path + "/" + UNMODIFIED_DATA_FILE_NAME;
														return File.write(unmodifiedDataFilePath, unmodifiedDSSResponse);
													})
													.then(function () {
														logger.info('DSS Data file written. Notifying widget(s) of external data update...');
														WidgetEventController.notifyWidgetsOfExternalDataUpdate(mediaAsset, integration, dssResponse);
														return $.Deferred().resolve();
													})
													.then(function () {
														logger.debug('Reinit the remote files sub dir so we get the latest list of files');
														remoteFilesSubDirectoryInfo = new DirectoryInfo(remoteFilesSubDirectoryInfo.path);
														return remoteFilesSubDirectoryInfo.initialize();
													})
													.then(function () {
														logger.info('Do cleanup of any files that are no longer in use...');
														return cleanupRemoteFilesNoLongerInUse(remoteFilesSubDirectoryInfo, filesInUse);
													})
													.then(function () {
														logger.info('Finished Syncing integration for media asset');
														mediaAssetIntegrationSyncDFD.resolve();
													})
													.catch(function (error) {
														logger.error('Error syncing integration: ' + error);
														mediaAssetIntegrationSyncDFD.resolve();
													});
											})
											.catch(function (error) {
												logger.error('Error syncing integration: ' + error);
												mediaAssetIntegrationSyncDFD.resolve();
											});
									}
									else {
										logger.info('DSS Data has not been updated. No need to update the stored data and notify the widget(s)');
										mediaAssetIntegrationSyncDFD.resolve();
									}
								})
								.catch(function (error) {
									logger.error('Error syncing integration: ' + error);
									mediaAssetIntegrationSyncDFD.resolve();
								});
							return mediaAssetIntegrationSyncDFD;
						});
					});
					// Return a promise that resolves once all media assets that use the integration have been synced
					return integrationSyncedForAllMediaAssets;
						
				})
				.then(function() {
					logger.info('All data files downloaded and stored. Scheduling for resync in: ' + integration.pollFrequency * 60 * 1000 + "ms");
					// Set next time that we should try to sync
					var nextSyncTime = new Date(new Date().getTime() + integration.pollFrequency * 60 * 1000);
					logger.info('Integration: ' + integration.objectId + ' next sync time: ' + nextSyncTime);
					self.integrationSyncTimers[integration.objectId] = nextSyncTime;
					// resolve
					syncIntegrationForMediaAssetsDFD.resolve();
				})
				.fail(function() {
					logger.error('An error occurred syncing integrations. Scheduling for resync in: ' + integration.pollFrequency * 60 * 1000 + "ms");
					// Set next time that we should try to sync
					var nextSyncTime = new Date(new Date().getTime() + integration.pollFrequency * 60 * 1000);
					logger.info('Integration: ' + integration.objectId + ' next sync time: ' + nextSyncTime);
					self.integrationSyncTimers[integration.objectId] = nextSyncTime;
					// reject
					syncIntegrationForMediaAssetsDFD.reject();
				});

			return syncIntegrationForMediaAssetsDFD;
		}

		function cleanupRemoteFilesNoLongerInUse(dirInfo, remoteFilesInUse) {
			var cleanupDFD = $.Deferred();
			// Figure out if there are any old files to delete
			var filesToDelete = [];
			$(dirInfo.files).each(function(i, file) {
				var cachedRemoteFiles = $.grep(remoteFilesInUse, function(remoteFile, index) {
					return remoteFile === file.path;
				});

				if (cachedRemoteFiles.length != 1) {
					filesToDelete.push(file);
				}
			});
			// Delete them
			logger.info('There are ' + filesToDelete.length + ' to delete');
			// delete each file synchronously
			var dfd = $.Deferred().resolve();
			filesToDelete.forEach(function(fileToDelete) {
				dfd = dfd.then(function() {
					return fileToDelete.delete();
				});
			});
			// report that deletion process has completed
			dfd.done(function() {
				logger.info('Remote Files no longer in use removed.');
				cleanupDFD.resolve();
			});
			return cleanupDFD;
		}

		function checkIfTheDSSResponseHasChanged(latestDSSResponse, unmodifiedDataFilePath) {
			logger.info('checking if DSS response has updated...');
			var responseChangedCheckDFD = $.Deferred();
			File.read(unmodifiedDataFilePath)
				.done(function(storedResponseString) {
					try {
						var queryName = Object.keys(latestDSSResponse)[0];
						var parsedStoredData = JSON.parse(storedResponseString);

						var storedLastUpdateTime = DateTime.parse(parsedStoredData[queryName].latestSuccessfulCaptureTimestamp);
						var latestLastUpdateTime = DateTime.parse(latestDSSResponse[queryName].latestSuccessfulCaptureTimestamp);

						logger.info(storedLastUpdateTime.toString() + ' < ' + latestLastUpdateTime.toString());

						if (storedLastUpdateTime.getTime() < latestLastUpdateTime.getTime()) {
							logger.info('DSS Response has updated');
							responseChangedCheckDFD.resolve(true);
						}
						else {
							logger.info('DSS Response has not updated');
							responseChangedCheckDFD.resolve(false);
						}

					}
					catch (error) {
						logger.warning('An error occurred loading the unmodified dss response file. It may not exist. Can not check for changes so return has changed as true');
						responseChangedCheckDFD.resolve(true);
					}
				})
				.fail(function() {
					logger.warning('An error occurred loading the unmodified dss response file. It may not exist. Can not check for changes so return has changed as true');
					responseChangedCheckDFD.resolve(true);
				});
			return responseChangedCheckDFD;
		}

		function checkSchemaAndDownloadRemoteFiles(dssResponse, remoteFilesDirPath) {
			var schemaCheckDFD = $.Deferred();

			try {
				logger.debug('Checking Schema and Handling Remote Files');
				var cachedRemoteFilesInUse = [];
				var queryName = Object.keys(dssResponse)[0];

				// Get ALL matching query definitions from the schema
				var schemaAllQueriesDefinition = self.dssSchema.data.__schema.types.find(function (type) {
					return type.kind === "OBJECT" && type.name === "Query";
				});

				// Find all queries with the same name
				var matchingQueries = schemaAllQueriesDefinition.fields.filter(function (queryDef) {
					return queryDef.name === queryName || queryDef.name === "webpage"; // Always check webpage?!
				});

				console.log(`Found ${matchingQueries.length} matching query definitions for "${queryName}"`);

				// For each query definition, find all remote file paths
				var allRemoteFilePaths = [];

				// Repeat for ALL of the matching queries
				for (var matchingQuery of matchingQueries) {
					var index = matchingQueries.indexOf(matchingQuery);
					var returnObjectTypeDefinition = getQueryReturnObjectTypeDefinitionFromSchema(self.dssSchema, matchingQueries, index);

					// If we have no matching return type then there are no RemoteFile types to check
					if (returnObjectTypeDefinition == null) {
						logger.info('No matching return object type or return object type is not an object');
						return schemaCheckDFD.resolve();
					}
					// Get a list of any RemoteFiles we need to process
					var currentPath = [queryName];
					var remoteFileTypes = findRemoteFileTypesInReturnType(returnObjectTypeDefinition.fields, currentPath, self.dssSchema.data.__schema.types);
					
					logger.debug(JSON.stringify(remoteFileTypes, undefined, 2));
					
					// If we have no remote files to download then just resolve here
					if (remoteFileTypes.length === 0) {
						logger.info('There are no remote files to download');
						return schemaCheckDFD.resolve();
					}
					// Add to total collection
					allRemoteFilePaths = allRemoteFilePaths.concat(remoteFileTypes);
				}

				function removeDuplicateObjects(arr, key) {
					var seen = {};
					return arr.filter(function(obj) {
						return seen.hasOwnProperty(obj[key]) ? false : (seen[obj[key]] = true);
					});
				}

				// Remove any duplicated paths
				var allUniqueRemoteFilePaths = removeDuplicateObjects(allRemoteFilePaths, "fieldName");

				// Otherwise we need to download the remote files and update the dss response with the local file urls
				var downloadedRemoteFiles = $.Deferred().resolve();

				logger.info(`Total remote file paths found: ${allUniqueRemoteFilePaths.length}`);
				logger.info("Remote file paths:", JSON.stringify(allUniqueRemoteFilePaths, null, 2));

				// Check each path in the response data
				console.log("\nChecking paths in the response data:");

				var filesFound = [];

				for (var remotePath of allUniqueRemoteFilePaths) {
					logger.info(`\nChecking path: ${remotePath.fieldName}`);

					if (remotePath.isList) { // LIST
						var resolvedPaths = resolveListPaths(dssResponse, remotePath.pathParts);
						logger.info(`Found ${resolvedPaths.length} concrete paths for list path`);

						for (var resolvedPath of resolvedPaths) {
							var remoteFile = getNestedObject(dssResponse, resolvedPath);
							if (remoteFile && remoteFile.url && remoteFile.name) {
								logger.info(`Found remote file at path: ${resolvedPath.join(".")}`);
								logger.info(`File URL: ${remoteFile.url}`);
								logger.info(`File Name: ${remoteFile.name}`);
								// ! If a converted file then update the filename based on the index position to make the filename unique for data sorting
								var convertedFileType = remoteFile.url.substring(remoteFile.url.lastIndexOf(".") + 1).split('?')[0];
								var convertedFileName = remoteFile.url.substring(remoteFile.url.lastIndexOf("/") + 1).split('?')[0];
								var namedFileType = remoteFile.name.substring(remoteFile.name.lastIndexOf(".") + 1);
								// ! If its a converted document then use its unique file download name - else keep the existing workflow for other DSS integrations
								remoteFile.convertedName = (convertedFileType !== namedFileType) ? convertedFileName : null;
								filesFound.push(remoteFile);
							}
						}
					} else { // OBJECT
						var remoteFile = getNestedObject(dssResponse, remotePath.pathParts);
						if (remoteFile && remoteFile.url) {
							logger.info(`Found remote file at path: ${remotePath.pathParts.join(".")}`);
							logger.info(`File URL: ${remoteFile.url}`);
							logger.info(`File Name: ${remoteFile.name}`);
							filesFound.push(remoteFile);
						} else {
							logger.info(`No file found at path: ${remotePath.pathParts.join(".")}`);
						}
					}
				}
				logger.info(`\nTotal files found: ${filesFound.length}`);

				// Download each remote file
				logger.info(`\nDownloading remote files...`);
				for (var i = 0; i < filesFound.length; i++) {
					// Use an IIFE to create a proper closure for each iteration
					(function(fileObj) {
						// Create a deep copy of the file object to avoid reference issues
						var fileCopy = {
							url: fileObj.url,
							name: fileObj.name
						};
						var fileUrl = fileCopy.url;
						
						downloadedRemoteFiles = downloadedRemoteFiles.then(function() {
							return DownloadRemoteFile(fileObj, fileUrl, remoteFilesDirPath, cachedRemoteFilesInUse);
						});
					})(filesFound[i]);
				}

				// When all RemoteFiles have been downloaded
				downloadedRemoteFiles
					.done(function() {
						logger.info('All remote files downloaded');
						schemaCheckDFD.resolve(cachedRemoteFilesInUse);
					})
					.fail(function(err) {
						logger.error('An error occurred processing all remote files');
						schemaCheckDFD.reject();
					});

			} catch(err)
			{
				logger.error('An error occurred checking the DSS schema and downloading remote files');
				schemaCheckDFD.reject();
			}
			return schemaCheckDFD;
		}

		function DownloadRemoteFile(remoteFile, fileUrl, integrationNameDirInfoPath, cachedRemoteFilesInUse) {
			var downloadFileDFD = $.Deferred();
			// ! Instead of storing the remoteFile by the original name - we store it by the name set from the API as this will be unique - fixes a bug in relation to document conversion when receiving multiple remoteFiles with the same name
			var fileName = (remoteFile.convertedName !== null) ? remoteFile.convertedName : remoteFile.name;
			var destPath = integrationNameDirInfoPath + "/" + fileName;

			// Do Download
			new Downloader().downloadFile(fileUrl, destPath)
				.then(function(remotePath, localPath) {
					logger.debug('Get play URL for: ' + localPath);
					// Add to remote files that are in use so we can ensure we do not delete it
					cachedRemoteFilesInUse.push(localPath);					
					// Get local play url for media file
					return PlayUrlHelper.getFileUrlForVirtualPath(localPath);
				})
				.then(function(playUrl) {
					logger.debug('Update screenshot src to: ' + playUrl);						
					// Timestamp the playUrl to force an update
					playUrl += '?' + new Date().getTime();
					// update dss response with local play url rather than remote url
					remoteFile.url = playUrl;
					downloadFileDFD.resolve();
				})
				.catch(function(err) {
					logger.error('Error downloading remote file: ' + err);
					downloadFileDFD.reject();
				});

			return downloadFileDFD;
		}

		function getNestedObject(nestedObj, pathArr) {
			return pathArr.reduce(
				function(obj, key) {
					return (obj && obj[key] !== 'undefined')
						? obj[key]
						: null
				},
				 nestedObj
			);
		  };

    function findRemoteFileTypesInReturnType(fields, currentPath, schemaTypes) {
      // Init Properties
      var resultsArray = [];
      if (currentPath == null) {
        currentPath = [];
      }
    
      var pathCopy = currentPath.slice(); // Create a copy to avoid modifying the original
    
      // Iterate array of fields
      for (var field of fields) {
        var fieldPath = pathCopy.slice(); // Fresh copy for each field
        fieldPath.push(field.name);
    
        if (!field.type) continue;
    
        var fieldType = field.type;
    
        // Handle NON_NULL types (unwrap them)
        if (fieldType.kind === "NON_NULL" && fieldType.ofType) {
          fieldType = fieldType.ofType;
        }
    
        // Handle LIST types (arrays)
        if (fieldType.kind === "LIST" && fieldType.ofType) {
          var listItemType = fieldType.ofType;
    
          // Handle NON_NULL list items
          if (listItemType.kind === "NON_NULL" && listItemType.ofType) {
            listItemType = listItemType.ofType;
          }
    
          // If list contains objects
          if (listItemType.kind === "OBJECT") {
            // Check if the list contains remote file objects
            if (listItemType.name.endsWith("RemoteFileDTO")) {
              resultsArray.push({
                fieldName: fieldPath.join("."),
                pathParts: fieldPath,
                isList: true,
              });
            } else {
              // Find the object definition
              var listObjectDef = schemaTypes.find(function (type) {
                return type.kind === "OBJECT" && type.name === listItemType.name;
              });
    
              // Recursively search this type
              if (listObjectDef && listObjectDef.fields) {
                var nestedResults = findRemoteFileTypesInReturnType(listObjectDef.fields, fieldPath, schemaTypes);
    
                // Mark these as being inside a list
                for (var j = 0; j < nestedResults.length; j++) {
                  nestedResults[j].isList = true;
                }
    
                resultsArray = resultsArray.concat(nestedResults);
              }
            }
          }
        }
        // Handle regular OBJECT types
        else if (fieldType.kind === "OBJECT") {
          // Check for Remote File DTOs (including any ending with RemoteFileDTO)
          if (fieldType.name.endsWith("RemoteFileDTO")) {
            resultsArray.push({
              fieldName: fieldPath.join("."),
              pathParts: fieldPath,
            });
          } else {
            // Find the object definition
            var objectDef = schemaTypes.find(function (type) {
              return type.kind === "OBJECT" && type.name === fieldType.name;
            });
    
            // Recursively search this type
            if (objectDef && objectDef.fields) {
              var objectResults = findRemoteFileTypesInReturnType(objectDef.fields, fieldPath, schemaTypes);
              resultsArray = resultsArray.concat(objectResults);
            }
          }
        }
      }
    
      return resultsArray;
    }

		function getQueryReturnObjectTypeDefinitionFromSchema(schemaObj, matchingQueries, queryIndex) {
			if (matchingQueries == null) {
				logger.error('Unable to find matching query object in schema');
				return null;
			}

			// Get the specific query by index
			var matchingQuerySchemaDefinition = matchingQueries[queryIndex];

			// We only need to check for custom types if the response object is of type object
			var matchingQuerySchemaType = (matchingQuerySchemaDefinition.type.kind === "OBJECT") ? matchingQuerySchemaDefinition.type : (matchingQuerySchemaDefinition.type.ofType) ? matchingQuerySchemaDefinition.type.ofType : null;
			if (matchingQuerySchemaType.kind === "OBJECT") {
				// Get details of the return object type - first we need to get the name so we can get the details
				var matchingQueryReturnObjectTypeName = matchingQuerySchemaType.name;
				var returnObjectTypeDefinition = schemaObj.data.__schema.types.find(function (type) {
					return type.kind === "OBJECT" && type.name === matchingQueryReturnObjectTypeName
				});
				if (returnObjectTypeDefinition == null) {
					logger.error('Unable to find matching return object type');
					return null;
				}
				logger.debug(JSON.stringify(returnObjectTypeDefinition, undefined, 2));
				return returnObjectTypeDefinition;
			}
		}

    // New helper function to resolve all concrete paths in the data for a path that includes lists
    function resolveListPaths(data, pathTemplate) {
      var paths = [];

      function traverse(obj, currentPath, remainingPath) {
        if (!remainingPath || remainingPath.length === 0) {
          // We've reached the end of the path template - this is a match
          paths.push(currentPath);
          return;
        }

        var nextSegment = remainingPath[0];
        var restPath = remainingPath.slice(1);

        // If we've hit a null/undefined, stop traversal on this branch
        if (!obj || typeof obj !== "object") {
          return;
        }

        // Check if this segment exists in the object
        if (!(nextSegment in obj)) {
          return;
        }

        // If this is an array, process each element
        if (Array.isArray(obj[nextSegment])) {
          for (var i = 0; i < obj[nextSegment].length; i++) {
            // For each array element, continue traversal with updated path
            traverse(obj[nextSegment][i], currentPath.concat([nextSegment, i]), restPath);
          }
        } else {
          // Regular object property - continue traversal
          traverse(obj[nextSegment], currentPath.concat([nextSegment]), restPath);
        }
      }

      // Start traversal from the root of the data
      traverse(data, [], pathTemplate);
      return paths;
    }

		function doHTTPCalloutToDSSAPI(integration) {
			try {
				var postUrl = self.dssConfig.dataApiUrl;
				logger.info('POST URL: ' + postUrl);
				return $.ajax({
					url: postUrl,
					data: JSON.stringify({
						query: integration.dssQuery
					}),
					processData: false,
					type: 'POST',
					contentType: 'application/json',
					timeout: 60000,
					beforeSend: function(xhr) {
						xhr.setRequestHeader('Authorization', 'Bearer ' + Settings.application.api.authToken);
					}
				});
			}
			catch (error) {
				logger.error(error);
                return $.Deferred().reject(error);
			}
		}

		function doHTTPSchemaCalloutToDSSAPI() {
			try {
				var postUrl = self.dssConfig.dataApiUrl;
				logger.info('POST URL: ' + postUrl);
				return $.ajax({
					url: postUrl,
					data: JSON.stringify({
						query: SCHEMA_QUERY
					}),
					processData: false,
					type: 'POST',
					contentType: 'application/json',
					timeout: 60000,
					beforeSend: function(xhr) {
						xhr.setRequestHeader('Authorization', 'Bearer ' + Settings.application.api.authToken);
					}
				});
			}
			catch (error) {
				logger.error('Error calling out to check dss schema');
				return $.Deferred().reject();
			}
		}

		function currentTokenHasDSSClaim() {
			var tokenModel = new TokenModel(Settings.application.api.authToken);
			return tokenModel.hasClaim(TokenClaims.DSS);
		}
	}
	return new DSSAPIInterface();
});

rjs.define('controllers/FirmwareUpdateController',["properties/App.Settings", "system/plugins/DownloadPlugin", "system/power/PowerInterface","controllers/UserMessageController"], function(Settings,DownloadPlugin,PowerInterface, UserMessageController) {
	/**
	 * The firmware update controller is responsible for checking for and performing device firmware updates.
	 */
	function FirmwareUpdateController(apiInterface){
		var self = this;
		var _apiInterface = apiInterface;		
		
		self.start = function()
		{
			var dfd = $.Deferred();

			// Implement logic to start checking for update and resolve after successful start
			
			return dfd;
		};

		self.stop = function()
		{
			var dfd = $.Deferred();

			// Implement logic to stop checking for updates
			
			return dfd;
		};

		self.isFirmwareUpdateRequired = function(firmwareUpdate) {
			
			if(firmwareUpdate.isValid()==false)
			{
				logger.debug("Firmware update information is not valid");
				return false;
			}
			
			var device_info = new BSDeviceInfo();
			if(device_info.version != firmwareUpdate.version)
			{
				logger.debug("Firmware versions do not match, update required");
				return true;
			}
			else {
				logger.debug("Firmware version is the same as that currently on device");
				return false;
			}

		};

		self.updateFirmware = function(firmwareUpdate)
		{
			logger.info('Starting firmware update...');
			var dfd = $.Deferred();

			self.downloadAndInstall(firmwareUpdate.url).done(function(){
				dfd.resolve();
			}).fail(function(){
				dfd.reject();
			});

			return dfd;
		};
		
		self.downloadAndInstall = function(url)
		{
			var dfd = $.Deferred();

			var filename = url.substring(url.lastIndexOf('/')+1);
			var localFileName = Settings.application.fileSystem.storageDirectory + "/" + filename;
			logger.debug("Firmware will download to: " + localFileName);
						
            // initialize the downloader
            var downloadPlugin = new DownloadPlugin();
			
			downloadPlugin.downloadFile(url, localFileName)
				.done(function(_remoteUrl, _localFile) {
					logger.debug("Firmware downloaded successfully, rebooting");
					
					UserMessageController.postMessage('Firmware update downloaded, restarting...');

					setTimeout(function() {
						PowerInterface.reboot();
					}, 5000);
					
					//A bit pointless but we will do it anyway.
					dfd.resolve();
				}  // On Download Failure
			).fail(function(_remoteUrl, _localFile) {
				dfd.reject("Could not download firmware");
			});

			return dfd;
		};

		self.cancelUpdate = function() {
			logger.info("Cancelling requested firmware update as it is not required.");
			
			_apiInterface.notifyFirmwareUpdateComplete();
		};
		
		function notifyFirmwareDownloadRequested(objectId) {
            _apiInterface.notifyDownloadStart(objectId);
		}
			
		function notifyFirmwareUpdateCompleted() {
			_apiInterface.notifyFirmwareUpdateComplete();
        }
		
		
	}
	
	return FirmwareUpdateController;
}); 
rjs.define('controllers/MediaSyncController',[], function() {
	function MediaSyncController() {
		var self = this;
        var syncRunning = false;
        var sync = null;
        var syncDetails = null;
        var isSyncMaster = false;

        self.EVENTS = {
            START_MASTER: "start_master",
            LISTEN_FOR_MASTER_BROADCASTS: "listen_for_master_broadcasts",
            STOP_LISTENING_FOR_BROADCASTS: "stop_listening_for_broadcasts",
            BROADCAST: "broadcast",
            CLOSE: "close",
            DISCONNECT: "disconnect",
            SYNC: "sync",
            SYNC_EVENT: "syncevent",
            TAKEOVER: "takeover",
            TAKEOVER_EVENT: "takeoverevent",
            MASTER_ALIVE: "masteralive",
            CONNECT: "connect",
            CONNECTION: "connection",
            ERROR: "error",
            CONNECT_ERROR: "connect_error",
            RECONNECT_ERROR: "reconnect_error",
            RECONNECT_FAILED: "reconnect_failed",
            LOG: "log"
        };

        self.startSync = function (syncConfig, syncGroupId, syncAddress, syncPort, syncEvent, syncMaster) {
            // Only set this if sync is not running
            if(!syncRunning) {
                syncRunning = true;
                syncDetails = syncConfig;
                sync = new BSSyncManager(syncGroupId.toString(), syncAddress, syncPort);
                sync.onsyncevent = syncEvent;
                isSyncMaster = syncMaster;
            }
            return $.Deferred().resolve(sync);
        };

        self.startMaster=function(syncGroupID,handleSyncEventCB,startPlaybackOnServiceLoaded)
        {
            // Not required for Screen Takeovers on Brightsign
            return $.Deferred().resolve();
        };
        
        self.startSlave=function(syncGroupID,handleSyncEventCB)
        {
            // Not required for Screen Takeovers on Brightsign
            return $.Deferred().resolve();
        };

        // Different implementation on brightsign
        self.sendTakeoverEvent = function (event, data) {
            console.log("SendTakeoverEvent");
            if(isSyncMaster) {
                sync.SetMaster(1);
                sync.Synchronize(data, 300);
            }
        };

        self.sendPlayAsset = function (index) {
            // Not required for Screen Takeovers on Brightsign
            return $.Deferred().resolve();
        };
        
        self.stopSyncService = function()
        {
            // Not required for Screen Takeovers on Brightsign
            return $.Deferred().resolve();
        };

        self.stop = function()
        {
            syncRunning = false;
            if(sync) {
                sync.Close();
            }
            sync = null;
            return $.Deferred().resolve();
        };
        
        self.isSyncRunning = function()
        {
            return syncRunning;
        };
    
    
	}

	return new MediaSyncController();
});

rjs.define('models/PlaybackStartEndReasons',[],function(){
	return {
		SCHEDULE:"Schedule",
		INTERRUPT:"Interrupt",
		SCREEN_OFF: "Screen Off",
		SETUP_SCREEN: "Access Signagelive Setup",
		ADJUST_LAYOUT_DIMENISONS: "Adjust Layout Dimensions",
		SYSTEM_TIME_CHANGED: "System Time Changed",
		REMOTE_SETTINGS_UPDATE: "Remote Settings Update",
		PRELOAD_LAYOUT: "Preloading Layout",
		TAKEOVER: "Screen Takeover"
	}
});
rjs.define('core/SLScreenshotUploadAPIInterface',["properties/App.Settings"], function(Settings) {
	/*
		SLScreenshotUploadAPIInterface - Provides an interface to communicate with the SL Screenshot Upload API
		------------------------------------------------------------------------------
		This module has been updated so that the authentication is now async rather than synchronous
	 */
	function SLScreenshotUploadAPIInterface() {
		var self = this;

		// default connection settings
		var _authenticationFailureRetryIntervalMS = 5000;

		var _UploadComplete;
		var _UploadFailed;

		var _HasUploadFailed = false;
		var _HasUploadStarted = false;
		var _HasUploadFinished = false;



		//
		// authenticate()
		//
		function authenticate() {
			var dfd = $.Deferred();

			logger.debug('auth about to be called');
			var jqXHR = executeAPICall('authenticate', {
				type: 'POST'
			});

			jqXHR.done(function(data, textStatus, jqXHR) {
				logger.debug('authenticated');
				
				var expireTime = new Date(data.expires);//We will re-authenticate 5 minutes before the token actually expires
                expireTime = new Date(expireTime - (300000));

                Settings.application.api.authToken = data.token;
                Settings.application.api.authTokenExpiry = expireTime;

                logger.debug("Token expires at: " + Settings.application.api.authTokenExpiry);

				// We are authenticated
				dfd.resolve(true);

			}).fail(function(jqXHR, textStatus, errorThrown) {
				logger.debug('auth failed');
				// log the error
				logAPIError(this.url, this.data, jqXHR.status, errorThrown, jqXHR.responseText);

				// attempt to re-athenticate in a few seconds
				//_authenticationTimerId = setTimeout(authenticate, _authenticationFailureRetryIntervalMS);

				// auth failed
				dfd.resolve(false);
			});


			return dfd;
		}


		function executeAPICall(operationName, settings) {
			try {
				logger.debug('executeAPICall ' + operationName);
				logger.debug('PLAYER API - Client ID: ' + Settings.application.api.clientId + ', Token: ' + Settings.application.api.authToken);

				var type = 'GET';
				var async = true;
				var data = null;

				if (settings != undefined) {
					if (settings.type != undefined)
						type = settings.type;

					if (settings.async != undefined)
						async = settings.async;

					if (settings.data != undefined)
						data = JSON.stringify(settings.data);
				}

				return $.ajax({
					url: Settings.application.api.url + "/" + operationName,
					type: type,
					contentType: 'application/json',
					dataType: 'json',
					async: async,
					data: data,
					timeout: 60000,
					beforeSend: function(xhr, settings) {
						xhr.setRequestHeader('X-SIGNAGELIVE-APP-ID', Settings.application.api.appId);
						xhr.setRequestHeader('X-SIGNAGELIVE-APP-KEY', Settings.application.api.appKey);
						xhr.setRequestHeader('X-SIGNAGELIVE-CLIENT-ID', Settings.application.api.clientId);
						xhr.setRequestHeader('X-SIGNAGELIVE-CLIENT-KEY', Settings.application.api.clientKey);
						xhr.setRequestHeader('X-SIGNAGELIVE-AUTH-TOKEN', Settings.application.api.authToken);
					}
				});
			} catch (e) {
				logger.error('Error executing api call ' + e.message);

				return null;
			}
		}


		self.uploadMultipleScreenshots = function(screenshotArray) {
			uploadScreenshotAPICall(screenshotArray).done(function(data, textStatus, jqXHR) {

				_UploadComplete();

			}).fail(function(jqXHR, textStatus, errorThrown) {
				// reset auth token if it is not valid
				if (jqXHR.status == 401) {
					Settings.application.api.authTokenExpiry = null;
				}

				// log the error
				logAPIError(this.url, this.data, jqXHR.status, errorThrown, jqXHR.responseText);

				_UploadFailed();

			});

		};

		self.uploadSingleScreenshot = function(screenshotUploadObject) {


			var screenshotArray = [screenshotUploadObject];

			uploadScreenshotAPICall(screenshotArray).done(function(data, textStatus, jqXHR) {

				_UploadComplete();

			}).fail(function(jqXHR, textStatus, errorThrown) {
				// reset auth token if it is not valid
				if (jqXHR.status == 401) {
					Settings.application.api.authTokenExpiry = null;
				}

				// log the error
				logAPIError(this.url, this.data, jqXHR.status, errorThrown, jqXHR.responseText);

				_UploadFailed();

			});

		};

		function uploadScreenshotAPICall(screenshotArray) {
			try {
				logger.debug('PLAYER API - Client ID: ' + Settings.application.api.clientId + ', Token: ' + Settings.application.api.authToken);

				var type = 'POST';
				var async = true;

				var screenshotUploadRequestBody = {};
				screenshotUploadRequestBody.screenshots = screenshotArray;

				return $.ajax({
					url: Settings.application.screenshotuploadapi.url + "/Upload",
					type: type,
					contentType: 'application/json',
					dataType: 'text',
					async: true,
					data: JSON.stringify(screenshotUploadRequestBody),
					timeout: 60000,
					beforeSend: function(xhr, settings) {
						xhr.setRequestHeader('X-SIGNAGELIVE-APP-ID', Settings.application.api.appId);
						//xhr.setRequestHeader('X-SIGNAGELIVE-APP-KEY', Settings.application.api.appKey);
						//xhr.setRequestHeader('X-SIGNAGELIVE-CLIENT-ID', Settings.application.api.clientId);
						//xhr.setRequestHeader('X-SIGNAGELIVE-CLIENT-KEY', Settings.application.api.clientKey);
						xhr.setRequestHeader('X-SIGNAGELIVE-AUTH-TOKEN', Settings.application.api.authToken);
					}
				});
			} catch (e) {
				logger.error('Error executing api call ' + e.message);

				return null;
			}
		}

		function isAuthenticationRequired() {
			logger.debug('isAuthenticationRequired()');

			if (Settings.application.api.authTokenExpiry == null)
				return true;
			if (Settings.application.api.authTokenExpiry < DateTime.now())
				return true;
			return false;
		}

		function logAPIError(requestUrl, requestData, responseStatus, responseMessage, responseText) {
			var logData = {
				url: requestUrl,
				/*requestData: requestData,*/
				responseStatus: responseStatus,
				responseMessage: responseMessage,
				responseBody: responseText
			};

			console.log("API Error " + JSON.stringify(logData));
		}


		self.initialize = function(onUploadComplete, onUploadFailed) {
			logger.info('Initializing SLScreenshotUploadAPIInterface');

			_UploadComplete = onUploadComplete;
			_UploadFailed = onUploadFailed;
		};

	}

	return SLScreenshotUploadAPIInterface;
});
rjs.define('models/ScreenshotFileList',["properties/App.Settings", "system/io/File"], function(Settings, File) {
	/**
	 * Used to manage the Local File List Manifest
	 */
	function ScreenshotFileList() {
		
		var self = this;
		self.files = [];
		
		/**
		 * Add a file to the file manifest
		 * @param {[type]} path [description]
		 */
		self.add = function(created,tz,ondemand,rotation,screenshotfilename)  {
			var dfd = $.Deferred();
			
			loadData().done(function()
			{
				//console.debug("Data is loaded, checking if screenshot is already in list (it shouldn't be)");
				// If the file doesn't already exist in the manifest file then add it
				if (exists(created) == false) {
					//console.debug("Screenshot is not in list, adding to file list");	
					self.files.push({
						created : created,
						tz: tz,
						ondemand: ondemand,
						rotation: rotation,
						filename : screenshotfilename
					});
					
				//	console.debug("Screenshot added, saving");
					
					saveFileSystemEnties().done(function(){
						//console.debug("Screenshot added. Screenshots in file: " + self.files.length);
						dfd.resolve();
					}).fail(function()
					{
						dfd.reject();
					});	
				} else {
					//Already in list?
					dfd.resolve();
				}	
			});
			
			return dfd.promise();
		};
		
		self.count = function() {
			var dfd = $.Deferred();

           // console.log("Checking count");
			
			loadData().done(function()
			{
                //console.log("Count is " + self.files.length);
				dfd.resolve(self.files.length);
			});
			
			return dfd.promise();
		};
		
		/**
		 * Gets the first item without removing it
		 */
		self.getFirst = function() {

			var dfd = $.Deferred();
			
			loadData().done(function()
			{
				if(self.files.length > 0)
					dfd.resolve(self.files[0]);
				else 
					dfd.resolve(null);						
			});
			
			return dfd.promise();		
		};
		
		self.getFirstXItems = function(itemCount) {
			var dfd = $.Deferred();
			
			loadData().done(function()
			{
				if(self.files.length > 0)
					dfd.resolve(self.files.slice(0,itemCount));
				else 
					dfd.resolve(null);
					
			});
			
			return dfd.promise();		
		};
		
		
		//Remove and get first item
		self.removeFirst = function() 
		{				
			var dfd = $.Deferred();
			
			loadData().done(function()
			{
				if(self.files.length > 0)
				{
					var item = self.files.shift();
					saveFileSystemEnties().done(function(){
						dfd.resolve(item);
					}).fail(function()
					{
						dfd.reject();
					});	
				} else 
					dfd.resolve(null);
			});
					
			return dfd.promise();
		};
	
		/**
		 * Remove a file from the file manifest
		 * @param  {[type]} path [description]
		 * @return {[type]}      [description]
		 */
		self.remove = function(created) {
			var dfd = $.Deferred();
			
			loadData().done(function()
			{
				// Check for matching files in the manifest
				var index = -1;
				indexes = $.map(self.files, function(obj, index) {
					if (obj.created == created) {
						return index;
					}
				});
	
				// If a match is found get the first match
				if (indexes.length > 0)
					index = indexes[0];
	
				// And delete it
				if (index != -1) {
					self.files.splice(index, 1);
					saveFileSystemEnties().done(function(){
						dfd.resolve();
					}).fail(function()
					{
						dfd.reject();
					});	
				}else{
					dfd.resolve();
				}	
			
			});
			
			
			return dfd.promise();
		};
		
		self.removeMultipleScreenshots = function(screenshotArray) {
			var dfd = $.Deferred();
			
			//Load the files, delete each in the array and save the file again
			loadData().done(function()
			{	
				try{
				screenshotArray.forEach(function(screenshot){
					// Check for matching files in the manifest
					var index = -1;
					indexes = $.map(self.files, function(obj, index) {
						if (obj.created == screenshot.created) {
							return index;
						}
					});
		
					// If a match is found get the first match
					if (indexes.length > 0)
						index = indexes[0];
		
					// And delete it
					if (index != -1) {
						//logger.debug("Removing screenshot taken at " + screenshot.created);
						self.files.splice(index, 1);					
					}
				});
				
				} catch(e)
				{
					logger.error(e.message);
				}
				
				//logger.debug("Screenshots removed, saving");
				saveFileSystemEnties().done(function(){
					//logger.debug("Screenshot list saved");
					dfd.resolve();
				}).fail(function()
				{
					dfd.reject();
				});		

			});
			
			
			return dfd.promise();
		};

		/**
		 *DATA MUST BE LOADED FIRST 
		 *
		 * Does the file exist?
		 * @param  {[type]} path [description]
		 * @return {[type]}      [description]
		 */
		function exists(created) {

			var exists = false;
			var matches = $.grep(self.files, function(obj, index) {
				return obj.created == created;
			});
			return (matches.length == 1);
		};
		
		function loadData()
		{
			var dfd = $.Deferred();
			
			File.read(Settings.application.fileSystem.systemFiles.screenshotFileListFilePath).done(function(data)
			{
				//Not a few file
				if(data!=null && data != "")
				{
					//console.debug("Screenshot list loaded");
					
					try{
						self.files = JSON.parse(data);
					} catch (e)
					{
					//	console.error("Error parsing file data: " + e.message + ", file: " + data);
					}
				}else{
			//		console.debug("No screenshots to load");
				}
				
				dfd.resolve();
			}).fail(function()
            {
                //File doesn't exist?
                saveFileSystemEnties().done(function()
                {
                    dfd.resolve();
                });

            });
			
			return dfd.promise();
		}

		self.clear = function() {
							
			self.files = [];

			return saveFileSystemEnties();
		};

		/**
		 * [saveFileSystemEnties description]
		 */
		function saveFileSystemEnties() {
			
			var dfd = $.Deferred();
					
			File.write(Settings.application.fileSystem.systemFiles.screenshotFileListFilePath,JSON.stringify(self.files)).done(function()
			{
			//	console.debug("Screenshot list saved");
				dfd.resolve();
			}).fail(function()
			{
			//	console.error("Error saving the screenshot file list");
				dfd.reject();
			});
	
			
			return dfd.promise();
		}

	}

	var sfl = new ScreenshotFileList();
	return sfl;
});
rjs.define('controllers/ScreenCaptureController',["properties/App.Settings", "system/storage/StorageInterface","controllers/UserConfigurationController", "core/SLScreenshotUploadAPIInterface","system/DateTime","system/io/File","models/ScreenshotFileList","system/io/Directory"], function(Settings, StorageInterface, UserConfigurationController, SLScreenshotUploadAPIInterface, DateTime, File,ScreenshotFileList, Directory) {
	function ScreenCaptureController(){
		var self=this;
		var Screenshot = require("@brightsign/screenshot");
			
		var bootupCleanDone = false;

		var minimumFreeDiskSpace = 100;
		var minimumStoredScreenshots = 1;
		var maximumStoredScreenshots = 1;
		var filesystemReady = false;

		var screenCaptureConfiguration; 
		
		var enabled = false;
		
		var _UploadAPI;
		
		var timer;
		var startTimer;
		
		var frequency;
		var uploadingOnDemandScreenshot = false;
		var performingScheduledUpload = false;
		var tmpScreenshot;
		var lastScheduledScreenshotTime;
		
		var screenshotCurrentlyUploading = null;

		var contentOverlayDeletionTimer = null;
			
		self.start=function(){
			if(enabled)
			{
		
				console.log("Starting screen capture controller");
				if(startTimer != null && startTimer != undefined)
					clearTimeout(startTimer);
					
				var timeToNextScreenshot = 0;
				if(lastScheduledScreenshotTime!= null && lastScheduledScreenshotTime!= undefined)
					timeToNextScreenshot = (lastScheduledScreenshotTime.getTime() + frequency) - DateTime.now().getTime();
					
				//We are now due a screenshot, so take one and start the timer
				if(timeToNextScreenshot < 1)
				{
					takeScheduledScreenCapture();
					
					timer = setInterval(function() {
						takeScheduledScreenCapture();
					}, frequency);
				}else{
					//We will start in the future as we aren't ready to take a screenshot yet
					startTimer = setTimeout(function(){
						self.start();
					},timeToNextScreenshot);
					
				}						
		
			}
		};
		
		
			
		self.stop=function(){
			if(timer != null && timer != undefined)
				clearInterval(timer);
		};
		
		self.initialize=function(configuration)
		{
			console.log("Configuring screen capture");
			screenCaptureConfiguration = configuration;
			
			if(configuration.logging.screencapture != null && configuration.logging.screencapture != undefined)
			{
				//GET ON
				enabled = configuration.logging.screencapture.on;
			}else {
				enabled = false;
			}
			
			//console.log("Screen capture enabled: " + enabled);
			
			if(enabled)
			{
				//Get frequency
				frequency = configuration.logging.screencapture.frequency * 60000;
			}	
										
		};

		self.reset = function()
		{
			var dfd = $.Deferred();	

			Directory.listFiles(Settings.application.fileSystem.root + "/" + Settings.application.fileSystem.storageDirectory).done(function(files)
			{		
				var screenshotfileRegex = new RegExp('^screenshot_20[1-2][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9]{6}Z.jpg$');
				$.each(files, function( index, filename ) {

					if(screenshotfileRegex.test(filename))
					{
						var filePath = Settings.application.fileSystem.root + "/" + Settings.application.fileSystem.storageDirectory + "/" + filename;
						if(File.exists(filePath))
							File.delete(filePath);
					}
					
				});

				ScreenshotFileList.clear().done(function(){
					dfd.resolve();
				});
			});			
				
			return dfd.promise();
		};
			
		function deleteScreenshot(screenshot)
		{
			var dfd = $.Deferred();			

			ScreenshotFileList.remove(screenshot.created).done(function(){
				logger.debug("Screenshot removed");
				File.delete(Settings.application.fileSystem.root + "/" + Settings.application.fileSystem.storageDirectory + "/" + screenshot.filename);

				dfd.resolve();
			}).fail(function()
			{
				dfd.reject();
			});	
		
			
			return dfd.promise();
		}

		function deleteScreenshotFile(screenshot)
		{
			File.delete(Settings.application.fileSystem.root + "/" + Settings.application.fileSystem.storageDirectory + "/" + screenshot.filename);
		}

		
		function deleteScreenshots(screenshotArray)
		{
			var dfd = $.Deferred();	

			$.each(screenshotArray, function(index,screenshot) {	
				File.delete(Settings.application.fileSystem.root + "/" + Settings.application.fileSystem.storageDirectory + "/" + screenshot.filename);
			});

			ScreenshotFileList.removeMultipleScreenshots(screenshotArray).done(function(){
				logger.debug("Screenshots removed");
				dfd.resolve();
			}).fail(function()
			{
				dfd.reject();
			});		
			
			return dfd.promise();
		}

		function addScreenshotToStore(screenshotPath,dateTimeTakenStr,tz,rotation,isOnDemandScreenshot)
		{	
			var dfd = $.Deferred();

			ScreenshotFileList.add(dateTimeTakenStr,tz,isOnDemandScreenshot,rotation,screenshotPath).done(function()
			{
				dfd.resolve();
			}).fail(function()
			{
				dfd.reject();
			});	
				
	
			return dfd.promise();
		}
		
				
		function takeScheduledScreenCapture(){
			logger.debug("Taking scheduled screen capture");			
						
			getScreenCapture().done(function(screenshot)
			{	
				logger.debug("Screenshot taken - " + screenshot.filename);		
				lastScheduledScreenshotTime = screenshot.created;
				
				addScreenshotToStore(screenshot.filename,screenshot.created.getStringRepresentation(),screenshot.tz,screenshot.rotation,false).done(function(){
					//console.log("Screenshot taken and added to store");
				}).fail(function()
				{
					//console.log("There was an enrror saving the screenshot to disk");
				});	
				
			});
		}

		function getScreenshotUploadObject(screenshot) {
			var dfd = $.Deferred();

			var imgurl = "/" + Settings.application.fileSystem.storageDirectory + "/" + screenshot.filename;
			logger.debug("Loading screenshot from " + imgurl);

			File.read(imgurl)
				.done(function (data) {
					logger.info("Image string data loaded ");
					if (data != null) {
						var str = data.toString('base64');
						logger.debug("Screenshot loaded as base64, uploading.");

						var screenshotUploadObject = {};
						screenshotUploadObject.data = str;
						screenshotUploadObject.created = screenshot.created;
						screenshotUploadObject.tz = screenshot.tz;
						screenshotUploadObject.ondemand = false;
						screenshotUploadObject.rotation = screenshot.rotation;

						str = null;

						dfd.resolve(screenshotUploadObject);
						screenshotUploadObject = null;
					}
				})
				.fail(function (err) {
					logger.error("Error loading image - " + err);
					dfd.reject();
				});

			return dfd;
		}
		

		self.takeOnDemandScreenshot = function()
		{
			
			getScreenCapture().done(function(screenshot)
			{
				uploadingOnDemandScreenshot = true;

				logger.debug("Loading on demand screenshot from disk");
				getScreenshotUploadObject(screenshot).done(function(screenshotUploadObject)
				{
					screenshotUploadObject.ondemand = true;
					//Save the data in case we have a problem uploading the screenshot
					tmpScreenshot = screenshotUploadObject;
					//Try to upload
					uploadScreenCapture(screenshotUploadObject);
				});
	
			});
		};
		
		function removeOldScreenshotsIfRequired()
		{
			logger.debug("Checking to see if we need to remove some screenshots");
			
			var dfdAllComplete = $.Deferred();
			
			var dfdCountComplete = $.Deferred();
			
						
			dfdCountComplete.done(function(){
				var dfdStorageClearComplete = $.Deferred();
				logger.debug("There are not too many screenshots, checking for space used");

				dfdStorageClearComplete.done(function()
				{
					console.log("Storage checks done");
					dfdAllComplete.resolve();
				});
				
				//Now, remove them if too much space is being used
				//console.log("Checking space used by screenshots in store to see if we need to remove some");
				removeOldScreenshotsToClearStorageIfRequired(dfdStorageClearComplete);		
			});
			
			
			//First, delete by count if too many
			//console.log("Checking number of screenshots in store to see if we need to remove some");
			removeOldScreenshotsToReduceNumberIfRequired(dfdCountComplete);
			
			return dfdAllComplete.promise();
		}
		
		function removeOldScreenshotsToReduceNumberIfRequired(dfdCountComplete)
		{
		

			try{
			

			ScreenshotFileList.count().done(function(count)
			{
				logger.debug("Screenshot count is " + count + ".");

				if(count < maximumStoredScreenshots)
				{
					logger.debug("Screenshot count is less than the maximum number");
					dfdCountComplete.resolve(true);
				} else {
					logger.debug("Screenshot count is " + count + ", removing a screenshot.");
					ScreenshotFileList.removeFirst().done(function(deletedScreenshot){

						deleteScreenshotFile(deletedScreenshot);

						removeOldScreenshotsToReduceNumberIfRequired(dfdCountComplete);
					});
				}				
			});	

			} catch(err)
			{
				console.log(err.message);
			}			
	
	
		}
		
		function removeOldScreenshotsToClearStorageIfRequired(dfdStorageClearComplete)
		{
			isFreeStorageAboveMin().done(function(result){
				if(result)
					dfdStorageClearComplete.resolve(true);
				else
				{
					logger.debug("Deleting a screenshot to clear space");
							
					if(ScreenshotFileList.count() == 0)
						dfdStorageClearComplete.resolve(false);
					else
						ScreenshotFileList.removeFirst().done(function(deletedScreenshot){
							deleteScreenshotFile(deletedScreenshot);
							removeOldScreenshotsToClearStorageIfRequired(dfdStorageClearComplete);
						});
								
				}	
			});
		}
		
		
		function isFreeStorageAboveMin()
		{	
			var dfd = $.Deferred();
			logger.debug('Checking if there is enough disk space to take a screenshot');

			StorageInterface.getTotalStorageAvailable()
			.done(function(space) {
				logger.debug("Space available: " + space + ", Space required: " + minimumFreeDiskSpace);
				
				var result = true;
				if(space < minimumFreeDiskSpace)
				{
					logger.debug('Less disk space than the set limit - delete a screenshots');
					result = false;
				} else{
					logger.debug('Enough disk space for screenshots');
				}
		    	dfd.resolve(result);
		    })
		    .fail(function(){
		    	dfd.reject(false);
		    });
			
			return dfd.promise();
		}
	
	
		function getScreenCapture(){
			//Check that we aren't exceeding any limits and remove screenshots if necessary
			var dfd = $.Deferred();
			
			removeOldScreenshotsIfRequired().done(function()
			{	
				var timeNow = DateTime.now();
				var screenshotTimeStr = timeNow.getStringRepresentation();
				logger.debug("There is enough space for a new screenshot.");		
				//console.log("Limits checked, taking screenshot");	
				
				var filename =  "screenshot_" + screenshotTimeStr.replace(new RegExp(":", "g"),"")  + ".jpg";

                var params = {};
                params.fileType = "JPEG";
                params.quality = 75;

				// If series 5 then create a screenshot based on the actual resolution
				var windowWidth = window.screen.availWidth;
				var windowHeight = window.screen.availHeight;
				var minWindow = Math.min(windowWidth, windowHeight);
				var maxWindow = Math.max(windowWidth, windowHeight);

				// Downsize resolution of screenshot by half if resolution returns greater than 1920x1080
				params.width = (Settings.application.features.deviceSeries == 5 && minWindow > 1920) ? windowWidth / 2 : 1920;
				params.height = (Settings.application.features.deviceSeries == 5 && minWindow >= 1080 && maxWindow > 1920) ? windowHeight / 2 : 1080;
				//params.rotation = 0;
                params.fileName = filename;               
                
                var screenshots = new Screenshot();
		
				var newscreenshot = {};
				newscreenshot.created = timeNow;
                newscreenshot.filename =  filename;
                newscreenshot.tz = UserConfigurationController.timeZone().id;

				try {
					var rotationScreenVal = UserConfigurationController.screenConfig().map(function (screen) {
						if (screen.enabled) {
							return Number((screen.transform !== "normal") ? screen.transform : 0);
						}
					}).filter(function (scr) {
						return scr !== undefined;
					})[0];
				} catch (e) {
					// Issue determining rotation to be used for Series 5 device - fallback to Series 4 config setting
					var rotationScreenVal = UserConfigurationController.rotationAngle();
				}

				// For series 5 and 6 always return the rotation value sent to the API as 0.
				// We do this because the image we get back is already rotated so we do not want the API to do any additional image rotation.
				newscreenshot.rotation = (Settings.application.features.deviceSeries == 5) ? 0 : UserConfigurationController.rotationAngle();				

				// if portrait then swap HxW for S5
				if(Settings.application.features.deviceSeries == 5 && (rotationScreenVal == 90 || rotationScreenVal == 270)) {
					var tempHeight = params.height;
					params.height = params.width;
					params.width = tempHeight;
				}

				screenshots.syncCapture(params);
				logger.debug("Screenshot captured");

                dfd.resolve(newscreenshot);

			});
			
			
			return dfd.promise();
		}

		self.getContentScreenshot = function() {
			var dfd = $.Deferred();
			
			var timeNow = DateTime.now();
			var filename =  "screenshot_screenTakeover.jpg";

			var params = {};
			params.fileType = "JPEG";
			params.quality = 50;
			params.width = 1920;
			params.height = 1080;
			//params.rotation = 0;
			params.fileName = filename;               
			
			var screenshots = new Screenshot();
			screenshots.syncCapture(params);
			logger.debug("Screenshot captured");		
			var newscreenshot = {};
			newscreenshot.created = timeNow;
			newscreenshot.filename =  filename;
			newscreenshot.tz = UserConfigurationController.timeZone().id;
			newscreenshot.rotation = UserConfigurationController.rotationAngle();

			getScreenshotUploadObject(newscreenshot).then(function(screenshotUploadObject) {
				dfd.resolve(screenshotUploadObject);
			});

			logger.debug("Deleting screenshot captured in 5 seconds ...");
			clearTimeout(contentOverlayDeletionTimer);
			contentOverlayDeletionTimer = setTimeout(function() {
				logger.debug("Deleting screenshot captured ...");
				deleteScreenshotFile(newscreenshot);
				newscreenshot = null;
				contentOverlayDeletionTimer = null;
			}, 5000);

			return dfd.promise();
		};
		
		self.uploadAllScreenshots = function()
		{
			if(performingScheduledUpload == false)
			{
				console.log('Uploading all screenshots');
				performingScheduledUpload = true;
				uploadNextScreenshotInQueue();
			} else {
				console.log('Screenshot upload already in progress');
			}
		};
		
		function uploadNextScreenshotInQueue()
		{				
			//Get first 5 screenshos in queue
			ScreenshotFileList.getFirstXItems(5).done(function(nextFiveScreenshots){
	 	
		 		screenshotCurrentlyUploading = nextFiveScreenshots;
				 	
				if(screenshotCurrentlyUploading == null)
				{
					logger.info("screenshotCurrentlyUploading is null");
					console.log('All Screenshots Uploaded');
					performingScheduledUpload = false;
				}else {
					var screenshotUploadObjectArray = [];
					var promises = [];

					logger.debug("Loading all screenshots from disk.");
					$.each(nextFiveScreenshots, function(index,screenshot) {	
						promises.push(getScreenshotUploadObject(screenshot));
					});

					$.each(promises, function(index,promise) {						
						promise.done(function(screenshotUploadObject)
						{
							logger.debug("Pushing screenshot to the upload array.");
							screenshotUploadObjectArray.push(screenshotUploadObject);
						});
					});


					$.when.apply($, promises).done(function()
					{
						logger.debug("Uploading multiple screenshots.");
						uploadMultipleScreenCaptures(screenshotUploadObjectArray);	
					});
				}
			});		

		}
		
		function screenshotUploadComplete()
		{
			if(uploadingOnDemandScreenshot)
			{
				console.log('On demand screenshot uploaded.');
				uploadingOnDemandScreenshot = false;
				tmpScreenshot = null;
			}else
			{	
				logger.debug('Screenshots uploaded. Deleting');
				if($.isArray(screenshotCurrentlyUploading)) {
					var promises = [];
					deleteScreenshots(screenshotCurrentlyUploading).done(function()
					{
						logger.debug("Screenshot deleted, upload next");
						screenshotCurrentlyUploading = null;
						//Upload next
						uploadNextScreenshotInQueue();
					});
		
				} else {
					deleteScreenshot(screenshotCurrentlyUploading).done(function()
					{
						logger.debug("Screenshot deleted, upload next");
						screenshotCurrentlyUploading = null;
						//Upload next
						uploadNextScreenshotInQueue();
					});
				}
	
			}
		}
		
		function screenshotUploadFailed()
		{
			console.log('Screenshot upload failed.');	
			
			if(uploadingOnDemandScreenshot)
			{
				//On demand screenshot could not be uploaded. Add to the screenshot list to upload later
				uploadingOnDemandScreenshot = false;
				
				addScreenshotToStore(tmpScreenshot.filename,tmpScreenshot.created,tmpScreenshot.tz,tmpScreenshot.rotation,true);			
			} else {
				//Currently this cancels the upload
				performingScheduledUpload = false;
			}	
		}
				
		function initializeUploadAPI()
		{
			if(_UploadAPI == null || _UploadAPI == undefined)
			{				
				_UploadAPI = new SLScreenshotUploadAPIInterface();
				_UploadAPI.initialize(screenshotUploadComplete,screenshotUploadFailed);			
			}
		}

		function uploadScreenCapture(screenshotUploadObject)
		{	
			logger.debug("uploadScreenCapture");
			initializeUploadAPI();
			//Upload the screenshot
			_UploadAPI.uploadSingleScreenshot(screenshotUploadObject);		
		}

		function uploadMultipleScreenCaptures(screenshotUploadObjectArray)
		{	
			logger.debug("uploadMultipleScreenCaptures");
			initializeUploadAPI();
			//Upload the screenshot
			_UploadAPI.uploadMultipleScreenshots(screenshotUploadObjectArray);		
		}

		function base64Encode(str) {
			var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
			var out = "", i = 0, len = str.length, c1, c2, c3;
			while (i < len) {
				c1 = str.charCodeAt(i++) & 0xff;
				if (i == len) {
					out += CHARS.charAt(c1 >> 2);
					out += CHARS.charAt((c1 & 0x3) << 4);
					out += "==";
					break;
				}
				c2 = str.charCodeAt(i++);
				if (i == len) {
					out += CHARS.charAt(c1 >> 2);
					out += CHARS.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));
					out += CHARS.charAt((c2 & 0xF) << 2);
					out += "=";
					break;
				}
				c3 = str.charCodeAt(i++);
				out += CHARS.charAt(c1 >> 2);
				out += CHARS.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
				out += CHARS.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
				out += CHARS.charAt(c3 & 0x3F);
			}
			return out;
		}
	
	}
	return ScreenCaptureController;
}); 
rjs.define('controllers/ScreenTakeoverController',["system/DateTime", "properties/App.Settings", "controllers/MediaSyncController", "models/PlaybackStartEndReasons", "system/SystemInterface", "controllers/ScreenCaptureController"], function (DateTime, Settings, MediaSyncController, PlaybackStartEndReasons, SystemInterface, ScreenCaptureController) {
    var instance = null;

    function ScreenTakeoverController() {
        var self = this;
        var playerType = SystemInterface.getPlayerType();

		var initBuffer = true;
        self._takeovers = null;
        var eventListenerTimer = null;
        var retryTimer = null;
        self._takeoverGroupConfig = null;
        self._clientDetails = null;
        self.scheduledTakeoverEvents = [];

        _screenCapController = new ScreenCaptureController();

        self.isEnabled = false;
        self.isMaster = false;
        self.syncEvent = null;

        self.running = false;
        self.takeoverInProgress = false;
        self.timer = null;
        self.savedPlaylistIndex = -1;

        self.scheduledPlaylistAssetIndex = {};
        var scheduledPlaylistAssetToPlay = {};
        var scheduledPlaylistLayerId = null;
        var scheduledContentAssetComplete = {}; // This is used just to track whether the scheduled content asset has successfully started and completed playback, otherwise we may end up replaying the same asset (based on its index)
        var scheduledContentIsPlayingGapless = false; //NOTE: Is this required for CrOS?
        self.takeoverPlaylistAssetIndex = -1;
		self.takeoverPreloadTimeout = 3500; //This is used when attempting to preload all takeovers for smoother transitions
		self.scheduledContentPreloadTimeout = 1500; //This is used when attempting to move from takeover content back to scheduled content
        self.failOutOfTakeoverTimeout = 10000; //This is the duration it will wait before failing out of a takeover
        var brightsignSyncDelay = 300;
        self.failureTimer = null;
        self.takeoverAssetFailureTimer = null;

        self._takeoverEventTimer = null;
        self._preloadTimer = null;
        self.currentTakeoverEvent = null;
        self.preloading = false;
        self.contentPreloaded = false;
		self.preloadingSecondTakeover = false;

        if(self._mediaSyncController == null) {
            self._mediaSyncController = MediaSyncController;
        }

        self.mediaPlayerView = null;
        self._layouts = null;
        var _currentLayout = null;

        var currentHour;

        var feedAssetsPlayed = 0; // This is primarily used to track nested playlists that are calculated as contentType feed, as the feed index to be tracked will be different to the main playlist index

        if (instance !== null) {
            logger.error("Screen Takeovers: Only using one instance for screenTakeover requirements - use getInstance()");
        }

        self.sendTakeoverEvent = function (takeover) {
            logger.debug('Screen Takeovers: sendTakeoverEvent called with ' + JSON.stringify(takeover));
            // NOTE: We handle sync and takeovers slightly differently on LG so currently to differentiate we will call TAKEOVER as opposed to SYNC
            var takeoverEvent = (playerType == "LG") ? self._mediaSyncController.EVENTS.TAKEOVER : self._mediaSyncController.EVENTS.SYNC;
            self._mediaSyncController.sendTakeoverEvent(takeoverEvent, JSON.stringify(takeover));
        }; 

        var screenTakeoverOverlay = 'screenTakeover_overlay';
        function getContentScreenshot() {
            var dfd = $.Deferred();

            if(playerType == "LG" || playerType == "BrightSign") {
                try {
                    // Due to issues with video pausing on LG showing a black screen - try and use a content overlay instead
                    _screenCapController.getContentScreenshot().then(function(screenshot) {
                        if($('#' + screenTakeoverOverlay).css('visibility') !== 'visible' && self.preloading == true) { //Lower spec S3 BrightSign sometimes has a delayed response for the screenshot therefore if we are no longer preloading then content is already playing so the transition helper needs to be ignored
                            $('#' + screenTakeoverOverlay).attr('src', 'data:image/jpeg;base64,' + screenshot.data);
                            $('#' + screenTakeoverOverlay).css('visibility', 'visible');
                            screenshot = null;
                        }
                    }).always(function() {
                        dfd.resolve(); //If we fail to get a screenshot then resolve regardless
                    });
                } catch (e) {
                    logger.error("Error getting content screenshot for takeover visuals");
                    dfd.resolve();
                }
            } else {
                dfd.resolve();
            }
            return dfd;
        };

		function pauseActiveVideoPlayers() {
			var dfd = $.Deferred();
            try {
                if(self.takeoverInProgress) {
                    if(self.mediaPlayerView.layoutView) {
                        $(self.mediaPlayerView.layoutView.zoneViews).each(function(index, zoneView) {
                            if(zoneView.widgetViewer && zoneView.widgetViewer.widgetVideoPlayer) { //Tizen+BrightSign has a widget video player that needs to be controlled
                                zoneView.widgetViewer.widgetVideoPlayer.pause();
                            }
                            if (zoneView.videoViewer) {
                                zoneView.videoViewer.pause();
                            }
                            if(zoneView.svgElem) { //If this property exists then this zoneView is a ticker element
                                zoneView.stop();
                            }
                        });
                    }
    
                    if(self.preloadingSecondTakeover) {
                        if(self.mediaPlayerView.inactiveTakeoverViewer) {
                            $(self.mediaPlayerView.inactiveTakeoverViewer.zoneViews).each(function(index, zoneView) {
                                if(zoneView.widgetViewer && zoneView.widgetViewer.widgetVideoPlayer) { //Tizen has a widget video player that needs to be controlled
                                    zoneView.widgetViewer.widgetVideoPlayer.pause();
                                }
                                if (zoneView.videoViewer) {
                                    zoneView.videoViewer.pause();
                                }
                            });
                        }
                    }
                } else {
                    if(self.mediaPlayerView.activeTakeoverViewer) {
                        $(self.mediaPlayerView.activeTakeoverViewer.zoneViews).each(function(index, zoneView) {
                            if(zoneView.widgetViewer && zoneView.widgetViewer.widgetVideoPlayer) { //Tizen+BrightSign has a widget video player that needs to be controlled
                                zoneView.widgetViewer.widgetVideoPlayer.pause();
                            }
                            if (zoneView.videoViewer) {
                                zoneView.videoViewer.pause();
                            }
                        });
                    }
                }
            } catch (e) {
                logger.warning('Screen Takeovers: Issue pausing zoneviews for takeover transitions: ' + e);
            };
            dfd.resolve();
            return dfd;
		};

		self.currentMimeType = null;
		self.takeoverMimeType = null;
		var transitionDelayTimer = null;
		function stopActiveVideoPlayers(currentMimeType, takeoverMimeType, _currentLayout) {
			var dfd = $.Deferred();

			if(currentMimeType && takeoverMimeType) {
				// To attempt to improve transitions for takeovers then we need to know what the mimetype is for first takeover asset
				self.currentMimeType = currentMimeType;
				self.takeoverMimeType = takeoverMimeType;
				currentMimeTypeIsPossiblyVideo = (currentMimeType == "video" || currentMimeType == "widget") ? true : false;
				takeoverMimeTypeIsPossiblyVideo = (takeoverMimeType == "video" || takeoverMimeType == "widget") ? true : false;
				transitionDelayTimer = (currentMimeTypeIsPossiblyVideo && takeoverMimeTypeIsPossiblyVideo) ? false : true;
			}


            // NOTE: Is scheduled content fullscreen?
            var isCurrentContentFullscreen = (self.mediaPlayerView.layoutView && self.mediaPlayerView.layoutView.layout.zones.length == 1 && self.mediaPlayerView.layoutView.layout.zones[0].width == self.mediaPlayerView.layoutView.layout.width && self.mediaPlayerView.layoutView.layout.zones[0].height == self.mediaPlayerView.layoutView.layout.height);
            // NOTE: Is takeover content fullscreen?
            var isTakeoverFullscreen = (_currentLayout && _currentLayout.zones.length == 1 && _currentLayout.zones[0].width == _currentLayout.width && _currentLayout.zones[0].height == _currentLayout.height);

            // NOTE: Layout transitioning
            if(_currentLayout && (!isCurrentContentFullscreen || !isTakeoverFullscreen)) {
                transitionDelayTimer = true;
            }

            if(self.takeoverInProgress) {
                if(self.mediaPlayerView.layoutView) {
                    $(self.mediaPlayerView.layoutView.zoneViews).each(function(index, zoneView) {
                        if(zoneView.videoViewer) {
                            zoneView.videoViewer.stop(true);
                        }
                        if(zoneView.widgetViewer && zoneView.widgetViewer.widgetVideoPlayer && zoneView.widgetViewer.widgetVideoPlayer.status == 'playing') {
                            zoneView.widgetViewer.widgetVideoPlayer.stop(true);
                            zoneView.widgetViewer.hide(true);
                        }
                        if(zoneView.videoViewer && zoneView.videoViewer.stopAndHideDoNotMoveNext) { //BrightSign specific
                            zoneView.videoViewer.stopAndHideDoNotMoveNext(true);
                            zoneView.videoViewer.clearSync();
                            clearVideo(zoneView, self.mediaPlayerView.layoutView.zoneViews.length);
                        }
                    });
                }

                if (self.preloadingSecondTakeover) {
                    if (self.mediaPlayerView.inactiveTakeoverViewer) {
                        $(self.mediaPlayerView.inactiveTakeoverViewer.zoneViews).each(function (index, zoneView) {
                            if (zoneView.videoViewer) {
                                zoneView.videoViewer.stop(true);
                            }
                            if (zoneView.widgetViewer && zoneView.widgetViewer.widgetVideoPlayer && zoneView.widgetViewer.widgetVideoPlayer.status == 'playing') {
                                zoneView.widgetViewer.widgetVideoPlayer.stop(true);
                                zoneView.widgetViewer.hide(true);
                            }
                            if (zoneView.videoViewer && zoneView.videoViewer.stopAndHideDoNotMoveNext) { //BrightSign specific
                                zoneView.videoViewer.stopAndHideDoNotMoveNext(true);
                                zoneView.videoViewer.clearSync();
                                clearVideo(zoneView, self.mediaPlayerView.inactiveTakeoverViewer.zoneViews.length);
                            }
                        });
                    }
                }
            } else {
                if(self.mediaPlayerView.activeTakeoverViewer) {
                    $(self.mediaPlayerView.activeTakeoverViewer.zoneViews).each(function(index, zoneView) {
                        if(zoneView.videoViewer) {
                            zoneView.videoViewer.stop(true);
                        }
                        if(zoneView.widgetViewer && zoneView.widgetViewer.widgetVideoPlayer && zoneView.widgetViewer.widgetVideoPlayer.status == 'playing') {
                            zoneView.widgetViewer.widgetVideoPlayer.stop(true);
                            zoneView.widgetViewer.hide(true);
                        }
                        if(zoneView.videoViewer && zoneView.videoViewer.stopAndHideDoNotMoveNext) { //BrightSign specific
                            zoneView.videoViewer.stopAndHideDoNotMoveNext(true);
                            clearVideo(zoneView, self.mediaPlayerView.activeTakeoverViewer.zoneViews.length);
                        }
                    });
                }
            }

            return dfd.resolve(transitionDelayTimer);
		};

        function stopTakeoverVideoViewers() {
            if (self.mediaPlayerView.activeTakeoverViewer) {
                $(self.mediaPlayerView.activeTakeoverViewer.zoneViews).each(function(index, zoneView) {
                    if (zoneView.videoViewer) {
                        zoneView.videoViewer.stop(false);
                    }
                    if (zoneView.widgetViewer && zoneView.widgetViewer.widgetVideoPlayer && zoneView.widgetViewer.widgetVideoPlayer.status == 'playing') {
                        zoneView.widgetViewer.widgetVideoPlayer.stop(false);
                        zoneView.widgetViewer.hide(false);
                    }
                    if (zoneView.videoViewer && zoneView.videoViewer.stopAndHideDoNotMoveNext) { //BrightSign specific
                        zoneView.videoViewer.stopAndHideDoNotMoveNext(false);
                    }
                });
            }

            if (self.mediaPlayerView.inactiveTakeoverViewer) {
                $(self.mediaPlayerView.inactiveTakeoverViewer.zoneViews).each(function(index, zoneView) {
                    if (zoneView.videoViewer) {
                        zoneView.videoViewer.stop(false);
                    }
                    if (zoneView.widgetViewer && zoneView.widgetViewer.widgetVideoPlayer && zoneView.widgetViewer.widgetVideoPlayer.status == 'playing') {
                        zoneView.widgetViewer.widgetVideoPlayer.stop(false);
                        zoneView.widgetViewer.hide(false);
                    }
                    if (zoneView.videoViewer && zoneView.videoViewer.stopAndHideDoNotMoveNext) { //BrightSign specific
                        zoneView.videoViewer.stopAndHideDoNotMoveNext(false);
                    }
                });
            }
        }

        function stopScheduledContentVideoViewers() {
            if (self.mediaPlayerView.layoutView) {
                $(self.mediaPlayerView.layoutView.zoneViews).each(function(index, zoneView) {
                    if (zoneView.videoViewer) {
                        zoneView.videoViewer.stop(true);
                    }
                    if (zoneView.widgetViewer && zoneView.widgetViewer.widgetVideoPlayer && zoneView.widgetViewer.widgetVideoPlayer.status == 'playing') {
                        zoneView.widgetViewer.widgetVideoPlayer.stop(true);
                        zoneView.widgetViewer.hide(true);
                    }
                    if (zoneView.videoViewer && zoneView.videoViewer.stopAndHideDoNotMoveNext) { //BrightSign specific
                        zoneView.videoViewer.stopAndHideDoNotMoveNext(true);
                    }
                });
            }
        }

        // BrightSign has transition issues in regards to clearing the video element from the canvas between transitions. For layouts where we are using >1 zone then we dont need a timeout but for full screen content we do. Adding a timeout in relation to layouts instead can show a black transition
        function clearVideo(zoneView, layers) {
            if(layers > 1) {
                zoneView.videoViewer.clearVideo();
            } else {
                setTimeout(function() {
                    zoneView.videoViewer.clearVideo();
                }, 100);
            }
        };

        self.synchronize = function(eventData) {
            // Ignore event if controller is not running
            if (self.running === false) {
                logger.warning('Screen Takeovers: Received takeover event but controller is not running. Event will be discarded');
                return;
            }

            logger.debug('Screen Takeovers: Takeover Event received: ' + JSON.stringify(eventData));
            
            var eventType = eventData.type;
            switch(eventType)
            {
                case "PRELOAD_TAKEOVER":
                    var layoutId = eventData.layoutId;
                    _currentLayout = getLayoutById(layoutId);
                    if (_currentLayout && (!self.takeoverInProgress || eventData.preloadingSecondTakeover)) {
                        self.preloading = true;

                        self.takeoverInProgress = true;

						pauseActiveVideoPlayers().then(function() {
                            var stopLayoutViewsImmediately = playerType == "LG";
                            self.mediaPlayerView.preloadScreenTakeover(_currentLayout, PlaybackStartEndReasons.PRELOAD_LAYOUT, null, eventData.preloadingSecondTakeover, stopLayoutViewsImmediately);      
                            self.contentPreloaded = true;
                            getContentScreenshot().then(function() {
                                if(self.preloading && self.takeoverInProgress) {
                                    stopScheduledContentVideoViewers();                             
                                    logger.debug("Preloaded takeover");
                                }
                        });
                        });
                    }
					registerEventListeners(eventData.index);
                    // Fail out of takeover timer if disconnected from master or no content update to initiate takeover
                    self.failureTimer = setTimeout(function() { failOutOfTakeover(layoutId); }, self.failOutOfTakeoverTimeout);
                    break;
                case "TAKEOVER_START":
					try {

                        // Stop listening for asset updates
                        $(self.mediaPlayerView.layoutView.zoneViews).each(function(index, zoneView) {
                            if(zoneView.removeAssetStartEventListener) {
                                zoneView.removeAssetStartEventListener();
                                zoneView.removeAssetCompleteEventListener();
                            }
                        });
                        
                        $(self.mediaPlayerView.inactiveTakeoverViewer.zoneViews).each(function(index, zoneView) {
                            if(zoneView.removeAssetStartEventListener) {
                                zoneView.removeAssetStartEventListener();
                                zoneView.removeAssetCompleteEventListener();
                            }
                        });

                        clearTimeout(self.failureTimer);
                        self.preloading = false;

                        if(self.contentPreloaded == false) {
                            logger.error("Takeover not preloaded; failing");
                            failOutOfTakeover(layoutId);
                            return;
                        };

                        self.contentPreloaded = false;
                        
						var layoutId = eventData.layoutId;
						_currentLayout = getLayoutById(layoutId);
						if (_currentLayout) {
                            logger.debug("Started takeover");
                            var currentMediaAssetMimeType = (self.mediaPlayerView.layoutView && self.mediaPlayerView.layoutView.mainZone.currentScheduledPlaylistMediaAsset) ? self.mediaPlayerView.layoutView.mainZone.currentScheduledPlaylistMediaAsset.mediaAsset.contentType : null;
							var firstTakeoverAssetMimeType = _currentLayout.zones[0].defaultPlaylist.playlistMediaAssets[0].mediaAsset.contentType;

                            self.takeoverAssetFailureTimer = (_currentLayout.zones[0].defaultPlaylist.playlistMediaAssets[0].duration && _currentLayout.zones[0].defaultPlaylist.playlistMediaAssets[0].duration > 0) ? (_currentLayout.zones[0].defaultPlaylist.playlistMediaAssets[0].duration + 10000) : (_currentLayout.zones[0].defaultPlaylist.playlistMediaAssets[0].mediaAsset.duration + 10000);
							logger.debug("Screen Takeovers: Play takeover: " + layoutId + " Layout Name is: " + _currentLayout.zones[0].defaultPlaylist.name);

							// Start playing the takeover
							// Widgets usually wait 1.5s before showing their content on the media media player in regards to transitions. We want to temporarily for the beginning of a takeover ignore this constraint.
							stopActiveVideoPlayers(currentMediaAssetMimeType, firstTakeoverAssetMimeType, _currentLayout).then(function(delayTimer) {
                                self.preloadingSecondTakeover = false; //Reset this flag

                                $(self.mediaPlayerView.activeTakeoverViewer.zoneViews).each(function(index, zoneView) {
                                    if(zoneView.widgetViewer) {
                                        zoneView.widgetViewer.defaultTimeToWaitForReadyEventInMS = 0;
                                    }
                            });

								self.mediaPlayerView.playScreenTakeover(_currentLayout, PlaybackStartEndReasons.TAKEOVER, delayTimer);
							});
	
                            registerEventListeners(eventData.index);

                            if(self.takeoverAssetFailureTimer) {
                                logger.debug("First Asset Failure Timer: " + self.takeoverAssetFailureTimer);
                                self.failureTimer = setTimeout(function() { failOutOfTakeover(layoutId); }, self.takeoverAssetFailureTimer);
                            }
						}
					} catch (e) {
						logger.debug("Starting the takeover failed: " + JSON.stringify(e));
						failOutOfTakeover(layoutId);
					}
                    break;
                case "MOVE_NEXT":
                    clearTimeout(self.failureTimer);
                    if(self.takeoverInProgress) { //If slave player already playing then update index only - else start the takeover from this index
                        self.takeoverAssetFailureTimer = (_currentLayout.zones[0].defaultPlaylist.playlistMediaAssets[eventData.index].mediaAsset.duration !== 0) ? (_currentLayout.zones[0].defaultPlaylist.playlistMediaAssets[eventData.index].mediaAsset.duration * 1000) + 10000 : null;
                        // Just move to the provided asset in the main zone
                        pauseActiveVideoPlayers().then(function() {
                            getContentScreenshot().then(function() {
                                $(self.mediaPlayerView.activeTakeoverViewer.zoneViews).each(function(index, zoneView) {
                                    if (zoneView.widgetViewer) {
                                        zoneView.widgetViewer.defaultTimeToWaitForReadyEventInMS = 0;
                                    }
                                });
                                self.mediaPlayerView.moveToAssetMainZone(eventData.index);
                            });
                        });

                        // NOTE: Automatically fail out of a takeover if it doesnt complete the master's first asset duration loop
                        if(self.takeoverAssetFailureTimer) {
                            self.failureTimer = setTimeout(function() { failOutOfTakeover(layoutId); }, self.takeoverAssetFailureTimer);
                        }

                    }
                    // NOTE: If a takeover misses the start - should it try and join in partway through?
                    // else {
                    //     eventData.type = "TAKEOVER_START";
                    //     self.synchronize(eventData);
                    // }
                    break;
                case "TAKEOVER_COMPLETE":
                    clearTimeout(self.failureTimer);
                    // Return to playing scheduled content
                    if (self.takeoverInProgress && self.preloading == false) {
                        self.preloading = true;

                        var layoutId = eventData.layoutId;

                        if (eventData.preloadingSecondTakeover == true && eventData.timeUntilNextTakeoverEvent > 0) {
                            self.preloadingSecondTakeover = true;
                            self.mediaPlayerView.switchActiveTakeoverViewers();
                            if (self.isMaster) {
                                self.startSyncTimer(true).then(function (startPlayback) {
                                    if (startPlayback) {
                                        setTimeout(function () {
                                            var secondTakeover = {
                                                type: 'TAKEOVER_START',
                                                layoutId: self.scheduledTakeoverEvents[0].takeover.layoutId,
                                                index: -1
                                            };
                                            // Send the event so the slaves start playing
                                            logger.info('Screen Takeovers: Telling slaves to start screen takeover');
                                            self.sendTakeoverEvent(secondTakeover); //For other connected players
                                            setTimeout(function () {
                                                self.preloading = false;
                                                logger.info('Screen Takeovers: Starting Takeover Playing!')
                                                if (playerType !== "BrightSign") {
                                                    self.synchronize(secondTakeover) //For the Master
                                                }
                                            }, 150);
                                        }, timeUntilNextTakeoverEvent)
                                    }
                                });
                            }
                        } else {
							self.preloadingSecondTakeover = false;

							try {
                                // This has to be here due to transition issues with putting the self.takeoverInProgress before now causing gapless videos to continue playing even when you try and stop them ..
                                // For non-LG update the takeoverInProgress status here as per the above comment. This may need to be adjusted for Brightsign?
                                if (playerType != "LG" && playerType != "BrightSign") {
                                    self.takeoverInProgress = false;
                                }

                                logger.debug('pausing takeover video');

                                pauseActiveVideoPlayers().then(function() {

                                    logger.debug('Takeover video paused');
                                    logger.debug('Taking screenshot...');
                                    getContentScreenshot().then(function() {

                                        setTimeout(function() {

                                            // Stop and hide the takeover viewer. Fixes WOS-199 where the last frame of the screen takeover can flash during the transition.
                                            // At this point we are shoing the screen capture overlay so we no longer need this to be loaded.
                                            logger.debug('Stopping takeover video');
                                            stopTakeoverVideoViewers();

                                            logger.debug('Stopping active videos');
                                            stopActiveVideoPlayers().then(function(delayTimer) {
                                                self.mediaPlayerView.switchActiveTakeoverViewers();

                                                // Preload the Scheduled Content
                                                var stopTakeoverViewsImmediately = playerType == "LG" || playerType == "BrightSign";
                                                self.mediaPlayerView.preloadScheduledContent(PlaybackStartEndReasons.PRELOAD_LAYOUT, stopTakeoverViewsImmediately);

                                                setTimeout(function () {
                                                    
                                                    // For LG update the takeoverInProgress status here to ensure transitions are correct
                                                    if(playerType == "LG" || playerType == "BrightSign") {
                                                        self.takeoverInProgress = false;
                                                    }

                                                    $(self.mediaPlayerView.layoutView.zoneViews).each(function(index, zoneView) {
                                                        if(zoneView.widgetViewer) {
                                                            zoneView.widgetViewer.defaultTimeToWaitForReadyEventInMS = 0;
                                                        }
                                                    });

                                                    // If the scheduled content completed its media asset playback before going into a takeover then it will need to +1 to ensure it plays a new asset based on the moveNext functionality when starting the playlist
                                                    // Otherwise to prevent double skipping, etc we should -1 the asset index here
                                                    if(scheduledContentAssetComplete[scheduledPlaylistLayerId] && !scheduledContentIsPlayingGapless) {
                                                        scheduledPlaylistAssetToPlay[scheduledPlaylistLayerId] -= 1;
                                                    }

                                                    self.mediaPlayerView.resumeScheduledContentFromAsset(PlaybackStartEndReasons.SCHEDULE, scheduledPlaylistAssetToPlay);
                                                    self.mediaPlayerView.layoutView.mainZone.scheduleTakeoverSync(self._takeoverGroupConfig, self._clientDetails).then(function () {
                                                        logger.debug("Screen Takeovers: Scheduled next takeover ..");
                                                        setTimeout(function() {
                                                            $(self.mediaPlayerView.layoutView.zoneViews).each(function(index, zoneView) {
                                                                if(zoneView.widgetViewer) {
                                                                    zoneView.widgetViewer.defaultTimeToWaitForReadyEventInMS = 1500; //Reset this back to what it should be ...
                                                                }
                                                            });
                                                        }, 1500);
                                                    });
                                                   
                                                    registerEventListeners(self.scheduledPlaylistAssetIndex);
                                                    self.preloading = false;
                                                }, self.scheduledContentPreloadTimeout); //was 1500ms
                                            });
                                        }, 250);
                                    });
                                });
							} catch (e) {
								logger.error("Error stopping takeover");
							}
						}
					}
                    break;
            }
        };

		function failOutOfTakeover(layoutId) {
			var dfd = $.Deferred();
			logger.error("Error starting takeover - defaulting back to scheduled content");

			if(self.isMaster) {
				// If the master does this then it should also tell the Slaves to? NEW: - artifically complete the takeover?						
				self.sendTakeoverEvent({
					type: 'TAKEOVER_COMPLETE',
					index: self.takeoverPlaylistAssetIndex,
					layoutId: layoutId,
                    preloadingSecondTakeover: false
				});

				// End the current takeover
                if(playerType !== "BrightSign") {
                    self.synchronize({
                        type: "TAKEOVER_COMPLETE",
                        index: self.takeoverPlaylistAssetIndex,
                        layoutId: layoutId,
                        preloadingSecondTakeover: false
                });
                }
			} else {
                // Slaves should end their current takeover and fail out due to issues staying connected to the master ..
				self.synchronize({
					type: "TAKEOVER_COMPLETE",
					index: self.takeoverPlaylistAssetIndex,
					layoutId: layoutId,
                    preloadingSecondTakeover: false
                });
            }

			// Reset Takeover playlist for next takeover
			self.takeoverPlaylistAssetIndex = -1;

			return dfd.resolve();
		};

        self.setSyncInProgress = function (status) {
            self.takeoverInProgress = status;
            return $.Deferred().resolve();
        }

        self.savePlaylistIndex = function(index) {
            logger.debug("Saving current playlist index as: " + index);
            self.scheduledPlaylistAssetIndex = index;
            self.savedPlaylistIndex = index;
            return $.Deferred().resolve();
        };

        self.getPlaylistIndex = function() {
            logger.debug("Getting current playlist index as: " + self.savedPlaylistIndex);
            return $.Deferred().resolve(self.savedPlaylistIndex);
        };

        self.start = function (mediaPlayerView, layouts, takeovers, clientDetails, takeoverGroup) {
            self.clearTakeoverTimers();

            // NOTE: Update takeover group details in line and then disconnect and reconnect the media sync server as required
            try {
                if(self._takeoverGroupConfig || self._mediaSyncController.isSyncRunning()) {
                    if ((self._takeoverGroupConfig && self._takeoverGroupConfig.master !== takeoverGroup.master) || !self._takeoverGroupConfig) {
                        self._mediaSyncController.stop();
                    }
                }
            } catch (e) {
                logger.debug("Failed to update takeover configuration: " + JSON.stringify(e));
            }

            self._takeovers = takeovers.reverse(); //Revert the order of STs so that the most recent published schedule is the most valid
            self.mediaPlayerView = mediaPlayerView;
            self._layouts = layouts;
            self._clientDetails = clientDetails;
            self._takeoverGroupConfig = takeoverGroup;

            logger.info("Screen Takeovers: Starting Screen Takeover Controller ..");

            // Is this player a master or slave?
            self.isMaster = (self._takeoverGroupConfig.master == clientDetails.signageliveSerialNumber);

            // Is the master or slave connecting twice?
            // Start the Sync Service so players can connect but do not start sync playback yet
            if (self.isMaster) {
                logger.debug('Player is the master');
                // NOTE: consider making these calls return a promise so we know when the master and slave is actually ready
                self._mediaSyncController.startMaster(self._takeoverGroupConfig.id, self.synchronize, false); // Do not start straight away
            }
            else {
                logger.debug('Player is a slave');
                // NOTE: consider making these calls return a promise so we know when the master and slave is actually ready
                self._mediaSyncController.startSlave(self._takeoverGroupConfig.id, self.synchronize, resumeScheduledContent);
            }

            if (self.isMaster) { // NOTE: There is no enabled flag on takeoverGroupConfig so this cant be checked
                calculateTakeoverSchedules();
            }

            self.running = true;
            registerEventListeners(self.scheduledPlaylistAssetIndex);
        }

        self.clearTakeoverTimers = function() {
            clearTimeout(retryTimer);
            clearTimeout(self.failureTimer);
            clearTimeout(self.takeoverAssetFailureTimer);
            clearTimeout(self._takeoverEventTimer); //Clear the previous event timer as the media player doesnt stop when a new schedule is published (?)
            clearTimeout(self._preloadTimer);
        };

        self.stop = function () {
        	var dfd = $.Deferred();
        	logger.info("Screen Takeovers: Stopping Screen Takeover Controller ..");
        	self.running = false;
            self.preloading = false;
        	self.takeoverInProgress = false;
            self._takeovers = []; //Reset the takeover configuration of valid events
        	self.scheduledTakeoverEvents = []; //Reset the events available as well
        	self.clearTakeoverTimers();
			self.mediaPlayerView.activeTakeoverViewer.stop(PlaybackStartEndReasons.SCHEDULE);
            $('#' + screenTakeoverOverlay).css('visibility', 'hidden');
            $('#' + screenTakeoverOverlay).attr('src', ''); //unset the source
        	dfd.resolve();
        	return dfd;
        };

		function resumeScheduledContent() {
			if(self.takeoverInProgress) {
				logger.info("Screen Takeovers: Connection Failed to Master - Stopping Screen Takeover If In Progress and Restarting Scheduled Content ..");
				self.running = false;
				self.takeoverInProgress = false;
				self.scheduledTakeoverEvents = []; //Reset the events available as well
				self.clearTakeoverTimers();
				self.mediaPlayerView.preloadScheduledContent(PlaybackStartEndReasons.PRELOAD_LAYOUT);
				setTimeout(function () {
					stopActiveVideoPlayers()//.then(function(delayTimer) {
                        $(self.mediaPlayerView.layoutView.zoneViews).each(function(index, zoneView) {
                            if(zoneView.widgetViewer) {
                                zoneView.widgetViewer.defaultTimeToWaitForReadyEventInMS = 0;
                            }
                        });

                        // If the scheduled content never completed its media asset playback before going into a takeover then it will need to +1 to ensure it plays a new asset based on the moveNext functionality when starting the playlist
                        // Otherwise to prevent double skipping, etc we should -1 the asset index here
                        if(scheduledContentAssetComplete[scheduledPlaylistLayerId] && !scheduledContentIsPlayingGapless) {
                            scheduledPlaylistAssetToPlay[scheduledPlaylistLayerId] -= 1;
                        }

						self.mediaPlayerView.resumeScheduledContentFromAsset(PlaybackStartEndReasons.TAKEOVER, scheduledPlaylistAssetToPlay);
						self.mediaPlayerView.layoutView.mainZone.scheduleTakeoverSync(self._takeoverGroupConfig, self._clientDetails).then(function () {
							logger.info("Screen Takeovers: Terminated and restarted Screen Takeover connection");
						});
					registerEventListeners(self.scheduledPlaylistAssetIndex);
				}, self.scheduledContentPreloadTimeout); //was 1500ms
			} else {
				// TODO: If a takeover is not in progress then still need to disconnect and reconnect to the Master
				logger.info("Screen Takeovers: Connection Failed to Master - Stopping Screen Takeover If In Progress and Restarting Scheduled Content ..");
				self.running = false;
				self.scheduledTakeoverEvents = []; //Reset the events available as well
				self.clearTakeoverTimers();
				self.mediaPlayerView.layoutView.mainZone.scheduleTakeoverSync(self._takeoverGroupConfig, self._clientDetails).then(function () {
					logger.info("Screen Takeovers: Terminated and restarted Screen Takeover connection");
				});
			}
		};

        var onZoneAssetStart = function(currentAssetIndex, layoutId, layerId) {
            logger.debug('onZoneAssetStart() Index: ' + currentAssetIndex);

            if(currentAssetIndex == -1) {
                currentAssetIndex++;
            }

            // Keep track of where we are in the scheduled playlist
            if (self.takeoverInProgress == false) {
                scheduledPlaylistLayerId = layerId;
                scheduledContentAssetComplete[layerId] = false;
                self.scheduledPlaylistAssetIndex[layerId] = currentAssetIndex; //This should save whenver an asset transitions in playlist
                scheduledPlaylistAssetToPlay[layerId] = currentAssetIndex;
            }
            else {
                self.takeoverPlaylistAssetIndex = currentAssetIndex;
            }
        };

        var onZoneAssetComplete = function(currentAssetIndex, layoutId, layerId, playlistEnded, nextAssetIndex) {

            clearTimeout(self.failureTimer);

            if(currentAssetIndex == -1) {
                currentAssetIndex++;
            }

            logger.debug('onZoneAssetComplete() Index: ' + currentAssetIndex);
            // Keep track of where we are in the scheduled playlist
            if (self.takeoverInProgress == false) {
                scheduledPlaylistLayerId = layerId;
                scheduledContentAssetComplete[layerId] = true;
                self.scheduledPlaylistAssetIndex[layerId] = currentAssetIndex; //This should save whenver an asset transitions in playlist
                scheduledPlaylistAssetToPlay[layerId] = currentAssetIndex;
            }
            else {
                self.takeoverPlaylistAssetIndex = currentAssetIndex;
            }
            
            logger.debug('Is Master: ' + self.isMaster);

            if (self.isMaster) {
                // Control moving next or ending the takeover. Check that this ZONE_ASSET_COMPLETE event is for an asset that is part of the current takeover layout before proceeding
                if (self.takeoverInProgress && self.preloading == false && _currentLayout.externalId == layoutId) {
                    var takeoverPlaylist = getTakeoverPlaylist();
                    var takeoverLayoutID = self.currentTakeoverEvent;
                    var preloadingNextTakeover = false;

                    // NOTE: If there is no takeover playlist here then we dont complete takeover playback - need to consider if this will cause issues or not
                    if (takeoverPlaylist) {

                        if (playlistEnded) {
                            logger.debug('Playlist has completed. Sending end takeover event...');

                            try {
                                logger.debug('Determine when next takeover is due to start');
                                // Get next takeover
                                var currentTime = DateTime.now();
                                // Add preload buffer to next takeover time (??)
                                var nextTakeoverTime = (self.scheduledTakeoverEvents[0]) ? self.scheduledTakeoverEvents[0].startTime.getTime() : null;
                                var timeUntilNextTakeover = nextTakeoverTime - currentTime.getTime(); // self.scheduledTakeoverEvents[0].startTime.getTime() - currentTime.getTime();
                            
                                if (timeUntilNextTakeover < 2000 && timeUntilNextTakeover > 0) {
                                    timeUntilNextTakeoverEvent = (timeUntilNextTakeoverEvent < 2000) ? 2000 : timeUntilNextTakeoverEvent;
                                    preloadingNextTakeover = true;
                                }
                            } catch(e) {
                                logger.warning("Issue determining when next takeover is due to start");
                            }

                            self.sendTakeoverEvent({
                                type: 'TAKEOVER_COMPLETE',
                                index: -1,
                                layoutId: (takeoverLayoutID && takeoverLayoutID.layoutId) ? takeoverLayoutID.layoutId : takeoverLayoutID.takeover.layoutId,
                                nextTakeoverTime: (self.scheduledTakeoverEvents[0]) ? self.scheduledTakeoverEvents[0].startTime.getTime() : null,
                                preloadingSecondTakeover: preloadingNextTakeover,
                                timeUntilNextTakeoverEvent: timeUntilNextTakeover
                            });

                            // End the current takeover
                            if(playerType !== "BrightSign") {
                                self.synchronize({
                                    type: "TAKEOVER_COMPLETE",
                                    index: -1,
                                    layoutId: takeoverLayoutID.layoutId,
                                    nextTakeoverTime: (self.scheduledTakeoverEvents[0]) ? self.scheduledTakeoverEvents[0].startTime.getTime() : null,
                                    preloadingSecondTakeover: preloadingNextTakeover,
                                    timeUntilNextTakeoverEvent: timeUntilNextTakeover
                                });
                            }

                            // Reset Takeover playlist for next takeover
                            self.takeoverPlaylistAssetIndex = -1;

                        } else {

                            logger.debug('Playlist has not completed. Sending takeover move next event...');

                            self.sendTakeoverEvent({
                                type: 'MOVE_NEXT',
                                index: self.takeoverPlaylistAssetIndex,
                                layoutId: (takeoverLayoutID && takeoverLayoutID.layoutId) ? takeoverLayoutID.layoutId : takeoverLayoutID.takeover.layoutId
                            });

                            // NOTE: We are currently choosing to moveNext() here for the master as well
                            if(playerType !== "BrightSign") {
                                self.synchronize({
                                    type: "MOVE_NEXT",
                                    index: self.takeoverPlaylistAssetIndex,
                                    layoutId: takeoverLayoutID.layoutId
                                });
                            }

                            self.takeoverPlaylistAssetIndex = nextAssetIndex;
                        }
                    }
                }
            }
        }

        var getTakeoverPlaylist = function() {
            // Return if we do not have a current layout set
            if (_currentLayout == null) {
                logger.error('Current layout not set. Unable to get next asset in takeover playlist');
                return null;
            }

			// NOTE: This is sometimes null?
            var takeoverPlaylist = (self.mediaPlayerView.activeTakeoverViewer.mainZone) ? self.mediaPlayerView.activeTakeoverViewer.mainZone.currentPlaylist : null;
            if (takeoverPlaylist == null) {
                logger.error('Unable to get current playlist for takeover layout. Unable to get next asset in takeover playlist');
                return null;
            }

            return takeoverPlaylist;
        }

        var getLayoutById = function(layoutId) {
            var matchingLayouts = $.grep(self._layouts, function(layout) {
                return layout.externalId == layoutId;
            });
            return matchingLayouts[0];
        }

        function registerEventListeners(index) {
            // NOTE: On init the main zone is not available - therefore keep trying this until we can apply the event listeners.
            // On takeover start and complete we also need the delay so the mainZone loads with the new layer ID otherwise the listener event gets applied to the wrong layer.
            eventListenerTimer = setTimeout(function() {
                // NOTE: Depending on whether a takeover is currently in progress or not we have a different LayoutView that we need to add the listeners to.
				if (self.takeoverInProgress) {
                    // If takeover is in progress then we only care about the mainZone (takeovers are playlist only so can only have 1 zone)
                    if (self.mediaPlayerView.activeTakeoverViewer && self.mediaPlayerView.activeTakeoverViewer.mainZone && self.mediaPlayerView.activeTakeoverViewer.mainZone.id) {
                        clearTimeout(eventListenerTimer);
                        self.mediaPlayerView.activeTakeoverViewer.mainZone.registerAssetStartEventListener(onZoneAssetStart);
                        self.mediaPlayerView.activeTakeoverViewer.mainZone.registerAssetCompleteEventListener(onZoneAssetComplete);
                        if (index > -1) {
                            self.mediaPlayerView.activeTakeoverViewer.mainZone.setStartingAssetIndex(index);
                        }
                    }
                    else {
                        registerEventListeners(index); // Try again
                    }
                } else {
                    // Scheduled Content - We need to register the listener for all zones
                    if (self.mediaPlayerView.layoutView.zoneViews && self.mediaPlayerView.layoutView.zoneViews.length > 0) {
                        clearTimeout(eventListenerTimer);
                        $(self.mediaPlayerView.layoutView.zoneViews).each(function(index, zoneView) {
                            if(zoneView.registerAssetStartEventListener) { // TextZoneViews do not have these functions 
                                zoneView.registerAssetStartEventListener(onZoneAssetStart);
                                zoneView.registerAssetCompleteEventListener(onZoneAssetComplete);
                                if (index[zoneView.id] > -1) {
                                    zoneView.setStartingAssetIndex(index[zoneView.id]);
                                }
                            }
                        });
                        
                    }
                    else {
                        registerEventListeners(index); // Try again
                    }
                }
            }, 750); // TODO: Should this be configurable? What does this actually do?
        };

        function sortTakeoverEventsByDate() {
            self.scheduledTakeoverEvents.sort(function(a, b) {
                if (a.startTime < b.startTime) {
                    return -1;
                }
                if (a.startTime > b.startTime) {
                    return 1;
                }
                return 0;
            });
        };

        function calculateTakeoverSchedules(fromDate) {
            return new Promise(function (resolve, reject) {
				// Get the start time based on a :00 seconds timestamp
                var currentDate = fromDate ? fromDate : new Date(DateTime.now().setSeconds(0));
                var bufferDateTime = fromDate ? fromDate : new Date(currentDate + 10000); //10 Seconds Buffer
                currentHour = new Date(currentDate).getHours();
                var startOfCurrentHour = new Date(currentDate).setMinutes(0, 0);
                var oneHourFromNow = new Date(startOfCurrentHour).setHours(currentHour + 1);
                var currentScheduleTime = new Date(startOfCurrentHour).getTime();
                var offsetTimeInMS = 250;

                // 1. Filter out schedules that are not valid to play today based on days (not implemented in prototype)
                // Also need to make sure it is considering those valid before the hour time slot completion
                var filteredConfig = self._takeovers.filter(function (takeover) {
                    var scheduleStartDate = new Date(currentDate).setTime('00:00:00');
                    if (takeover.recurrence.isActiveOnDate(scheduleStartDate) && (currentDate.getTime() > new Date(takeover.start).getTime() || new Date(takeover.start).getTime() < oneHourFromNow)) { // is it valid today?
                        return takeover;
                    }
                });

                // 2: Iterate over schedules and build up a flattened schedule starting from the beginning of the current hour.
                // Further filter out schedules that are not valid to play based on recurrence when to play times
                for (var takeover of filteredConfig) {
                    switch (takeover.recurrence.whenToPlay) {
                        case "play_once_a_day":
                            var sentSingleEvent = false;
                            var nextEvent = takeover.getNextTakeoverEvent();

                            // If schedule is currently valid
                            var scheduleStartTime = (takeover.start) ? takeover.start.getTime() : null;
                            var scheduleEndTime = (takeover.end) ? takeover.end.getTime() : null;

                            if (nextEvent.startTime.getTime() >= startOfCurrentHour && nextEvent.startTime.getTime() >= scheduleStartTime && nextEvent.startTime.getTime() < scheduleEndTime) {
                                // If valid within the next hour then we need to schedule it
                                if (nextEvent.startTime.getTime() <= oneHourFromNow) {
                                    var isConflictingEvent = self.scheduledTakeoverEvents.some(function (elem) {
                                        if (((nextEvent.startTime < elem.endTime) && (nextEvent.endTime > elem.startTime) && elem.takeover.recurrence.whenToPlay !== elem.takeover.recurrence.whenToPlay) || (elem.takeover.start < nextEvent.startTime && elem.id !== nextEvent.id && elem.takeover.recurrence.whenToPlay !== elem.takeover.recurrence.whenToPlay || (elem.takeover.recurrence.whenToPlay == "PlayOnceADay" && nextEvent.startTime.getTime() == elem.startTime.getTime()))) {
                                            return true;
                                        }
                                    });

                                    if (isConflictingEvent === false && nextEvent.takeover.getTakeoverDuration() !== 0) {
                                        if (sentSingleEvent == false) {
                                            sentSingleEvent = true;
                                            // Add the takeover event because it is valid
                                            self.scheduledTakeoverEvents.push(nextEvent);
                                        }
                                    }
                                }
                            }
                            break;
                        case "play_every_x_seconds_all_day":
                            // Check should play based on recurrence and start
                            var scheduleStartTime = (takeover.start) ? takeover.start.getTime() : null;
                            var scheduleEndTime = (takeover.end) ? takeover.end.getTime() : null;

                            var recurringStartTime = (takeover.recurrence.startTime) ? new Date(DateTime.now()).setTime(takeover.recurrence.startTime).getTime() : new Date(DateTime.now()).setTime("00:00:00").getTime();
                            var recurringEndTime = (takeover.recurrence.endTime) ? new Date(DateTime.now()).setTime(takeover.recurrence.endTime).getTime() : new Date(DateTime.now()).setTime("23:59:59").getTime();

                            // Store current asset index for the takeover
                            takeover.currentAssetIndex = 0;

                            if (!takeover.currentScheduleTime && takeover.recurrence.startTime !== "00:00:00") {
                                takeover.currentScheduleTime = recurringStartTime;
                            } else {
                                takeover.currentScheduleTime = currentScheduleTime; //Is this correct?                               
                            }

                            // Create schedules for current takeover working from the start of the current hour and looking ahead for one hour
                            while (takeover.currentScheduleTime < oneHourFromNow) {

                                if (takeover.currentScheduleTime >= startOfCurrentHour && takeover.currentScheduleTime >= scheduleStartTime && takeover.currentScheduleTime < scheduleEndTime && takeover.currentScheduleTime > recurringStartTime && takeover.currentScheduleTime < recurringEndTime) {
                                    // Get the next takeover event
                                    var nextEvent = takeover.getNextTakeoverEvent();

                                    // Check for conflicts with existing events
                                    var isConflictingEvent = self.scheduledTakeoverEvents.some(function (elem) {
                                        var elemRecurrenceStartTime = (elem.takeover.recurrence.startTime) ? new Date(DateTime.now()).setTime(elem.takeover.recurrence.startTime) : null;
                                        var elemRecurrenceEndTime = (elem.takeover.recurrence.endTime) ? new Date(DateTime.now()).setTime(elem.takeover.recurrence.endTime) : null;

                                        if((nextEvent.startTime < elem.endTime) && (nextEvent.endTime > elem.startTime) ||
                                        // If the same schedule type is already scheduled for today
                                        (elem.takeover.recurrence.whenToPlay == nextEvent.takeover.recurrence.whenToPlay && elem.id !== nextEvent.id) ||
                                        ((elem.takeover.recurrence.whenToPlay == "PlayBetween") && (elemRecurrenceStartTime < nextEvent.startTime) && (nextEvent.startTime < elemRecurrenceEndTime) && (elemRecurrenceEndTime > nextEvent.endTime) && nextEvent.takeover.currentScheduleTime >= elem.startTime)
                                        ) {
                                            return true;
                                        }
                                    });

                                    if (isConflictingEvent === false && nextEvent.takeover.getTakeoverDuration() !== 0) {
                                        // Add Takeover event
                                        self.scheduledTakeoverEvents.push(nextEvent);
                                    }
                                } else {
                                    // Increment by the repeat interval to continue checking?
                                    takeover.currentScheduleTime = new Date(takeover.currentScheduleTime).addSeconds(takeover.recurrence.repeatEveryXSeconds).getTime();
                                }
                            }
                            break;
                        case "play_every_x_seconds_between_confgured_start_and_end_times":
                            // Check should play based on recurrence and start
                            var scheduleStartTime = (takeover.start) ? takeover.start.getTime() : null;
                            var scheduleEndTime = (takeover.end) ? takeover.end.getTime() : null;

                            var recurringStartTime = (takeover.recurrence.startTime) ? new Date(DateTime.now()).setTime(takeover.recurrence.startTime).getTime() : null;
                            var recurringEndTime = (takeover.recurrence.endTime) ? new Date(DateTime.now()).setTime(takeover.recurrence.endTime).getTime() : null;

                            // Store current asset index for the takeover
                            takeover.currentAssetIndex = 0;

                            if (!takeover.currentScheduleTime && takeover.recurrence.startTime !== "00:00:00") {
                                takeover.currentScheduleTime = recurringStartTime;
                            } else {
                                takeover.currentScheduleTime = currentScheduleTime; //Is this correct?                               
                            }

                            // Create schedules for current takeover working from the start of the current hour and looking ahead for one hour
                            while (takeover.currentScheduleTime < oneHourFromNow) {

                                if (takeover.currentScheduleTime >= startOfCurrentHour && takeover.currentScheduleTime >= scheduleStartTime && takeover.currentScheduleTime < scheduleEndTime && takeover.currentScheduleTime > recurringStartTime && takeover.currentScheduleTime < recurringEndTime) {
                                    // Get the next takeover event
                                    var nextEvent = takeover.getNextTakeoverEvent();

                                    // Check for conflicts with existing events
                                    var isConflictingEvent = self.scheduledTakeoverEvents.some(function (elem) {
                                        var elemRecurrenceStartTime = (elem.takeover.recurrence.startTime) ? new Date(DateTime.now()).setTime(elem.takeover.recurrence.startTime) : null;
                                        var elemRecurrenceEndTime = (elem.takeover.recurrence.endTime) ? new Date(DateTime.now()).setTime(elem.takeover.recurrence.endTime) : null;

                                        if((nextEvent.startTime < elem.endTime) && (nextEvent.endTime > elem.startTime) ||
                                        // If the same schedule type is already scheduled for today and the recurrence times are the same (or they overlap? NOTE:)
                                        (elem.takeover.recurrence.whenToPlay == nextEvent.takeover.recurrence.whenToPlay && elem.id !== nextEvent.id) ||
                                        ((elem.takeover.recurrence.whenToPlay == "PlayEveryXSecondsAllDay") && (elemRecurrenceStartTime < nextEvent.startTime) && (nextEvent.startTime < elemRecurrenceEndTime) && (elemRecurrenceEndTime > nextEvent.endTime) && nextEvent.takeover.currentScheduleTime >= elem.startTime)
                                        ) {
                                            return true;
                                        }
                                    });

                                    if (isConflictingEvent === false && nextEvent.takeover.getTakeoverDuration() !== 0) {
                                        // Add Takeover event
                                        self.scheduledTakeoverEvents.push(nextEvent);
                                    } else {
                                        // NOTE: Does there need to be a change here so if the events conflict we change the currentScheduleTime to be based on the startTime instead?
                                    }
                                } else {
                                    // Increment by the repeat interval to continue checking?
                                    // NOTE: Also need to make sure that we start from the beginning of a takeover playlists validity period (??)
                                    takeover.currentScheduleTime = new Date(takeover.currentScheduleTime).addSeconds(takeover.recurrence.repeatEveryXSeconds).getTime();
                                }
                            }
                            break;
                    }
                }
		
                // Filter events in the past
                // NOTE: Added a buffer time for this in regards to when it resyncs - bufferDateTime
                self.scheduledTakeoverEvents = self.scheduledTakeoverEvents.filter(function(event) { if(event.startTime.getTime() > bufferDateTime.getTime()) { return event; } else if (event.startTime.getTime() == currentDate.getTime() && bufferDateTime.getTime() == currentDate.getTime()) { return event; } });

                // NOTE: Brightsign only - offset the start calculation of events to be 250ms beforehand due to the Synchronize() logic
                self.scheduledTakeoverEvents.forEach(function(event) {
                    event.startTime = new Date(event.startTime.getTime() - offsetTimeInMS);
                    event.endTime = new Date(event.endTime.getTime() - offsetTimeInMS);
                });

                // sort the array by startTime. Ensures when we have multiple schedules that all events are in chronological order
                sortTakeoverEventsByDate();

                resolve();

                logger.debug("Scheduled takeovers available in total: " + self.scheduledTakeoverEvents.length);
            });
        };

        var lastHourChecked;
        self.startSyncTimer = function(forcePreloadNow) {
            var dfd = $.Deferred();
            self.takeoverInProgress = false;

            if (self._takeoverEventTimer !== null) {
                self.clearTakeoverTimers();
            }

            // NOTE: To circumvent two controllers being active after a content update ..
            if (self.running === false) {
                logger.warning('Screen Takeovers: Received start sync timer command but controller is not running. Action will be discarded');
                return dfd.resolve(false);
            }

            logger.info("Starting takeover scheduler ..");
            var currentTime = DateTime.now();

            // NOTE: Issue when transitioning between scheduled content and takeover content if the time between layer transitioning is only a few seconds - might be to do with an inability to preload (?)
			// NOTE: Add a minute buffer time to initialise and get takeover events before starting to signal for a new event.
            var bufferTime = new Date(currentTime.getTime() + 1*60000);
            var preloadBufferTime = new Date(currentTime.getTime() + 1*self.takeoverPreloadTimeout); //Make sure they wait 10s before transitioning to next takeover
			// NOTE: Consider here - timing overlap and buffering - do I add this to the start or end time? Also consider takeover>takeover
            self.scheduledTakeoverEvents = self.scheduledTakeoverEvents.filter(function(event) {
				if(initBuffer) {
					return event.startTime.getTime() > bufferTime.getTime();
				} else {
					return event.startTime.getTime() > preloadBufferTime.getTime();
				}
			});
			initBuffer = false;
            if (self.scheduledTakeoverEvents.length > 0) {

                // Suppress any logging issues
                try {
                    // Fix an issue here with next event timer?
                    var nextEventTimeTimeout = (forcePreloadNow !== true) ? self.scheduledTakeoverEvents[0].startTime - currentTime : 0;
                    var preloadTimeTimeout = (nextEventTimeTimeout - self.takeoverPreloadTimeout > self.takeoverPreloadTimeout && forcePreloadNow !== true) ? nextEventTimeTimeout - self.takeoverPreloadTimeout + brightsignSyncDelay: (forcePreloadNow == true) ? 0 + brightsignSyncDelay : self.takeoverPreloadTimeout + brightsignSyncDelay; //minus 3.5s - w/ 2 seconds worked for img/vid but looking at issue with widget //WIP:
                    logger.debug("Preload Timeout: " + preloadTimeTimeout);
                    logger.debug("Scheduling takeover event: Takeover: " + self.scheduledTakeoverEvents[0].takeover.layoutName + " Start: " + self.scheduledTakeoverEvents[0].startTime + " End: " + self.scheduledTakeoverEvents[0].endTime);
                    logger.debug("Scheduling takeover event: Takeover: " + self.scheduledTakeoverEvents[0].takeover.playlistMAs[0].mediaAsset.contentType);
                    logger.debug("Scheduled takeover media assets to play: " + self.scheduledTakeoverEvents[0].takeover.playlistMAs.length);
                    logger.debug("Scheduling takeover - This takeover is going to be: Takeover: " + self.scheduledTakeoverEvents[0].takeover.layoutName + " Start: " + self.scheduledTakeoverEvents[0].startTime + " End: " + self.scheduledTakeoverEvents[0].endTime );
                    if(self.scheduledTakeoverEvents[1]) {
                        logger.debug("Scheduling takeover - Next takeover is going to be: Takeover: " + self.scheduledTakeoverEvents[1].takeover.layoutName + " Start: " + self.scheduledTakeoverEvents[1].startTime + " End: " + self.scheduledTakeoverEvents[1].endTime );
                    }
                } catch (e) {};


                // NOTE: Work on preloading the takeover event before transitioning and playing the content
                self._preloadTimer = setTimeout(function() {
                    self.sendTakeoverEvent({
                        type: 'PRELOAD_TAKEOVER',
                        layoutId: self.scheduledTakeoverEvents[0].takeover.layoutId,
                        index: -1,
						preloadingSecondTakeover: (forcePreloadNow) ? true : false
                    });
                }, preloadTimeTimeout);

                self._takeoverEventTimer = setTimeout((function () {
                    dfd.resolve(true);
                }), nextEventTimeTimeout);
            } else {
                if (self.isMaster) {
                    clearTimeout(retryTimer);
                    var thisHour = new Date(DateTime.now()).getHours();
                    var nextHourDateTime = new Date(DateTime.now().setHours(thisHour + 1, 0, 0, 0));

                    if (lastHourChecked && lastHourChecked == new Date(DateTime.now()).getHours() + 1) {
                        logger.info("We have already checked this slot therefore nothing is valid, so wait until the hour elapses");
                        var nextSlotStartTime = msToNextHour(); //Wait until the full hour slot has elapsed as we have already checked in advance and nothing is valid
                        calculateNextTakeoverSchedule(nextSlotStartTime).then(function(startPlayback) {
                            dfd.resolve(startPlayback);
                        });
                        // Therefore we know nothing is valid and it will then restart this check but fall into the next hours validity period - this is to circumvent an endless loop of checking
                    } else {
                        var nextSlotStartTime = (msToNextHour() > 60000) ? msToNextHour() - 60000 : 0; //Start the next slot scheduler check at XX:59:00 instead of XX:00:00 for on the hour calculations
                        lastHourChecked = thisHour;

                        if (currentHour !== thisHour) {
                            //If we have just entered the next hourly condition then we should attempt to recalculate the slots straight away
                            nextSlotStartTime = 0;
                        }
    
                        calculateNextTakeoverSchedule(nextSlotStartTime, nextHourDateTime).then(function(startPlayback) {
                            dfd.resolve(startPlayback);
                        });
                    }
                }
            }
            return dfd;
        };

        function calculateNextTakeoverSchedule(nextSlotStartTime, nextHourDateTime) {
            var dfd = $.Deferred();
            logger.info("No takeovers available to be scheduled, therefore waiting until the next slot start time in: " + nextSlotStartTime + "ms");
            retryTimer = setTimeout(function () {
                logger.info("Recalculating takeover schedules to be setup ..");
                    lastHourChecked = new Date(DateTime.now()).getHours() + 1;
                    calculateTakeoverSchedules(nextHourDateTime).then(function () {
                        self.startSyncTimer().then(function (startPlayback) {
                            dfd.resolve(startPlayback);
                        });
                    });
            }, nextSlotStartTime);
            return dfd;
        };

        function msToNextHour() {
            return 3600000 - DateTime.now().getTime() % 3600000;
        }

        self.setSyncEvent = function(e) {
            self.syncEvent = e;
        }

        self.getNextTakeover = function() {
            self.currentTakeoverEvent = self.scheduledTakeoverEvents.shift();
            return self.currentTakeoverEvent;
        }

    };

    ScreenTakeoverController.getInstance = function () {
        if (instance === null) {
            instance = new ScreenTakeoverController();
        }
        return instance;
    };

    return ScreenTakeoverController.getInstance();
});
rjs.define('core/SLAPIInterface',["properties/App.Settings", "properties/App.Properties", "system/io/File", "system/io/Directory", "models/ConfigurationModel", "models/ObjectProcessingQueueModel", "models/ConnectivityIntervalSchemeModel", "models/MediaTypeConstants", "system/DateTime", "models/SystemReportModel", "controllers/UserMessageController", "models/RssChannelCache", "models/RssChannelModel", "controllers/RealTimeEventsController", "models/DeviceInfoModel","core/SLAPIMethodsInterface","models/API","system/network/NetworkInterface", "core/DSSAPIInterface", "system/plugins/DownloadPlugin", "controllers/FirmwareUpdateController", "controllers/ScreenTakeoverController"], function(Settings, Properties, File, Directory, ConfigurationModel, ObjectProcessingQueueModel, ConnectivityIntervalSchemeModel, MediaTypeConstants, DateTime, SystemReportModel, UserMessageController, RssChannelCache, RssChannel, RealTimeEventsController, DeviceInfoModel, APIMethodsInterface, API, NetworkInterface, DSSAPIInterface, DownloadPlugin, FirmwareUpdateController, ScreenTakeoverController) {
    /*
    	SLAPIInterface - Provides an interface to communicate with the SL API
    	------------------------------------------------------------------------------
    	This module has been updated so that the authentication is now async rather than synchronous
     */
    function SLAPIInterface(apiEndpoint, apiCreds) {
        var self = this;

        // call back methods for registration and configuration
        var _registrationCallback;
        var _configurationCallback;
        var _alreadyRegisteredCallback;
        var _configurationNoChangeCallback;

        // default connection settings
        var _supportCheckFailureRetryIntervalMS = 10000;
        var _supportCheckUnsupportedRetryIntervalMS = 60000;
        var _registrationFailureRetryIntervalMS = 5000;
        var _authenticationFailureRetryIntervalMS = 5000;

        var _defaultConfigurationCheckIntervalMS = 60000;
        // 60 seconds
        var _defaultHealthCheckIntervalSecondMS = 60000;
        var _lastHealthCheckIntervalMS = _defaultHealthCheckIntervalSecondMS;

        // default to one hour
        var _defaultSystemReportIntervalMS = 3600000;

        var _registrationTimerId = 0;
        var _authenticationTimerId = 0;
        var _configurationTimerId = 0;
        var _healthCheckTimerId = 0;
        var _systemReportTimerId = 0;
        var _supportCheckTimerId = 0;

        self.lastConnectionTime = null;

        var _APIMethodsInterface = new APIMethodsInterface();
        var _screenTakeoverController = ScreenTakeoverController;

        var _lastConfigurationModifiedDate = new Date(1970, 0, 1);
        var _lastConfigurationResponse;

        // don't permit configuration checks when content is being downloaded
        var _contentDownloadInProgress = false;

        var _externalAssets = [];

        var _APIMethodsInterface = new APIMethodsInterface();

          //
        // checkDeviceSupport()
        //
        function checkDeviceSupport() {
            logger.info('Checking API for device support');

            UserMessageController.postMessage('Confirming device support');

            var devInfo = new DeviceInfoModel();

            devInfo.generate().done(function()
            {
                var jqXHR = executeAPICall('device', {
                    type: 'POST',
                    contentType: 'application/json',
                    data: devInfo,
                    beforeSend: function(xhr, settings) {
                        xhr.setRequestHeader('X-SIGNAGELIVE-APP-ID', Settings.application.api.appId);
                        xhr.setRequestHeader('X-SIGNAGELIVE-CLIENT-ID', Settings.application.api.clientId);
                    }
                });

                if (jqXHR != null) {
                    jqXHR.done(function(data, textStatus, jqXHR) {
                        self.lastConnectionTime = new Date();
                        switch(data.result)
                        {
                            case 1:
                                //Supported
                                register();
                                break;
                            case 2:
                                UserMessageController.postMessage(data.message);

                                if(data.data && data.data != '')
                                {
                                    fwUpdate = new FirmwareUpdateController();

                                    fwUpdate.downloadAndInstall(data.data).done(function()
                                    {
                                        //Not much to do here really as we will reboot in 10 seconds...
                                    }).fail(function(){
                                        //Could not download, retry later
                                        _supportCheckTimerId = setTimeout(checkDeviceSupport, _supportCheckUnsupportedRetryIntervalMS);
                                    }                                    
                                    );
                                } else {
                                    _supportCheckTimerId = setTimeout(checkDeviceSupport, _supportCheckUnsupportedRetryIntervalMS);
                                }                                
                                break;
                            case 3:
                                UserMessageController.postMessage("A client update is required. This will happen automatically.");
                                _supportCheckTimerId = setTimeout(checkDeviceSupport, _supportCheckUnsupportedRetryIntervalMS);
                                break;
                            case 0:
                                UserMessageController.postMessage('This device is not supported');
                                _supportCheckTimerId = setTimeout(checkDeviceSupport, _supportCheckUnsupportedRetryIntervalMS);
                            default:
                                break;
                        }
    
                    }).fail(function(jqXHR, textStatus, errorThrown) {
                        logger.error('Error connecting with Signagelive. Retrying...');
                    //_supportCheckUnsupportedRetryIntervalMS
                        // call the support method again
                        _supportCheckTimerId = setTimeout(checkDeviceSupport, _supportCheckFailureRetryIntervalMS);
                    });
                } else {
                    logger.error('Error connecting with Signagelive. Retrying...');
                    
                    // call the support method again
                    _supportCheckTimerId = setTimeout(checkDeviceSupport, _supportCheckFailureRetryIntervalMS);
                }

            });

        }

        //
        // register()
        //
        function register() {
            logger.info('Register with Signagelive API ' + Settings.application.api.clientId);

            UserMessageController.postMessage('Registering with Signagelive');

            var jqXHR = executeAPICall('register', {
                type: 'POST'
            });

            if (jqXHR != null) {
                jqXHR.done(function(data, textStatus, jqXHR) {
                    self.lastConnectionTime = new Date();
                    // set the client token
                    Settings.application.api.clientKey = data.client_key;

                    // execute the callback
                    if (_registrationCallback != undefined & _registrationCallback != null)
                        _registrationCallback(data.client_key);

                    // start the main configuration process
                    configuration();
                    postSystemReport();

                }).fail(function(jqXHR, textStatus, errorThrown) {

                    // conflict - this will indicate that the mac address is already registered
                    if (jqXHR.status == 409) {
                        _alreadyRegisteredCallback();
                    }

                    // log the error
                    logAPIError(this.url, this.data, jqXHR.status, errorThrown, jqXHR.responseText);

                    // call the registration method again
                    _registrationTimerId = setTimeout(register, _registrationFailureRetryIntervalMS);
                });
            } else {

                // Log error
                logger.error('Error registing with Signagelive. Retrying...');

                // call the registration method again
                _registrationTimerId = setTimeout(register, _registrationFailureRetryIntervalMS);
            }
        }

        function authenticate() {
            return _APIMethodsInterface.authenticate(API.PLAYER_API);
        }

        function configuration() {
            if (_contentDownloadInProgress) {
                // reset the configuration timer for the default period before trying again
                _configurationTimerId = setTimeout(configuration, _defaultConfigurationCheckIntervalMS);
                return;
            }

            // does the client need to authenticate again?
            if (isAuthenticationRequired()) {

                // Need to authenticate asynchronously
                authenticate().done(function(authenticated) {

                    // Client is not authenticated try again soon
                    if (!authenticated) {
                        setConfigurationCheckTimer();
                        return;
                    } else {
                        // Client is now authenticated - check for config update
                        checkForConfigurationUpdate();
                    }
                });
            } else {
                // Client is already authenticated - check for config update
                checkForConfigurationUpdate();
            }
        }

        //
        // checkForConfigurationUpdate()
        //
        function checkForConfigurationUpdate() {

            try {
                logger.info('Checking for a configuration update');

                // check to see if there is an update?
                var jqXHR = $.ajax({
                    url: Settings.application.api.url + "/" + 'configuration',
                    type: 'HEAD',
                    contentType: 'application/json',
                    timeout: 60000,
                    beforeSend: function(xhr, settings) {
                        xhr.setRequestHeader('X-SIGNAGELIVE-APP-ID', Settings.application.api.appId);
                        xhr.setRequestHeader('X-SIGNAGELIVE-APP-KEY', Settings.application.api.appKey);
                        xhr.setRequestHeader('X-SIGNAGELIVE-CLIENT-ID', Settings.application.api.clientId);
                        xhr.setRequestHeader('X-SIGNAGELIVE-CLIENT-KEY', Settings.application.api.clientKey);
                        xhr.setRequestHeader('X-SIGNAGELIVE-AUTH-TOKEN', Settings.application.api.authToken);
                        if (Settings.application.api.brand !== 'Signagelive') {
                            xhr.setRequestHeader('X-SIGNAGELIVE-INSTALL-ID', Settings.application.api.brand);
                        }
                    }
                });

                jqXHR.done(function(data, textStatus, jqXHR) {
                    self.lastConnectionTime = new Date();
                    // check the last modified date to see if there is an update
                    var lastModifiedDate = new Date(jqXHR.getResponseHeader('Last-Modified'));

                    if (lastModifiedDate.getTime() != _lastConfigurationModifiedDate.getTime()) {
                        var getConfigurationPromise = getConfiguration();

                        getConfigurationPromise.done(function() {
                            // remember the date of the last configuration response.
                            _lastConfigurationModifiedDate = lastModifiedDate;
                        }).fail(function() {

                        }).always(function() {
                            // reset the configuration timer
                            setConfigurationCheckTimer();
                        });
                    } else {
                        // No Change
                        if (_configurationNoChangeCallback != null || _configurationNoChangeCallback != undefined)
                            _configurationNoChangeCallback();

                        if (Settings.application.features.canDownloadFiles) {
                            // sync the external media assets
                            synchronizeExternalMediaAssets().always(function() {
                                // reset the configuration timer
                                setConfigurationCheckTimer();
                            });
                        } else {
                            // reset the configuration timer
                            setConfigurationCheckTimer();
                        }
                    }

                }).fail(function(jqXHR, textStatus, errorThrown) {

                    // reset auth token if it is not valid
                    if (jqXHR.status == 401) {
                        Settings.application.api.authTokenExpiry = null;

                        // try the configuration again in 30 seconds
                        _configurationTimerId = setTimeout(configuration, 30000);
                    } else {
                        // reset the configuration timer
                        setConfigurationCheckTimer();
                    }
                });
            } catch (e) {
                logger.error('Exception trying to perform content check.');

                // reset the configuration timer
                setConfigurationCheckTimer();
            }
        }

        //
        // synchronizeExternalMediaAssets()
        //
        function synchronizeExternalMediaAssets() {
            logger.debug('Sync External Media Assets');
            var extenalSyncDeferred = $.Deferred();

            // sync the externally available assets
            // set-up the asset download queue (for this playlist)
            var assetQueue = new ObjectProcessingQueueModel();
            $(_externalAssets).each(function(index, mediaAsset) {
                assetQueue.push(mediaAsset);
            });

            // download the queue
            downloadMediaAssetQueue(assetQueue, false).done(function() {
                extenalSyncDeferred.resolve();
            }).fail(function() {
                extenalSyncDeferred.reject();
            });

            return extenalSyncDeferred;
        }

        //
        // getConfiguration()
        //
        function getConfiguration() {

            var dfd = $.Deferred();

            // does the client need to authenticate again?
            if (isAuthenticationRequired()) {

                authenticate().done(function(authenticated) {

                    // Client is not authenticated try again soon
                    if (!authenticated) {
                        dfd.reject();
                        return;
                    } else {
                        // Authenticated get new config
                        getUpdatedConfiguration()
                            .done(function() {
                                dfd.resolve();
                            })
                            .fail(function() {
                                dfd.reject();
                            });
                    }
                });
            } else {
                // Authenticated - get new config
                getUpdatedConfiguration()
                    .done(function() {
                        dfd.resolve();
                    })
                    .fail(function() {
                        dfd.reject();
                    });
            }

            return dfd;
        }

        //
        // getUpdatedConfiguration()
        //
        function getUpdatedConfiguration() {

            // Deffered object to use
            var getConfigurationDeferred = $.Deferred();

            UserMessageController.postMessage('Getting player configuration');

            var getConfigurationjqXHR = executeAPICall('configuration');

            var configurationJsonData = '';

            getConfigurationjqXHR.done(function(data, textStatus, jqXHR) {
                // remember the configuration response - we will need this once the configuration response has been
                // successfully processed
                //logger.debug("DOWNLOADED CONFIG: " + JSON.stringify(data));

                _lastConfigurationResponse = new ConfigurationModel(data);

                // remember the json response so that it can be saved to disk when required.
                configurationJsonData = data;

                // has the health check interval been updated?
                if (_lastConfigurationResponse.connectivity.healthCheckInterval != _lastHealthCheckIntervalMS) {
                    logger.debug('Setting health check interval to ' + _lastConfigurationResponse.connectivity.healthCheckInterval);
                    // the health check interval has been updated - remember the new interval and reset the process
                    _lastHealthCheckIntervalMS = _lastConfigurationResponse.connectivity.healthCheckInterval;
                    clearTimeout(_healthCheckTimerId);

                    healthCheck();
                }

                // Notify Real Time Event Controller of any changes
                RealTimeEventsController.updateConfiguration(_lastConfigurationResponse);

                // process any content updates.
                downloadContent(_lastConfigurationResponse.content)
                    .then(function() {
                        logger.info('All content downloaded.');
                        // If screen takeovers is running then we need to stop the controller here incase the takeovers are being updated and to prevent scheduling conflict when the media player restarts
                        // NOTE: Amend this here so that we dont fully stop the takeover controller everytime and instead we just temporarily pause it unless the configuration has removed its takeoverConfig? Otherwise we are continually disabling and reinstaniating the controller ..
                        if(Settings.application.features.canTakeoverContent) {
                            if(_screenTakeoverController.running == true) {
                                _screenTakeoverController.stop();
                            }
                            // Reset the takeover group details just in case the MSC needs to stop()
                            if(!_lastConfigurationResponse.takeoverGroup) {
                                // NOTE: If no takeover group config then we need to make sure its updated so the MSC stops when the media player stops to then load the new content, otherwise this will get updated on the STC start() process
                                _screenTakeoverController._takeoverGroupConfig = null;
                            }
						}

                        // If we have a config that includes data sync services config then start syncing
                        if (_lastConfigurationResponse && _lastConfigurationResponse.dataSyncServices && _lastConfigurationResponse.content.integrations && _lastConfigurationResponse.content.integrations.length > 0) {
                            logger.info('Restarting data sync services sync...');
                            return DSSAPIInterface.synchroniseIntegrationsForMediaAssets(_lastConfigurationResponse.dataSyncServices, _lastConfigurationResponse.content.integrations, _lastConfigurationResponse.content.mediaAssets);
                        }
                        else {
                            logger.info('No last config or data sync services config object in configuration. Skipping DSS synchronisation...');
                            return $.Deferred().resolve();
                        }
                    })
                    .then(function() {
                        logger.info('Data Sync Services Synchronisation complete. Download Process Completed');
                        // execute the configuration complete callback
                        if (_configurationCallback != null || _configurationCallback != undefined)
                            _configurationCallback(_lastConfigurationResponse);
                        // save the configuration to disk
                        try {
                            logger.debug('Saving configuration to storage');
                            // save the file to storage
                            var configString = JSON.stringify(configurationJsonData);
                            File.write(Settings.application.fileSystem.systemFiles.lastConfigFilePath, configString);
                        } catch (e) {
                            logger.error('Error whilst saving configuration file: ' + e.message);
                        }
                        // get the assets that are available externally (e.g. RSS feeds) as SL will not know that
                        // these have been updated.
                        _externalAssets = $.grep(_lastConfigurationResponse.content.mediaAssets, function(mediaAsset, index) {
                            return mediaAsset.isExternallyDelivered;
                        });
                        getConfigurationDeferred.resolve();
                    }).catch(function() {
                        logger.error('Download Process Failed');
                        getConfigurationDeferred.reject();
                    }).always(function() {
                        // always reset the download flag
                        _contentDownloadInProgress = false;
                    });         

            }).fail(function(jqXHR, textStatus, errorThrown) {
                // log the error
                logAPIError(this.url, this.data, jqXHR.status, errorThrown, jqXHR.responseText);

                getConfigurationDeferred.reject();

            });

            return getConfigurationDeferred.promise();
        }

        //
        // setConfigurationCheckTimer()
        //
        function setConfigurationCheckTimer() {
            // calculate when the next configuration check should occur based on settings in the configuration response.
            var interval = calculateNextConfigurationCheckInterval();

            // reset the configuration timer
            _configurationTimerId = setTimeout(configuration, interval);
        }

        //
        // calculateNextConfigurationCheckInterval()
        //
        function calculateNextConfigurationCheckInterval() {
            var interval = _defaultConfigurationCheckIntervalMS;

            if (_lastConfigurationResponse != null) {
                if (_lastConfigurationResponse.connectivity.configurationIntervalScheme == ConnectivityIntervalSchemeModel.REGULAR_INTERVAL) {
                    // set the configuration check interval to the one in the response
                    interval = _lastConfigurationResponse.connectivity.configurationCheckInterval;
                } else {
                    // calculate how long until the next time of day configuration check
                    var nextCheckInTime = DateTime.now().setTime(_lastConfigurationResponse.connectivity.configurationCheckTime);
                    if (nextCheckInTime < DateTime.now())
                        nextCheckInTime = nextCheckInTime.addDays(1);

                    interval = nextCheckInTime.subtract(DateTime.now());
                }
            }

            return interval;
        }

        //
        // calculateNextHealthCheckInterval()
        //
        function calculateNextHealthCheckInterval() {
            var interval = _defaultHealthCheckIntervalSecondMS;

            if (_lastConfigurationResponse != null) {
                // set the health check interval to the one in the response
                interval = _lastConfigurationResponse.connectivity.healthCheckInterval;
            }

            logger.debug('Health Check interval is : ' + interval);

            return interval;
        }

        //
        // downloadContent(content)
        //
        function downloadContent(content) {
            UserMessageController.postMessage('Downloading content');

            // prevent any content checks whilst content is being downloaded
            _contentDownloadInProgress = true;

            var updateSuccess = false;

            var dfd = $.Deferred();

            if (Settings.application.features.canDownloadFiles) {

                var tempDirClearDfd = $.Deferred();
                //Clear the temp directory
                Directory.listFiles(Settings.application.fileSystem.root + "/" + Settings.application.fileSystem.tempDirectory).done(function(files)
                {		
                    $.each(files, function( index, filename ) {
                        var filePath = Settings.application.fileSystem.root + "/" + Settings.application.fileSystem.tempDirectory + "/" + filename;
                        if(File.exists(filePath))
                            File.delete(filePath);
                        
                    });    

                    logger.debug("Temp directory cleared");
                    tempDirClearDfd.resolve();
                });

                tempDirClearDfd.done(function()
                {

                    // get the layouts that require a download
                    var layoutsToDownload = getLayoutsRequiringDownload(content);

                    // add the layouts to a queue
                    var layoutQueue = new ObjectProcessingQueueModel(layoutsToDownload);

                    // process the layout download queue
                    downloadLayoutQueue(layoutQueue).done(function(processedItems) {
                        logger.info('Layouts Downloaded');

                        updateSuccess = true;

                    }).fail(function(processedItems, failedItems) {
                        logger.error('Layout Download Queue Failed');

                        updateSuccess = false;
                    }).always(function() {
                        // after layouts have been processed process the default playlist
                        if (content.defaultPlaylist != null) {
                            logger.info('Downloading Default Playlist');

                            // add the playlist to a queue - so we can use promises
                            var playlistQueue = new ObjectProcessingQueueModel();
                            playlistQueue.items.push(content.defaultPlaylist);

                            // process the queue
                            downloadPlaylistQueue(playlistQueue).done(function(processedItems) {
                                logger.debug('Default Playlist Downloaded');

                                updateSuccess = updateSuccess & true;
                            }).fail(function(processedItems, failedItems) {
                                logger.error('Default Playlist Download Failed');

                                updateSuccess = false;
                            }).always(function() {
                                // return the overall success of the entire done load
                                if (updateSuccess)
                                    dfd.resolve();
                                else
                                    dfd.reject();
                            });
                        } else {
                            if (updateSuccess) {
                                dfd.resolve();
                            } else {
                                dfd.reject();
                            }
                        }
                    });
                });

            } else {
                // can't download - just fake it!
                setTimeout(function() {
                    dfd.resolve();
                }, 500);
            }

            return dfd.promise();
        }

        //
        // getLoyoutsRequiringDownlood(layouts)
        //
        function getLayoutsRequiringDownload(content) {
            // TODO - are there any layouts that require a download in the new configuration?
            // are there layouts where the content is not available locally...

            var layoutsRequiringDownload = [];

            $(content.schedules).each(function(i, schedule) {
                var addToSchedule = false;
                if (schedule.layout.isDownloaded == false) {
                    // find the media asset
                    var layouts = $.grep(layoutsRequiringDownload, function(layout, index) {
                        return layout.externalId == schedule.layout.externalId;
                    });

                    if (layouts.length == 0)
                        addToSchedule = true;
                } else {
                    // TODO - remove and enhance to check layout design, playlists etc
                    // find the media asset
                    var layouts = $.grep(layoutsRequiringDownload, function(layout, index) {
                        return layout.externalId == schedule.layout.externalId;
                    });
                    if (layouts.length == 0)
                        addToSchedule = true;
                }

                if (addToSchedule) {
                    layoutsRequiringDownload.push(schedule.layout);
                }
            });

            // Add interrupts
            $(content.interrupts).each(function(i, interrupt) {
                var addToSchedule = false;
                // find the media asset
                var layouts = $.grep(layoutsRequiringDownload, function(layout, index) {
                    return layout.externalId == interrupt.action.layout.externalId;
                });
                if (layouts.length == 0) {
                    layoutsRequiringDownload.push(interrupt.action.layout);
                }
            });

            // Add takeovers
            $(content.takeovers).each(function(i, takeover) {
                var addToSchedule = false;
                // find the media asset
                var layouts = $.grep(layoutsRequiringDownload, function(layout, index) {
                    return layout.externalId == takeover.layoutId;
                });
                if (layouts.length == 0) {
                    layoutsRequiringDownload.push(takeover.layout);
                }
            });

            return layoutsRequiringDownload;
        }

        //
        // downloadLayoutQueue(layoutQueue)
        //
        function downloadLayoutQueue(layoutQueue) {
            var queueDFD = $.Deferred();
            downloadNextLayoutInQueue(layoutQueue, queueDFD);
            return queueDFD.promise();
        }

        //
        // downloadNextLayoutInQueue(layoutQueue, queueDfd)
        //
        function downloadNextLayoutInQueue(layoutQueue, queueDfd) {
            // is the queue empty - return the success of the queue processing?
            if (layoutQueue.items.length == 0) {
                if (layoutQueue.failedItems.length == 0)
                    queueDfd.resolve(layoutQueue.processedItems);
                else
                    queueDfd.reject(layoutQueue.processedItems, layoutQueue.failedItems);
                return;
            }

            var layout = layoutQueue.currentItem();

            logger.info('Layout Download Start ' + layout.name);

            // notify the SL API the download is starting
            self.notifyDownloadStart(layout.externalId);

            // setup the playlist and asset download queues
            var playlistQueue = new ObjectProcessingQueueModel();

            // the assets that will be downloaded are misc objects
            // such as default zone assets, backgrounds etc
            var assetQueue = new ObjectProcessingQueueModel();

            if (layout.backgroundAsset != null) {
                assetQueue.push(layout.backgroundAsset);
            }

            // setup the queues
            $(layout.zones).each(function(index, zone) {
                if (zone.backgroundAsset != null)
                    assetQueue.push(zone.backgroundAsset);

                if (zone.seperatorAsset != null)
                    assetQueue.push(zone.seperatorAsset);

                if (zone.defaultMediaAsset != null)
                    assetQueue.push(zone.defaultMediaAsset);

                if (zone.defaultPlaylist != null)
                    playlistQueue.push(zone.defaultPlaylist);

                // add all of the scheduled playlists to the queue
                $(zone.scheduledPlaylists).each(function(index, scheduledPlaylist) {
                    // check the asset is not already in the queue
                    var matchingItemsInQueue = $.grep(playlistQueue.items, function(playlist, index) {
                        return playlist.externalId == scheduledPlaylist.playlist.externalId;
                    }).length;

                    if (matchingItemsInQueue == 0)
                        playlistQueue.push(scheduledPlaylist.playlist);
                });
            });

            // process the playlist queue
            downloadPlaylistQueue(playlistQueue).done(function(processedItems) {
                var layout = layoutQueue.shift();
                layoutQueue.processedItems.push(layout);

                logger.info('Layout Download Complete ' + layout.name);

                // notify the SL API the download is complete
                self.notifyDownloadComplete(layout.externalId);

            }).fail(function(processedItems, failedItems) {
                var layout = layoutQueue.shift();
                layoutQueue.failedItems.push(layout);

                logger.error('Layout Download Failed ' + layout.name);

                // notify the SL API the download has failed
                self.notifyDownloadFail(layout.externalId);

            }).always(function() {
                // Download all layout media assets
                downloadMediaAssetQueue(assetQueue, true).done(function() {
                    // Nothing we need to do here
                    logger.debug('Layout Media Assets Downloaded');
                }).fail(function(processedItems, failedItems) {
                    var layout = layoutQueue.shift();
                    layoutQueue.failedItems.push(layout);
                    logger.error('Layout has failed asset downloads. Layout Download Failed: ' + layout.name);
                    // notify the SL API the download has failed
                    notifyDownloadFail(layout.externalId);
                }).always(function() {
                    // process the next item in the queue
                    setTimeout(function() {
                        downloadNextLayoutInQueue(layoutQueue, queueDfd);
                    }, 500);
                });
            });
        }

        //
        // downloadPlaylistQueue(playlistQueue)
        //
        function downloadPlaylistQueue(playlistQueue) {
            var queueDFD = $.Deferred();
            downloadNextPlaylistInQueue(playlistQueue, queueDFD);
            return queueDFD.promise();
        }

        //
        // downloadNextPlaylistInQueue(playlistQueue, queueDfd)
        //
        function downloadNextPlaylistInQueue(playlistQueue, queueDfd) {
            // is the queue empty - return the success of the queue processing
            if (playlistQueue.items.length == 0) {
                if (playlistQueue.failedItems.length == 0)
                    queueDfd.resolve(playlistQueue.processedItems);
                else
                    queueDfd.reject(playlistQueue.processedItems, playlistQueue.failedItems);
                return;
            }

            // process the next item
            var playlist = playlistQueue.currentItem();
            logger.info('Playlist Download Start ' + playlist.name);

            // notify the SL API the download is starting
            self.notifyDownloadStart(playlist.externalId);

            // set-up the asset download queue (for this playlist)
            var assetQueue = new ObjectProcessingQueueModel();
            $(playlist.playlistMediaAssets).each(function(index, playlistMediaAsset) {

                //If this is a feed, add the assets to the queue
                if (playlistMediaAsset.mediaAsset.contentType == MediaTypeConstants.FEED) {
                    $(playlistMediaAsset.feed.feedMediaAssets).each(function(index, feedMediaAsset) {
                        var matchingItemsInQueue = $.grep(assetQueue.items, function(mediaAsset, index) {
                            return mediaAsset.externalId == feedMediaAsset.mediaAssetId;
                        }).length;

                        if (matchingItemsInQueue == 0) {
                            // check the file is not already downloaded
                            assetQueue.push(feedMediaAsset.mediaAsset);
                        }
                    });
                } else {
                    // check the asset is not already in the queue
                    var matchingItemsInQueue = $.grep(assetQueue.items, function(mediaAsset, index) {
                        return mediaAsset.externalId == playlistMediaAsset.mediaAssetId;
                    }).length;

                    if (matchingItemsInQueue == 0) {
                        // check the file is not already downloaded
                        assetQueue.push(playlistMediaAsset.mediaAsset);
                    }
                }
            });

            logger.debug("Items in queue: " + assetQueue.items.length);

            // process the media asset queue
            downloadMediaAssetQueue(assetQueue, true).done(function(downloadedAssets) {
                // add the playlist to the successful items collection
                var playlist = playlistQueue.items.shift();
                playlistQueue.processedItems.push(playlist);

                logger.info('Playlist Download Complete ' + playlist.name);

                // notify the SL API the download is complete
                self.notifyDownloadComplete(playlist.externalId);

            }).fail(function(downloadedAssets, failedAssets) {
                // add the playlist to the successful items collection
                var playlist = playlistQueue.items.shift();
                playlistQueue.failedItems.push(playlist);

                logger.error('Playlist Download Fail ' + playlist.name + ' ' + failedAssets.length + ' assets have failed to download');

                // notify the SL API the download has failed
                self.notifyDownloadFail(playlist.externalId);

            }).always(function() {
                // process the next item in the queue
                setTimeout(function() {
                    downloadNextPlaylistInQueue(playlistQueue, queueDfd);
                }, 500);
            });
        }

        //
        // downloadMediaAssetQueue(assetQueue)
        //
        function downloadMediaAssetQueue(assetQueue, withDownloadNotifications) {
            var queueDFD = $.Deferred();
            downloadNextMediaAssetInQueue(assetQueue, queueDFD, withDownloadNotifications);
            return queueDFD.promise();
        }

        //
        // downloadNextMediaAssetInQueue(assetQueue, queueDFD)
        //
        function downloadNextMediaAssetInQueue(assetQueue, queueDFD, withDownloadNotifications) {
            // is the queue empty - return the success of the queue processing
            if (assetQueue.items.length == 0) {
                if (assetQueue.failedItems.length == 0)
                    queueDFD.resolve(assetQueue.processedItems);
                else
                    queueDFD.reject(assetQueue.processedItems, assetQueue.failedItems);
                return;
            }

            // does the client need to authenticate again?
            if (isAuthenticationRequired()) {

                authenticate().done(function(authenticated) {
                    if (!authenticated) {
                        // Auth failed
                        queueDFD.reject(assetQueue.processedItems, assetQueue.failedItems);
                    } else {
                        // Authenticated now - do download
                        doDownloadNextMediaAssetInQueue(assetQueue, queueDFD, withDownloadNotifications);
                    }
                });
            } else {
                // Already authenticated - do download
                doDownloadNextMediaAssetInQueue(assetQueue, queueDFD, withDownloadNotifications);
            }
        }

        /**
         * Attempts to download the next media asset in the download queue
         * @param  {[type]} assetQueue                [description]
         * @param  {[type]} queueDFD                  [description]
         * @param  {[type]} withDownloadNotifications [description]
         * @return {[type]}                           [description]
         */
        function doDownloadNextMediaAssetInQueue(assetQueue, queueDFD, withDownloadNotifications) {
            // process the next item
            var asset = assetQueue.currentItem();

            logger.info('Checking if asset requires a download');

            // does the media asset require a download?
            var requiresDownload = doesAssetRequireDownload(asset);
            if (requiresDownload == false) {
                // remove this item from the queue
                // add the asset to the successful items collection
                var asset = assetQueue.items.shift();
                assetQueue.processedItems.push(asset);

                // process the next item in the queue and ignore this one (as it either is already downloaded or is not downloadable)
                setTimeout(function() {
                    downloadNextMediaAssetInQueue(assetQueue, queueDFD, withDownloadNotifications);
                }, 500);
                return;
            }

            logger.info('Media Asset Download Start ' + asset.name);

            // notify the SL API the download has started
            if (withDownloadNotifications)
                self.notifyDownloadStart(asset.externalId);

            // initialize the downloader
            var downloadPlugin = new DownloadPlugin();

            // SL Media Asset Downloads on the SSSP must append the authentication values to the url
            // as the FileDownloadPlugin does not support custom headers

            // Add Query String Params if the request is for content from the SL API
            var authString = '';
            if (asset.remoteUrl.lastIndexOf(Settings.application.api.url, 0) === 0)
            {
                authString = "?sl_app_id=" + Settings.application.api.appId + "&sl_app_key=" + Settings.application.api.appKey + "&sl_client_id=" + Settings.application.api.clientId + "&sl_client_key=" + Settings.application.api.clientKey + "&sl_auth_token=" + encodeURIComponent(Settings.application.api.authToken);
                if (Settings.application.api.brand !== 'Signagelive') {
                    authString += "&sl_install_id=" + Settings.application.api.brand;
                }
            }
                

            var remoteUrl = asset.remoteUrl + authString;

            // Build Local File Url
            var localFile = "";
            var destinationFile="";
            if (Settings.application.fileSystem.root !== "") {
                localFile = localFile + Settings.application.fileSystem.root + "/";
                destinationFile = destinationFile + Settings.application.fileSystem.root + "/";
            }
            if (Settings.application.fileSystem.mediaDirectory !== "") {
                destinationFile = destinationFile + Settings.application.fileSystem.mediaDirectory + '/';
                localFile = localFile + Settings.application.fileSystem.tempDirectory + '/';
            }
            localFile = localFile + asset.localFileName;
            destinationFile = destinationFile + asset.localFileName;

            logger.debug('Local file name: ' + localFile);

            // Start download
            downloadPlugin.downloadFile(remoteUrl, localFile, asset.contentType)
                // On Download Success
                .done(function(_remoteUrl, _localFile) {

                    if (asset.contentType == MediaTypeConstants.RSS) 
                    {
                        File.move(_localFile,destinationFile).done(function()
                        {
                            // Add to channel cache then add to process items and notify of success	
                            RssChannelCache.loadRssAssetIntoCache(asset);
                            handleAssetDownloadSuccess(assetQueue, withDownloadNotifications).then(function() {
                                downloadNextMediaAssetInQueue(assetQueue, queueDFD, withDownloadNotifications);
                            });
                            
                        });

                    } else {

                        var checkDfd = $.Deferred();

                        checkDfd.done(function () {
                            // Default case - nothing to do so just handle success
                            File.move(_localFile, destinationFile).done(function () {
                                // Handle RSS ContentType
                                if (asset.contentType == MediaTypeConstants.RSS) {
                                    // Add to channel cache then add to process items and notify of success	
                                    RssChannelCache.loadRssAssetIntoCache(asset);
                                    handleAssetDownloadSuccess(assetQueue, withDownloadNotifications).then(function () {
                                        downloadNextMediaAssetInQueue(assetQueue, queueDFD, withDownloadNotifications);
                                    });
                                }
                                // Handle Widget ContentType
                                else if (asset.contentType == MediaTypeConstants.WIDGET) {
                                    // Need to extract the widget and handle failures appropriately
                                    logger.info("Unzipping widget: " + asset.localFileName);

                                    // Get widget folder
                                    var widgetDestination = "";
                                    if (Settings.application.fileSystem.root !== "") {
                                        widgetDestination = widgetDestination + Settings.application.fileSystem.root + "/";
                                    }
                                    if (Settings.application.fileSystem.widgetDirectory !== "") {
                                        widgetDestination = widgetDestination + Settings.application.fileSystem.widgetDirectory + '/';
                                    }
                                    widgetDestination = widgetDestination + asset.localFileName.replace('.wgt', '');

                                    logger.debug("Creating new widget directory: " + widgetDestination);

                                    // Create new directory - it will not exist at this point
                                    Directory.createDirectory(widgetDestination)
                                        // On directory creation success
                                        .done(function () {
                                            // Extract Archive
                                            logger.debug("New Directory created. Unzipping " + _localFile + " to destination " + widgetDestination);

                                            File.unzip(destinationFile, widgetDestination)
                                                // On unzip success
                                                .done(function () {
                                                    logger.debug("Successfully extracted widget file: " + destinationFile);
                                                    handleAssetDownloadSuccess(assetQueue, withDownloadNotifications).then(function () {
                                                        downloadNextMediaAssetInQueue(assetQueue, queueDFD, withDownloadNotifications);
                                                    });
                                                })
                                                // On unzip failure
                                                .fail(function () {
                                                    logger.error("Failed to upzip widget package");
                                                    handleAssetDownloadFailure(destinationFile, assetQueue, withDownloadNotifications).then(function () {
                                                        downloadNextMediaAssetInQueue(assetQueue, queueDFD, withDownloadNotifications);
                                                    });
                                                });
                                        })
                                        // On Directory creation failure
                                        .fail(function () {
                                            handleAssetDownloadFailure(destinationFile, assetQueue, withDownloadNotifications).then(function () {
                                                downloadNextMediaAssetInQueue(assetQueue, queueDFD, withDownloadNotifications);
                                            });
                                        });
                                }
                                // Handle All Other ContentTypes
                                else {

                                    handleAssetDownloadSuccess(assetQueue, withDownloadNotifications).then(function () {
                                        downloadNextMediaAssetInQueue(assetQueue, queueDFD, withDownloadNotifications);
                                    });

                                }

                            }).fail(function () {
                                logger.error("Fail moving file from temp to media directory");
                                handleAssetDownloadFailure(_localFile, assetQueue, withDownloadNotifications).then(function () {
                                    downloadNextMediaAssetInQueue(assetQueue, queueDFD, withDownloadNotifications);
                                });
                            });
                        }).fail(function(){
                            handleAssetDownloadFailure(_localFile, assetQueue, withDownloadNotifications).then(function () {
                                downloadNextMediaAssetInQueue(assetQueue, queueDFD, withDownloadNotifications);
                            });
                        });

                        if(asset.hash && asset.hashType == "CRC32" && Settings.application.features.supportsHashCRC32)
                        {
                            File.crc32(_localFile).done(function(crc32){

                                logger.info("CRC for downloaded file: " + crc32);
    
                                if(asset.hash == crc32)
                                {
                                    logger.info("CRC match - download success ");
                                    checkDfd.resolve();                                
                                } else {
                                    logger.error("CRC did not match - asset download failure");
                                    checkDfd.reject();
                                }
                             
                            });
                        } else {
                            logger.info("Hash check not supported");
                            //CRC32 not supported, skip
                            checkDfd.resolve();  
                        }      
                    }        
                   
                })
                // On Download Failure
                .fail(function(_remoteUrl, _localFile) {
                    handleAssetDownloadFailure(_localFile, assetQueue, withDownloadNotifications).then(function() {
                        downloadNextMediaAssetInQueue(assetQueue, queueDFD, withDownloadNotifications);
                    });
                });
        }

        /**
         * Updates queue and sends notification on download success
         * @param  {[type]} assetQueue                [description]
         * @param  {[type]} withDownloadNotifications [description]
         * @return {[type]}                           [description]
         */
        function handleAssetDownloadSuccess(assetQueue, withDownloadNotifications) {
            var dfd = $.Deferred();
            // add the asset to the successful items collection
            var asset = assetQueue.items.shift();
            assetQueue.processedItems.push(asset);

            // Log Successfull download
            logger.info('Media Asset Download Complete ' + asset.name);

            // notify the SL API the download is complete
            if (withDownloadNotifications) {

                RealTimeEventsController.sendMediaAssetDownloaded(asset);

                self.notifyDownloadComplete(asset.externalId).always(function() {
                    // This small wait seems important in order to ensure there are no IO errors when writing the next file
                    setTimeout(function() {
                        dfd.resolve();
                    }, 500);
                });
            } else {
                setTimeout(function() {
                    dfd.resolve();
                }, 500);
            }

            return dfd;
        }

        /**
         * Updates the queue, sends failure notification and attempts to delete partially downloaded file
         * @param  {[type]} localFile                 [description]
         * @param  {[type]} assetQueue                [description]
         * @param  {[type]} withDownloadNotifications [description]
         * @return {[type]}                           [description]
         */
        function handleAssetDownloadFailure(localFile, assetQueue, withDownloadNotifications) {
            var dfd = $.Deferred();
            // add the asset to the failed items collection
            var asset = assetQueue.items.shift();
            assetQueue.failedItems.push(asset);

            logger.error('Media Asset Download Fail ' + asset.name);

            // notify the SL API the download has failed
            if (withDownloadNotifications) {
                self.notifyDownloadFail(asset.externalId).always(function() {
                    try {
                        File.delete(localFile).always(function() {
                            setTimeout(function() {
                                dfd.resolve();
                            }, 500);
                        });
                    } catch (e) {
                        logger.error('Error whilst removing a failed download file ' + e.message);
                        setTimeout(function() {
                            dfd.resolve();
                        }, 500);
                    }
                });
            } else {
                setTimeout(function() {
                    dfd.resolve();
                }, 500);
            }

            // remove the partially downloaded file - this does not need to be done prior to resolving method


            return dfd;
        }

        function notifySystemStart() {
            var dfd = $.Deferred();

            notify({
                type: "SystemStart"
            }).done(function() {
                dfd.resolve();
            }).fail(function() {
                dfd.reject();
            });

            return dfd;
        }

        function healthCheck() {
            // does the client need to authenticate again?
            if (isAuthenticationRequired()) {

                authenticate().done(function(authenticated) {

                    if (!authenticated) {
                        // reset the configuration timer
                        _healthCheckTimerId = setTimeout(healthCheck, calculateNextHealthCheckInterval());
                        return;
                    } else {
                        // Authenticated now - do health check
                        doHealthCheck();
                    }
                });
            } else {
                // Authenticated already - do health check
                doHealthCheck();
            }


        }

        function doHealthCheck() {
            var jqXHR = executeAPICall('notifications', {
                type: 'POST',
                data: {
                    type: 'HealthCheck'
                }
            });

            jqXHR.done(function(data, textStatus, jqXHR) {
                self.lastConnectionTime = new Date();
                logger.info('Health Check Code ' + jqXHR.status);

            }).fail(function(jqXHR, textStatus, errorThrown) {
                // log the error
                logAPIError(this.url, this.data, jqXHR.status, errorThrown, jqXHR.responseText);

                // reset auth token if it is not valid
                if (jqXHR.status == 401) {
                    Settings.application.api.authTokenExpiry = null;
                }
            }).always(function() {
                // reset the configuration timer
                _healthCheckTimerId = setTimeout(healthCheck, calculateNextHealthCheckInterval());
            });
        }

        function notify(data) {
            var dfd = $.Deferred();

            logger.info('Notify ' + data.type + ', Object Id: ' + data.object_id);

            // does the client need to authenticate again?
            if (isAuthenticationRequired()) {

                authenticate().done(function(authenticated) {

                    if (!authenticated) {
                        // Not authed - cant send notification
                        dfd.reject();
                    } else {
                        // Now authenticated do send notification
                        sendNotification(data).done(function() {
                            dfd.resolve();
                        }).fail(function() {
                            dfd.reject();
                        });
                    }
                });
            } else {
                // Already authenticated - send the notification request
                sendNotification(data).done(function() {
                    dfd.resolve();
                }).fail(function() {
                    dfd.reject();
                });
            }

            return dfd;
        }

        function sendNotification(data) {
            var dfd = $.Deferred();

            var jqXHR = executeAPICall('notifications', {
                type: 'POST',
                data: data
            });

            jqXHR.done(function(data, textStatus, jqXHR) {
                self.lastConnectionTime = new Date();
                logger.info('Notify Response Code ' + jqXHR.status);
                dfd.resolve();
            }).fail(function(jqXHR, textStatus, errorThrown) {
                // reset auth token if it is not valid
                if (jqXHR.status == 401) {
                    Settings.application.api.authTokenExpiry = null;
                }

                // log the error
                logAPIError(this.url, this.data, jqXHR.status, errorThrown, jqXHR.responseText);
                dfd.reject();
            });

            return dfd;
        }

        function executeAPICall(operationName, settings) {
            try {
                logger.debug('executeAPICall ' + operationName);
                logger.debug('PLAYER API - Client ID: ' + Settings.application.api.clientId + ', Token: ' + Settings.application.api.authToken);

                var type = 'GET';
                var async = true;
                var data = null;

                if (settings != undefined) {
                    if (settings.type != undefined)
                        type = settings.type;

                    if (settings.async != undefined)
                        async = settings.async;

                    if (settings.data != undefined)
                        data = JSON.stringify(settings.data);
                }

                return $.ajax({
                    url: Settings.application.api.url + "/" + operationName,
                    type: type,
                    contentType: 'application/json',
                    dataType: 'json',
                    async: async,
                    data: data,
                    timeout: 60000,
                    cache: false,
                    beforeSend: function(xhr, settings) {
                        xhr.setRequestHeader('X-SIGNAGELIVE-APP-ID', Settings.application.api.appId);
                        xhr.setRequestHeader('X-SIGNAGELIVE-APP-KEY', Settings.application.api.appKey);
                        xhr.setRequestHeader('X-SIGNAGELIVE-CLIENT-ID', Settings.application.api.clientId);
                        xhr.setRequestHeader('X-SIGNAGELIVE-CLIENT-KEY', Settings.application.api.clientKey);
                        xhr.setRequestHeader('X-SIGNAGELIVE-AUTH-TOKEN', Settings.application.api.authToken);
                        if (Settings.application.api.brand !== 'Signagelive') {
                            xhr.setRequestHeader('X-SIGNAGELIVE-INSTALL-ID', Settings.application.api.brand);
                        }
                    }
                });
            } catch (e) {
                logger.error('Error executing api call ' + e.message);

                return null;
            }
        }

        function isRegistrationRequired() {
            return (Settings.application.api.clientKey === undefined || Settings.application.api.clientKey === null || Settings.application.api.clientKey === "");
        }

        function isAuthenticationRequired() {
            logger.debug('isAuthenticationRequired()');

            if (Settings.application.api.authTokenExpiry == null)
                return true;
            if (Settings.application.api.authTokenExpiry < DateTime.now())
                return true;
            return false;
        }

        self.notifyDownloadStart = function(objectId) {
            return notify({
                type: "DownloadStart",
                object_id: objectId
            });
        };

        self.notifyDownloadComplete = function(objectId) {
            return notify({
                type: "DownloadComplete",
                object_id: objectId
            });
        };

        self.notifyDownloadFail = function(objectId) {
            return notify({
                type: "DownloadFail",
                object_id: objectId
            });
        };

        function doesAssetRequireDownload(mediaAsset) {
            var requiresDownload = false;
            try {

                // is the asset downloadable
                var downloadable = mediaAsset.isDownloadable;

                // is it already downloaded?
                if (downloadable) {
                    if (mediaAsset.isExternallyDelivered)
                        requiresDownload = true;
                    else {
                        // Build Local File Url
                        var localFile = "";
                        if (Settings.application.fileSystem.root !== "") {
                            localFile = localFile + Settings.application.fileSystem.root + "/";
                        }
                        if (Settings.application.fileSystem.mediaDirectory !== "") {
                            localFile = localFile + Settings.application.fileSystem.mediaDirectory + '/';
                        }
                        localFile = localFile + mediaAsset.localFileName;
                        if (File.exists(localFile) == false) {
                            requiresDownload = true;
                        }
                    }
                }
            } catch (e) {
                logger.error('Error while checking if a download is required ' + e.message);
            }

            return requiresDownload;
        }

        function postSystemReport() {
            try {

                logger.info('Sending system report');

                // does the client need to authenticate again?
                if (isAuthenticationRequired()) {
                    if (authenticate() == false) {
                        // reset the system report timer
                        setSystemReportTimer(true);
                        return;
                    }
                }
                var sysReport = new SystemReportModel();
                sysReport.generate().done(function() {
                    var s = JSON.stringify(sysReport);
                    logger.debug(s);

                    var jqXHR = executeAPICall('systemreport', {
                        type: 'POST',
                        data: sysReport
                    });

                    jqXHR.done(function(data, textStatus, jqXHR) {
                        self.lastConnectionTime = new Date();
                        logger.info('System Report Done ' + jqXHR.status);
                        setSystemReportTimer(false);
                    }).fail(function(jqXHR, textStatus, errorThrown) {
                        // log the error
                        logAPIError(this.url, this.data, jqXHR.status, errorThrown, jqXHR.responseText);
                        setSystemReportTimer(true);
                    })
                }).fail(function() {
                    logger.debug('System Report Generation Failed');
                    setSystemReportTimer(true);
                });
            } catch (e) {
                logger.debug('System Report Generation Failed ' + e.message);
                setSystemReportTimer(true);
            }
        }

        function setSystemReportTimer(retry) {
            // calculate when the next system report should occur based on settings in the configuration response.
            var interval = calculateNextSystemReportInterval(retry);

            // reset the configuration timer
            logger.info('Next System Report to be sent in ' + (interval / 1000) + ' seconds');
            _systemReportTimerId = setTimeout(postSystemReport, interval);
        }

        function calculateNextSystemReportInterval(retry) {
            var interval = _defaultSystemReportIntervalMS;

            // If we need to retry because auth failed for example then retry in 60 seconds
            if (retry === true) {
                return 60000;
            }

            if (_lastConfigurationResponse != null) {
                if (_lastConfigurationResponse.connectivity.diagnosticsUpdateIntervalScheme == ConnectivityIntervalSchemeModel.REGULAR_INTERVAL) {
                    // set the interval to the one in the response
                    interval = _lastConfigurationResponse.connectivity.diagnosticsUpdateInterval;
                } else {
                    // calculate how long until the next time of day configuration check
                    var nextCheckInTime = DateTime.now().setTime(_lastConfigurationResponse.connectivity.diagnosticsUpdateTime);
                    if (nextCheckInTime < DateTime.now())
                        nextCheckInTime = nextCheckInTime.addDays(1);

                    interval = nextCheckInTime.subtract(DateTime.now());
                }
            }

            return interval;
        }

        function logAPIError(requestUrl, requestData, responseStatus, responseMessage, responseText) {

            //If we have an auth token and server demands reauthentication, set expiry to null to force reauthentication
            if (responseStatus == 401 && Settings.application.api.authTokenExpiry) {
                Settings.application.api.authTokenExpiry = null;
            }

            var logData = {
                url: requestUrl,
                requestData: requestData,
                responseStatus: responseStatus,
                responseMessage: responseMessage,
                responseBody: responseText
            };

            logger.error("API Error " + JSON.stringify(logData));
        }


        self.initialize = function (lastConfiguration, callbacks) {
            logger.info('Initializing SLAPIInterface');

            // register callbacks
            _registrationCallback = callbacks.deviceRegistered;
            _alreadyRegisteredCallback = callbacks.deviceAlreadyRegistered;
            _configurationCallback = callbacks.configuration;
            _configurationNoChangeCallback = callbacks.configurationNoChange;

            NetworkInterface.registerAPIInterface(self);

            if (isRegistrationRequired()) {
                checkDeviceSupport();
            } else {

                // load the configuration - if there is one
                if (lastConfiguration != null) {
                    _lastConfigurationResponse = lastConfiguration;
                    _lastConfigurationModifiedDate = lastConfiguration.lastModified;

                    // get the assets that are available externally (e.g. RSS feeds) as SL will not know that
                    // these have been updated.
                    _externalAssets = $.grep(_lastConfigurationResponse.content.mediaAssets, function (mediaAsset, index) {
                        return mediaAsset.isExternallyDelivered;
                    });
                }

                // notify that the client has started
                notifySystemStart().always(function () {

                    if (_lastConfigurationResponse && _lastConfigurationResponse.dataSyncServices) {
                        // Start Syncing Data Sync Services
                        DSSAPIInterface.synchroniseIntegrationsForMediaAssets(_lastConfigurationResponse.dataSyncServices, _lastConfigurationResponse.content.integrations, _lastConfigurationResponse.content.mediaAssets)
                            .always(function () {
                                // start the configuration process
                                configuration();
                                // start the health check process
                                _healthCheckTimerId = setTimeout(healthCheck, calculateNextHealthCheckInterval());
                                // start the system report process
                                postSystemReport();
                            });
                    }
                    // else just start the configuration process
                    else {
                        // start the configuration process
                        configuration();
                        // start the health check process
                        _healthCheckTimerId = setTimeout(healthCheck, calculateNextHealthCheckInterval());
                        // start the system report process
                        postSystemReport();
                    }
                });

            }
        };

        self.notifyFirmwareUpdateComplete = function() {
            notify({
                type: "FirmwareUpdateComplete"
            });
        };
        self.notifyFirmwareUpdateFail = function() {
            notify({
                type: "FirmwareUpdateFailed"
            });
        };
    }

    return SLAPIInterface;
});
rjs.define('gui/viewmodels/ZoneViewModel',[],function() {
	function ZoneViewModel(zoneModel, xScalar, yScalar) {
		var self = this;		
		self.id = zoneModel.id;
		self.x = Math.floor(zoneModel.x * xScalar);
		self.y = Math.floor(zoneModel.y * yScalar);
		self.width = Math.floor(zoneModel.width * xScalar);
		self.height = Math.floor(zoneModel.height * yScalar);
		
		// copy all properties so they are available
		for (var property in zoneModel) {

			if (property != 'x' & property != 'y' & property != 'width' & property != 'height')
				self[property] = zoneModel[property];
		}
	}

	return ZoneViewModel;
});

rjs.define('gui/viewmodels/LayoutViewModel',["gui/viewmodels/ZoneViewModel", "controllers/UserConfigurationController"], function (ZoneViewModel, UserConfigurationController) {
	function LayoutViewModel(layoutModel, screenWidth, screenHeight) {
		var self = this;

		// By default layouts are resized to fit the available display area by determining the scalar difference between the widget and height of the layout and the screen width and height
		var layoutFit = 'fit';
		var _xScalar = screenWidth / layoutModel.width;
		var _yScalar = screenHeight / layoutModel.height;

		// The layoutFit can be changed in the user preferences so that layouts are not resized. Load user-configured layoutFit
		// As this is a core module, protect against layoutFit() being undefined in cases where a media player may override the UserConfigurationController and not implement this

		if (typeof UserConfigurationController.layoutFit === "function") {
			// safe to use the function
			if (UserConfigurationController.layoutFit() != null) {
				layoutFit = UserConfigurationController.layoutFit().id;
				logger.info('Layout Fit: ' + layoutFit);
			}
		}

		// Update scalar based on user configured layout fit
		switch (layoutFit) {
			case "fit":
				// Nothing to do as this is the default
				break;
			case "none":
				// Reset scalars back to 1 so the layout and the zones are not resized
				_xScalar = 1;
				_yScalar = 1;
				break;
			default:
				// Nothing to do
				break;
		}

		// Update layout width and height based on scalar
		self.width = layoutModel.width * _xScalar;
		self.height = layoutModel.height * _yScalar;
		// Pass scalar to the zone models so they can also be scalred appropriately
		self.zones = [];
		$(layoutModel.zones).each(function (i, zone) {
			self.zones.push(new ZoneViewModel(zone, _xScalar, _yScalar));
		});

		self.zones.sort(function (a, b) {
			// a and b will here be two objects from the array
			// thus a[1] and b[1] will equal the names

			// if they are equal, return 0 (no sorting)
			if (a.zIndex == b.zIndex) {
				return 0;
			}
			if (a.zIndex > b.zIndex) {
				// if a should come after b, return 1
				return 1;
			} else {
				// if b should come after a, return -1
				return -1;
			}
		});

		// copy all properties so they are available on the view model
		for (var property in layoutModel) {

			if (property != 'width' & property != 'height' & property != 'zones')
				self[property] = layoutModel[property];
		}

		setVideoHWZIndexZonePosition();

		// ! For HWZ and Video - BrightSign specific
        // ! https://docs.brightsign.biz/developers/html-video#MWwHs
		function setVideoHWZIndexZonePosition() {
			self.zones = self.zones.map(function (zone) {
				var updatedZone = {};
				for (var key in zone) {
					if (zone.hasOwnProperty(key)) {
						updatedZone[key] = zone[key];
					}
				}
				updatedZone.videoIndex = -2; // Default to -2
				return updatedZone;
			});

			var i, j;
			for (i = 0; i < self.zones.length; i++) {
				for (j = i + 1; j < self.zones.length; j++) {
					var firstZone = self.zones[i];
					var secondZone = self.zones[j];

					// We need to compare x/y coordinates and their width/height to determine their positioning in the DOM
					var isSeparated = firstZone.x + firstZone.width <= secondZone.x || secondZone.x + secondZone.width <= firstZone.x || firstZone.y + firstZone.height <= secondZone.y || secondZone.y + secondZone.height <= firstZone.y;

					// Whether they overlap is the inverse to the result above
					var isOverlapping = !isSeparated;

					// We only need to update their z-index if they do overlap as otherwise we will use the pre-existing logic and set their index to be -2
					if (isOverlapping) {
						// We need to validate which zone is infront of the other (there should be no issue here as previously the zones were sorted)
						var frontRect, behindRect;
						if (firstZone.zIndex > secondZone.zIndex) {
							frontRect = firstZone;
							behindRect = secondZone;
						} else if (secondZone.zIndex > firstZone.zIndex) {
							frontRect = secondZone;
							behindRect = firstZone;
						} else { continue; } // If for some reason their z-indexes match then do nothing and continue the loop

						// Otherwise set the video index of the new top layered video to be higher than the z-index of the element that is meant to be behind
						var baseIndex = behindRect.zIndex;
						var newVideoIndex = baseIndex + 1;

						// We need to account for BrightSign documentation regarding certain numbering - To route to the GPU then use z-index: 0 or hwz="off" - and the legacy value of z-index: 1 or hwz="on" should be avoided
						if (newVideoIndex === 0 || newVideoIndex === 1) {
							// If the new index is either 0 or 1 then increment it to 2 (the lowest allowed value)
							newVideoIndex = 2;
						}

						// Ensure the videoIndex is higher than the frontRect's own z-index - so we do not incorrectly set it less than the zone layer position in the DOM
						if (newVideoIndex <= frontRect.zIndex) {
							newVideoIndex = frontRect.zIndex + 1;
							// Re-check the 0/1 constraint
							if (newVideoIndex === 0 || newVideoIndex === 1) {
								newVideoIndex = 2;
							}
						}

						// Set the new video index - to be used for the HWZ attribute
						frontRect.videoIndex = newVideoIndex;
					}
				}
			}

			return self.zones;
		}
	};

	return LayoutViewModel;
});
rjs.define('gui/views/LayerBase',[],function() {
	function LayerBase(zoneViewModel, layoutName) {
		this.id = 'layer_' + zoneViewModel.externalId;
		this.x = zoneViewModel.x;
		this.y = zoneViewModel.y;
		this.width = zoneViewModel.width;
		this.height = zoneViewModel.height;
		this.zIndex = zoneViewModel.zIndex;

		// Remove existing child div if it exists
		$('#layout_' + layoutName).children('#' + this.id).remove();

		// When switching between two takeover views - e.g. back to back takeovers - if the other takeover has a layer_ with the same naming condition then we need to update and make this layer unique in the DOM
		if($('#' + this.id).length > 0) {
			this.id = this.id + "_2";
		}
		
		// Append to the layout div
		$('#layout_' + layoutName).append("<div id='" + this.id + "' style='position:absolute;left:" + this.x + "px;top:" + this.y + "px;width:" + this.width + "px;height:" + this.height + "px;'></div>");

	}

	return LayerBase;
});

rjs.define('models/PlayDirectionConstants',[],function(){
	return{
		FORWARDS:'forwards',
		BACKWARDS:'backwards'
	}
})
;
rjs.define('models/PlayStatusConstants',[],function(){
	return{
		STOPPED:'stopped',
		PLAYING:'playing',
		PAUSED:'paused'
	}
})
;
rjs.define('gui/controls/VideoPlayer',["controllers/UserConfigurationController", "models/MediaTypeConstants", "properties/App.Settings","system/DateTime"], function(UserConfigurationController, MediaTypeConstants, Settings, DateTime) {
    function VideoPlayer(parentId, x, y, width, height, zIndex, callbacks) {
        var self = this;
        var _callbacks = callbacks;
        var $videoViewer;
        self.parentId = parentId;
        self.id = parentId + "_videoViewer";
        var hwz = "z-index:" + zIndex;
        self.videoStartTimer = null;
        self.videoPlaying = false;
        self.videoPreloaded = false;
        self.videoPreloading = false;
        self.videoDuration = 0;
        self.videoStartTime = null;
        self.videoEndTime = null;
        self.videoLastFrame = null;
        self.contentType = null;

        var cssZIndex = -1;
  

        if(UserConfigurationController.videoTransparency().enabled)
        {
            hwz = "z-index:2; luma-key: #FF1500";
            cssZIndex = 2;
        }

        if (UserConfigurationController.isRotated() && Settings.application.features.maxSupportedOutputs == 1 && Settings.application.features.deviceSeries < 5) 
        {
            switch (UserConfigurationController.rotationAngle())
            {
                case 90:
                    hwz += "; transform:rot270;";
                    break;
                 case 180:
                    hwz += "; transform:rot180;";
                    break;
                case 270:
                    hwz += "; transform:rot90;";
                    break;
            }
        }

        self.html = "<div id = '" + self.id + "_container' style='position:absolute;width:" + width + "px;height:" + height + "px;visibility:hidden;'><video hwz=\""+hwz+"\" viewmode=\"scale-to-fill\" id=\"" + self.id + "\" style=\"position:absolute;left:0px;top:0px;object-fit:fill;z-index:" + cssZIndex +"\" width=\"" + width + "\" height=\"" + height + "\"></video></div>";

        self.addedToCanvas = function() {
            $videoViewer = $('#' + self.id);

            $videoViewer.off();

            // Bind events
            // Call play once loaded
            $videoViewer.on('canplay', function() {
                if(!isNaN($videoViewer[0].duration))
                {
                    self.videoDuration = $videoViewer[0].duration;
                }
                if(self.videoPreloading)
                {
                    self.videoPreloaded = true;
                    self.videoPreloading = false;
                } else {
                    $videoViewer[0].play();
                }
            });

            $videoViewer.on('loadedmetadata', function() {
                if(!isNaN($videoViewer[0].duration))
                {
                    self.videoDuration = $videoViewer[0].duration;
                }
                logger.debug('Video Duration: ' + self.videoDuration);
            });

            // Show when playing
            $videoViewer.on('playing', function() {
                logger.debug('Video: Playing');
                // Wait half 100 ms so you dont see last frame of previous video
                self.show();

                if (_callbacks.playing != null)
                    _callbacks.playing();

                self.videoStartTime = DateTime.now();

                //Does not account for streams...      
                self.videoEndTime = self.videoStartTime.addSeconds(self.videoDuration);
            });

            // Hide and call callback on ended
            $videoViewer.on('ended', function() {
   
                if(self.contentType == MediaTypeConstants.CAPTURE)
                {
                    if(self.videoPlaying)
                    {
                        self.videoPlaying = false;
                        logger.debug('HDMI: ended');          
                        self.restartVideo();    
                    }
                    
                }else {
                    logger.debug('Video: Ended');
                    self.videoPlaying = false;
               
                    //Might be a very short video
                    if(self.videoStartTimer)
                        clearTimeout(self.videoStartTimer);
    
                    //self.videoLastFrame = getFrame();
    
                    //self.hide();
                    if (_callbacks.complete != null)
                        _callbacks.complete();
                }
            });

            // Hide and call failure callback on error
            $videoViewer.on('error', function(e) {
                //Setting src to empty can cause an error, so we only bother actioning an error if we have called play.
                if(self.videoPlaying)
                {
                    
                    if(self.contentType == MediaTypeConstants.CAPTURE)
                    {
                        //HDMI In
                        logger.error('HDMI: Error');
                        logger.error("Error " +  e.target.error.code);
                        self.restartVideo();       
                    } else 
                    {
                        //Video and IPTV
                        logger.error('Video: Error');
                        logger.error("Error " +  e.target.error.code);
                        self.hide();
    
                        if($videoViewer.attr('src'))
                            $videoViewer.attr('src', '');
    
                        if (_callbacks.fail != null)
                            _callbacks.fail();
                    }
                }
            });
        };

        function getFrame()
        {
            var canvas = document.createElement("canvas");
            canvas.width = $videoViewer[0].clientWidth;
            canvas.height = $videoViewer[0].clientHeight;
            canvas.getContext('2d').drawImage($videoViewer[0], 0, 0, canvas.width, canvas.height);
            
            return canvas.toDataURL();
        }

        self.clearSync = function()
        {
            try{
                logger.debug('Clearing video sync params');
                $videoViewer[0].setSyncParams(null,null, null);
            } catch(err)
            {
                logger.error("Error clearing sync params: " + err.message);
            }  
        };

        // Play a video - sets src and calls load()
        self.play = function(mediaAsset, syncEvent) {
            var path = "";
            self.contentType = mediaAsset.contentType;

            if(mediaAsset.contentType == MediaTypeConstants.VIDEO)
            {
                path =  "media/" + mediaAsset.localFileName;
            }else if (mediaAsset.contentType == MediaTypeConstants.CAPTURE) {
                
                path = "tv:brightsign.biz/hdmi";
            } else {
                //IPTV
                path = mediaAsset.remoteUrl;
            }
            logger.debug("Video Play: " + path);
            
            self.videoPlaying = true;    

            $videoViewer.attr('src', path);
            logger.debug("src set ");
            
            if(syncEvent)
            {
                try {
                    logger.debug("Setting sync params");
                    $videoViewer[0].setSyncParams(syncEvent.domain, syncEvent.id, syncEvent.iso_timestamp);
                } catch (err) {
                    logger.error("Error setting sync params: " + err.message);
                }
            }
            
            $videoViewer[0].load();
            //Only use the timer with video
            
            if(mediaAsset.contentType == MediaTypeConstants.VIDEO)
            {
                self.videoStartTimer = setTimeout(function()
                {
                    if($videoViewer[0].currentTime == 0)
                    {
                        logger.debug("Failing video as it does not appear to have started playing");
                        self.videoFailed();
                    }
                },6500);
            }
            
        };

        // Stop video - pause and trigger ended event which will hide and move next
        self.stop = function(doNotRaiseEndedEvent) {

            if(self.videoStartTimer)
                clearTimeout(self.videoStartTimer);

            logger.debug('Video Stop');
            self.videoPlaying = false;
            $videoViewer[0].pause();

            if(!doNotRaiseEndedEvent)
            {
                $videoViewer.trigger('ended');
            } 
        };

        self.pause = function() {
            $videoViewer[0].pause();
        };

        self.stopAndHideDoNotMoveNext = function(keepVideoVisible) {
            if(self.videoStartTimer)
                clearTimeout(self.videoStartTimer);

            logger.debug('Video StopAndHideDoNotMoveNext');
            self.videoPlaying = false;
            $videoViewer[0].pause();

            if(keepVideoVisible !== true) {
                self.hide();
            }
        };

        self.videoFailed = function()
        {
            if(self.videoStartTimer)
                clearTimeout(self.videoStartTimer);

            self.hide();
            if (_callbacks.fail != null)
                _callbacks.fail();
        }

        self.setMute = function(muted) {
             if(muted)
            {
                $videoViewer.attr("Pcmaudio", "none");
                $videoViewer.attr("Compaudio", "none");
                $videoViewer.attr("Multiaudio", "none");
            } else {
                var discreetAudioChannels = UserConfigurationController.audio().discreetAudioChannels.join(';').toLowerCase() || 'none';

                $videoViewer.attr("Pcmaudio", ((UserConfigurationController.audio().globalAudioChannel === 'hdmi' && discreetAudioChannels) ? discreetAudioChannels : UserConfigurationController.audio().globalAudioChannel));
                $videoViewer.attr("Compaudio", ((UserConfigurationController.audio().globalAudioChannel === 'hdmi' && discreetAudioChannels) ? discreetAudioChannels : UserConfigurationController.audio().globalAudioChannel));
                $videoViewer.attr("Multiaudio", ((UserConfigurationController.audio().globalAudioChannel === 'hdmi' && discreetAudioChannels) ? discreetAudioChannels : UserConfigurationController.audio().globalAudioChannel));              
            }
        };

        self.volumeUp = function() {

        };

        self.volumeDown = function() {

        };

        self.show = function() {
            $videoViewer.css('visibility', 'visible');
            $('#' + self.parentId).css('background-color', 'transparent'); // Note that we set background black so that it looks nice between assets but need to set transparent to see videos when z-index = -1
        };

        self.hide = function() {
            //logger.debug("VideoViewer hide");
            self.clearVideo();
            $videoViewer.css('visibility', 'hidden');
            $(self.parentId).css('background-color', '#000000');
        };

        self.clearVideo = function()
        {
            logger.debug("VideoViewer clearVideo");
            if($videoViewer.attr('src'))
                $videoViewer.attr('src', '');
        };

        self.restartVideo = function() {
            logger.debug('Restarting HDMI');
            $videoViewer.attr('src', '');
            $videoViewer.attr('src', 'tv:brightsign.biz/hdmi');
            $videoViewer[0].load();
        };

    }

    return VideoPlayer;
});
rjs.define('gui/controls/WidgetVideoPlayer',["controllers/UserConfigurationController", "models/MediaTypeConstants", "properties/App.Settings","system/DateTime", "models/PlayStatusConstants"], function(UserConfigurationController, MediaTypeConstants, Settings, DateTime, PlayStatusConstants) {
    function WidgetVideoPlayer(parentId, x, y, width, height, callbacks, screenTakeoverController) {
        var self = this;
        var _callbacks = callbacks;
        var $widgetVideoViewer;
        self.parentId = parentId;
        self.id = parentId + "_widgetVideoViewer";
        var hwz = "z-index:-2";
        self.status = PlayStatusConstants.STOPPED;
        self.videoStartTimer = null;
        self.videoStarted = false;
        self.videoPlaying = false;
        self.videoPreloaded = false;
        self.videoPreloading = false;
        self.videoDuration = 0;
        self.videoStartTime = null;
        self.videoEndTime = null;
        self.contentType = null;
        self.syncEvent = null;
        self.screenTakeoverController = screenTakeoverController;

        var _playingPlaylist = false;
        var _playlist = null;
        var _currentVideo = null;
        var _currentVideoIndex = -1;

        self.x = x;
        self.y = y;
        self.width = width;
        self.height = height;
        self.zIndex = 0;

        var cssZIndex = -1;
  

        if(UserConfigurationController.videoTransparency().enabled)
        {
            hwz = "z-index:2; luma-key: #FF1500";
            cssZIndex = 2;
        }

        if (UserConfigurationController.isRotated()) 
        {
            switch (UserConfigurationController.rotationAngle())
            {
                case 90:
                    hwz += "; transform:rot270;";
                    break;
                 case 180:
                    hwz += "; transform:rot180;";
                    break;
                case 270:
                    hwz += "; transform:rot90;";
                    break;
            }
        }

        self.html = "<div id = '" + self.id + "_container' style='position:absolute;width:" + width + "px;height:" + height + "px;visibility:hidden;'><video hwz=\""+hwz+"\" viewmode=\"scale-to-fill\" id=\"" + self.id + "\" style=\"position:absolute;left:0px;top:0px;object-fit:fill;z-index:" + cssZIndex +"\" width=\"" + width + "\" height=\"" + height + "\"></video></div>";

        self.addedToCanvas = function() {
            $widgetVideoViewer = $('#' + self.id);

            // self.setVideoSyncEvent(); // NOTE: The ability to sync widget video is not currently supported - during testing this caused additional transition issues

            $widgetVideoViewer.off();

            // Bind events
            // Call play once loaded
            $widgetVideoViewer.on('canplay', function() {
                if(!isNaN($widgetVideoViewer[0].duration))
                {
                    self.videoDuration = $widgetVideoViewer[0].duration;
                }
                if(self.videoPreloading)
                {
                    self.videoPreloaded = true;
                    self.videoPreloading = false;
                } else {
                    $widgetVideoViewer[0].play();
                }
            });

            $widgetVideoViewer.on('loadedmetadata', function() {
                if(!isNaN($widgetVideoViewer[0].duration))
                {
                    self.videoDuration = $widgetVideoViewer[0].duration;
                }
                logger.debug('Video Duration: ' + self.videoDuration);
            });

            // Show when playing
            $widgetVideoViewer.on('playing', function() {
                logger.debug('DEV -- Video: Playing');
                // Wait half 100 ms so you dont see last frame of previous video
                self.show();

                if (_callbacks.playing != null)
                    _callbacks.playing();

                self.videoStartTime = DateTime.now();

                //Does not account for streams...      
                self.videoEndTime = self.videoStartTime.addSeconds(self.videoDuration);
                self.videoPlaying = true;
            });

            $widgetVideoViewer.on('timeupdate', function() {
                var _currentTime = $widgetVideoViewer[0].currentTime;
                var _video = $widgetVideoViewer[0].src;

                if (_callbacks.timeupdate) {
                    _callbacks.timeupdate(_video, _currentTime);
                }
            });

            // Hide and call callback on ended
            $widgetVideoViewer.on('ended', function() {
                logger.info('Widget Video Player - Video Stream completed.');
   
                if(self.contentType == MediaTypeConstants.CAPTURE)
                {
                    if(self.videoStarted)
                    {
                        self.videoStarted = false;
                        self.videoPlaying = false;
                        logger.debug('HDMI: ended');          
                        self.restartVideo();    
                    }
                    
                }else {
                    logger.debug('Video: Ended');
                    self.videoStarted = false;
                    self.videoPlaying = false;
               
                    //Might be a very short video
                    if(self.videoStartTimer)
                        clearTimeout(self.videoStartTimer);
    
                    if (_callbacks.complete != null)
                        _callbacks.complete();

                    logger.info('Widget Video Player - Video in a playlist playback completed.');
                    _currentVideo = moveToNextVideo();
                    var endOfPlaylist = (_currentVideoIndex === 0);
                    if (self.loop === false && endOfPlaylist) { return; } else { logger.info('Widget Video Player - Moving to next video...'); }
                    // Play Next Video
                    playVideo(_currentVideo);

                }
            });

            // Hide and call failure callback on error
            $widgetVideoViewer.on('error', function(e) {
                //Setting src to empty can cause an error, so we only bother actioning an error if we have called play.
                if(self.videoStarted)
                {
                    
                    if(self.contentType == MediaTypeConstants.CAPTURE)
                    {
                        //HDMI In
                        logger.error('HDMI: Error');
                        logger.error("Error " +  e.target.error.code);
                        self.restartVideo();       
                    } else 
                    {
                        //Video and IPTV
                        logger.error('Video: Error');
                        logger.error("Error " +  e.target.error.code);
                        self.hide();
    
                        if($widgetVideoViewer.attr('src'))
                            $widgetVideoViewer.attr('src', '');
    
                        if (_callbacks.fail != null)
                            _callbacks.fail();
                    }
                }
            });
        };

        // self.setVideoSyncEvent = function () {
            // self.syncEvent = self.screenTakeoverController.syncEvent;
        // };

        self.clearSync = function()
        {
            try{
                logger.debug('Clearing video sync params');
                $widgetVideoViewer[0].setSyncParams(null,null, null);
            } catch(err)
            {
                logger.error("Error clearing sync params: " + err.message);
            }  
        };

        self.play = function(playlist, x, y, width, height, loop, play4k) {
            $widgetVideoViewer = $('#' + self.id); // Update Video Viewer Reference

            _playingPlaylist = true;
            // Reset position
            _currentVideoIndex = -1;
            logger.info("Widget Video Player - VideoPlayer.playPlaylist()");
            self.x = x;
            self.y = y;
            self.width = width;
            self.height = height;
            self.loop = loop;
            // Make a note if any of the videos in this playlist are 4K
            self.is4kPlaylist = play4k;
            // Set gapless playlist
            _playlist = playlist;
            // Move to next video
            _currentVideo = moveToNextVideo();
            // Play Next Video
            if (_currentVideo) {
                self.status = PlayStatusConstants.PLAYING;
                self.videoStarted = true;
                playVideo(_currentVideo);
            }

            self.videoStartTimer = setTimeout(function()
            {
                if($widgetVideoViewer[0].currentTime == 0)
                {
                    logger.debug("Failing video as it does not appear to have started playing");
                    self.videoFailed();
                }
            },6500);
        };

        function playVideo(url) {
            $widgetVideoViewer.attr('src', url);
            logger.debug("src set ");
            if(self.syncEvent)
                {
                    $widgetVideoViewer[0].setSyncParams(self.syncEvent.domain, self.syncEvent.id, self.syncEvent.iso_timestamp);
                }

            $widgetVideoViewer[0].load();
        };

        function moveToNextVideo() {
            var nextVideo = null;
            var nextVideoIndex = _currentVideoIndex;
            logger.info('Widget Video Player - Current video index: ' + _currentVideoIndex);
            // Work out next index
            nextVideoIndex = _currentVideoIndex + 1;
            if (nextVideoIndex >= _playlist.length) {
                nextVideoIndex = 0;
            }
            // Get next video to play
            nextVideo = _playlist[nextVideoIndex];
            logger.info('Widget Video Player - Next video to play: ' + nextVideo);
            // Update current index
            _currentVideoIndex = nextVideoIndex;
            // Return next PMA
            return nextVideo;
        }

        // Stop video - pause and trigger ended event which will hide and move next
        self.stop = function(doNotRaiseEndedEvent) {
            if (self.status === PlayStatusConstants.PLAYING) {
                if(self.videoStartTimer)
                    clearTimeout(self.videoStartTimer);

                logger.debug('Video Stop');
                self.videoStarted = false;
                self.videoPlaying = false;
                $widgetVideoViewer[0].pause();
                
                self.status = PlayStatusConstants.STOPPED;

                if(!doNotRaiseEndedEvent)
                {
                    $widgetVideoViewer.trigger('ended');
                } 
            }
        };

        self.pause = function() {
            $widgetVideoViewer[0].pause();
        };

        self.stopAndHideDoNotMoveNext = function(keepVideoVisible) {
            if(self.videoStartTimer)
                clearTimeout(self.videoStartTimer);

            logger.debug('Video StopAndHideDoNotMoveNext');
            self.videoStarted = false;
            self.videoPlaying = false;
            $widgetVideoViewer[0].pause();

            if(keepVideoVisible !== true) {
                self.hide();
            }
        };

        self.videoFailed = function()
        {
            if(self.videoStartTimer)
                clearTimeout(self.videoStartTimer);

            self.hide();
            if (_callbacks.fail != null)
                _callbacks.fail();
        }

        self.setMute = function(muted) {
             if(muted)
            {
                $widgetVideoViewer.attr("Pcmaudio", "none");
                $widgetVideoViewer.attr("Compaudio", "none");
                $widgetVideoViewer.attr("Multiaudio", "none");
            } else {
                $widgetVideoViewer.removeAttr("Pcmaudio");
                $widgetVideoViewer.removeAttr("Compaudio");
                $widgetVideoViewer.removeAttr("Multiaudio");
            }
        };

        self.volumeUp = function() {

        };

        self.volumeDown = function() {

        };

        self.show = function() {
            $widgetVideoViewer.css('visibility', 'visible');
            $('#' + self.parentId).css('background-color', 'transparent'); // Note that we set background black so that it looks nice between assets but need to set transparent to see videos when z-index = -1
        };

        self.hide = function() {
            //logger.debug("VideoViewer hide");
            self.clearVideo();
            $widgetVideoViewer.css('visibility', 'hidden');
            $(self.parentId).css('background-color', '#000000');
        };

        self.clearVideo = function()
        {
            logger.debug("VideoViewer clearVideo");
            if($widgetVideoViewer.attr('src'))
                $widgetVideoViewer.attr('src', '');
        };

        self.restartVideo = function() {
            logger.debug('Restarting HDMI');
            $widgetVideoViewer.attr('src', '');
            $widgetVideoViewer.attr('src', 'tv:brightsign.biz/hdmi');
            $widgetVideoViewer[0].load();
        };

    }

    return WidgetVideoPlayer;
});
rjs.define('system/GUID',[],function() {
	function Guid() {
		var self = this;
		self.newGuid = function() {
			function s4() {
				return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
			}

			return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
		};

	}

	return Guid;
});


rjs.define('gui/controls/WidgetViewer',["controllers/WidgetEventController", "gui/controls/WidgetVideoPlayer", "controllers/UserConfigurationController", "gui/PlayUrlHelper", "models/MediaTypeConstants", "properties/App.Settings", "system/SystemInterface", "system/GUID"], function(WidgetEventController, WidgetVideoPlayer, UserConfig, PlayUrlHelper, MediaTypeConstants, Settings, SystemInterface, GUID) {
    function WidgetViewer(parentId, x, y, width, height, parentLayoutId, callbacks) {
        var self = this;
        self.callbacks = callbacks;
        self.id = parentId + '_widgetViewer';
        self.x = x;
        self.y = y;
        self.width = width;
        self.height = height;
        self.zIndex = 0;
        var viewerOneId = parentId + '_widgetViewer_1';
        var viewerTwoId = parentId + '_widgetViewer_2';
        var html = "<iframe class='viewer' sandbox='allow-same-origin allow-scripts allow-forms allow-presentation' id='" + viewerOneId + "' name='" + viewerOneId + "' frameborder='0' scrolling='no' src='' style='position:absolute;left:0px;top:0px;width:" + width + "px;height:" + height + "px;visibility:hidden;'></iframe><iframe class='viewer' sandbox='allow-same-origin allow-scripts allow-forms' id='" + viewerTwoId + "' name='" + viewerTwoId + "' frameborder='0' scrolling='no' src='' style='position:absolute;left:0px;top:0px;width:" + width + "px;height:" + height + "px;visibility:hidden;'></iframe>";
        self.$activeViewer = null;
        self.$inactiveViewer = null;

        var showTimeout = null;
        var showing = false;
        var ignoreMoveNext = false;
        var videoPlayDFD = null;

        self.defaultTimeToWaitForReadyEventInMS = 1500;

        self.widgetVideoPlayer = new WidgetVideoPlayer(parentId, self.x, self.y, self.width, self.height, { playing: onVideoPlaying, stopped: onVideoStopped, complete: onVideoComplete, error: onVideoError, timeupdate: onVideoTimeUpdate });
        $('#' + parentId).append(self.widgetVideoPlayer.html);
        self.widgetVideoPlayer.addedToCanvas();

        self.appendToCanvas = function() {
            $('#' + parentId).append(html);
            WidgetEventController.registerWidgetViewer(self, parentLayoutId);
            self.$activeViewer = $('#' + viewerOneId);
            self.$inactiveViewer = $('#' + viewerTwoId);
        };

        self.play = function(url, duration, mediaAsset)  {
            logger.info('Playing widget using widget viewer control. url to play: ' + url);
            ignoreMoveNext = false;
            showing = false;
            self.mediaAsset = mediaAsset;
            self.currentAssetDuration = duration;
       
            self.$inactiveViewer.off('load').on('load', function() {
                self.$inactiveViewer.off('load');
                var currentSrc = self.$inactiveViewer.attr('src');
                if (currentSrc !== 'about:blank') {
                    showTimeout = setTimeout(function () {
                        clearTimeout(showTimeout);
                        logger.info('Showing widget after 1.5 seconds - ready event was not recieved');
                        self.show();
                    }, self.defaultTimeToWaitForReadyEventInMS);
                }
            });
            self.$inactiveViewer.attr("src", url);
        };

        // Basic video settings
        var videoSettings = {
            fileName: null,
            x: null,
            y: null,
            width: null,
            height: null,
            loop: false,
            play4K: false
        };

        self.prepareVideo = function(uris, x, y, width, height, loop, play4K) {
            videoPlayDFD = $.Deferred();
            // Store settings
            videoSettings.uris = uris;
            videoSettings.x = x;
            videoSettings.y = y;
            // Ensure video does not exceed webpage bounds
			if ((UserConfig.isRotated() && (UserConfig.rotationAngle() === 90 || UserConfig.rotationAngle() === 270))) {
				videoSettings.width = height;
				videoSettings.height = width;
			} else {
				videoSettings.width = width;
				videoSettings.height = height;
			}
            videoSettings.loop = loop;
            videoSettings.play4K = play4K;
            // Get the MediaZoneView to get available AV Players
            if (callbacks.prepareVideo) {
                callbacks.prepareVideo();
            }
            return videoPlayDFD;
        }

        self.playVideo = function() {
            // Check if this is a playlist of vides or individual video
            var isPlaylist = Array.isArray(videoSettings.uris);
            getFullURIs(videoSettings.uris, isPlaylist).done(function() {
                var uris = arguments;
                logger.info(JSON.stringify(uris, undefined, 2));
                try {
                    self.widgetVideoPlayer.play(uris, self.x + videoSettings.x, self.y + videoSettings.y, videoSettings.width, videoSettings.height, videoSettings.loop, videoSettings.play4K);
                    // TODO: Is this the right place to resolve?
                    videoPlayDFD.resolve(true);       
                }
                catch (e) {
                    logger.error(e);
                    self.widgetVideoPlayer.stop();
                    videoPlayDFD.resolve(false);
                }
            });
        };

        self.remove = function() {
            self.$activeViewer.remove();
            self.$inactiveViewer.remove();
            showing = false;
            clearTimeout(showTimeout);
            showTimeout = null;
            self.callbacks = {};
        };

        self.show = function() {

            logger.info("Show Widget Viewer...");
            if (!showing && ignoreMoveNext === false) {
                logger.info('Showing widget viewer control and clearing show timeout');
                clearTimeout(showTimeout);
                showTimeout = null;
                // Hide all other elements
                if (self.callbacks.ready) {
                    self.callbacks.ready(self.currentAssetDuration);
                }
                switchActiveViewer();
                self.$activeViewer.css('visibility', 'visible');
                hideInactiveViewer();
                showing = true;
            }
            else {
                logger.warning('Attempted to show widget viewer control but it is already showing');
            }       
        };

        self.hide = function(keepVideoVisible) {
            if (keepVideoVisible !== true) {
                self.widgetVideoPlayer.hide();
            }
            logger.info('Hiding widget viewer control and clearing timeout');
            clearTimeout(showTimeout);
            showTimeout = null;
            hideActiveViewer();
            showing = false;
        };
        
        self.stop = function() {
            ignoreMoveNext = false;
            self.sendWidgetClosingNotification();
            self.hide();
            WidgetEventController.unregisterWidgetViewer(self, parentLayoutId);
            self.widgetVideoPlayer.stop();
        };

        self.hideVideo = function() {
            self.widgetVideoPlayer.hide();
        }

        self.stopVideo = function() {
            self.widgetVideoPlayer.stop(true);
        }
        self.pauseVideo = function() {
            self.widgetVideoPlayer.pause();
        }

        self.requestMediaPlayerMoveToNextAsset = function() {
            if (ignoreMoveNext === false) {
                ignoreMoveNext = true;
                clearTimeout(showTimeout);
                self.callbacks.moveNext();
            }
            return $.Deferred().resolve();
        }

        self.sendWidgetClosingNotification = function() {
            WidgetEventController.sendWidgetClosingNotification(self.id);
        }

        function onVideoTimeUpdate(currentVideo, currentTime) {
            WidgetEventController.onVideoTimeUpdate(self.$activeViewer.attr('id'), currentVideo, currentTime);
        }
        function onVideoPlaying(currentVideo) {
            // WidgetEventController.onVideoPlaying(self.$activeViewer.attr('id'), currentVideo, currentTime);
        }

        function onVideoStopped(currentVideo) {
            if (self.callbacks.stopped) {
                self.callbacks.stopped();
            }
            // WidgetEventController.onVideoStopped(self.$activeViewer.attr('id'), currentVideo);
        }

        function onVideoComplete(currentVideo) {
            WidgetEventController.onVideoComplete(self.$activeViewer.attr('id'), currentVideo);
        }

        function onVideoError(currentVideo, errorMessage) {
            WidgetEventController.onVideoError(self.$activeViewer.attr('id'), currentVideo, errorMessage);
        }

        function switchActiveViewer() {
            logger.debug('Switching active Viewer');
            var $temp = self.$activeViewer;
            self.$activeViewer = self.$inactiveViewer;
            self.$inactiveViewer = $temp;
        }

        function hideActiveViewer() {
            logger.debug('Hide active Viewer');
            self.$activeViewer = hideViewer(self.$activeViewer);
        }

        function hideInactiveViewer() {
            logger.debug('Hide inactive Viewer');
            self.$inactiveViewer = hideViewer(self.$inactiveViewer);
        }

        function hideViewer($viewer) {
            var id = $viewer.attr('id');
            var html = "<iframe class='viewer' sandbox='allow-same-origin allow-scripts allow-forms' id='" + id + "' name='" + id + "' frameborder='0' scrolling='no' src='' style='position:absolute;left:0px;top:0px;width:" + width + "px;height:" + height + "px;visibility:hidden;'></iframe>";
            $("#" + id).css('visibility', 'hidden');
            $viewer.remove();
            $('#' + parentId).append(html);
            return $viewer = $('#' + id);
        }

        function getFullURIs(uris, isPlaylist) {
            logger.info('getting full video URIs');
            var urisArray = [];
            if (isPlaylist) {
                urisArray = uris;
            }
            else {
                urisArray.push(uris);
            }
            // Get base path
            var widgetBasePath = '';
            if (Settings.application.fileSystem.root !== "") {
                widgetBasePath += Settings.application.fileSystem.root + "/";
            }
            if (Settings.application.fileSystem.widgetDirectory !== "") {
                widgetBasePath += Settings.application.fileSystem.widgetDirectory + '/';
            }
            widgetBasePath += self.mediaAsset.localFileName.replace('.wgt', '') + '/';

            var promises = [];
            $.each(urisArray, function(index, uri) {
                // Check if we were provided the full URI or relative URI
                if (uri.startsWith('file:///')) {
                    promises.push($.Deferred().resolve(uri)); // resolve as is
                }
                else {
                    var fileURI = widgetBasePath + uri;
                    logger.info('Getting URI for virtual path: ' + fileURI);
                    promises.push(PlayUrlHelper.getFileUrlForVirtualPath(fileURI));
                }
            });

            return $.when.apply(undefined, promises).promise();   
        }

        self.onKeyPress = function(key) {
            try {
                var widgetIframe = document.getElementById(self.id);
                if (widgetIframe) {
                    widgetIframe.contentWindow.onKeyPress(key);
                }
            } catch (err) {
                //Function not on widget
            }
        };

    }

    return WidgetViewer;
});
rjs.define('gui/controls/TVPlayer',["controllers/UserConfigurationController", "models/MediaTypeConstants","properties/App.Settings"], function(UserConfigurationController, MediaTypeConstants, Settings) {
    function TVPlayer(parentId, x, y, width, height, callbacks) {
        var self = this;
        var _callbacks = callbacks;
        var $tvViewer;
        self.initialized = false;
        self.id = parentId + "_tvViewer";
        var hwz = "z-index:-1";
        self.videoStartTimer = null;
        self.videoPlaying = false;
        self.inUse = false;
    
        if (UserConfigurationController.isRotated() && Settings.application.features.maxSupportedOutputs == 1 && Settings.application.features.deviceSeries < 5) 
        {
            switch (UserConfigurationController.rotationAngle())
            {
                case 90:
                    hwz += "; transform:rot270;";
                    break;
                 case 180:
                    hwz += "; transform:rot180;";
                    break;
                case 270:
                    hwz += "; transform:rot90;";
                    break;
            }
        }

        self.html = "<div id = '" + self.id + "_container' style='position:absolute;left:" + x + "px;top:" + y + "px;width:" + width + "px;height:" + height + "px;visibility:hidden;'><video id=\"" + self.id + "\" viewmode=\"scale-to-fill\" hwz=\"" + hwz + "\" style=\"position:absolute;left:0px;top:0px;object-fit:fill\" width=\"" + width + "\" height=\"" + height + "\"></video></div>";

        self.addedToCanvas = function() {
            $tvViewer = $('#' + self.id);

            $tvViewer.off();

            $tvViewer.on('playing', function() {
                console.log('Playing hdmi');
                self.show();

                self.videoStartTimer = setTimeout(function()
                {               
                    if (_callbacks.playing != null)
                        _callbacks.playing();
                },100)
            });

            $tvViewer.on('waiting', function(e) {
                logger.debug('HDMI: waiting');
              
            });

            //If HDMI drops, this will fire ~5-6 seconds afterwards
            $tvViewer.on('ended', function(e) {
                logger.debug('HDMI: ended');          
                self.restartVideo();    
            });

            $tvViewer.on('error', function(e) {
                if(self.inUse)
                {
                    logger.error('HDMI: Error');
                    logger.error("Error " +  e.target.error.code);
                    self.restartVideo();                
                }
            });

            $tvViewer.on('loadedmetadata', function(e) {
                logger.debug('HDMI loadedmetadata');     
                $tvViewer[0].play();      
            });
        };

        self.play = function() {
            self.videoPlaying = true;
            self.inUse = true;
            $tvViewer.attr('src', 'tv:brightsign.biz/hdmi');
            $tvViewer[0].load();
            //$tvViewer[0].play();
        };

        self.restartVideo = function() {
            logger.debug('Restarting HDMI');
            $tvViewer.attr('src', '');
            $tvViewer.attr('src', 'tv:brightsign.biz/hdmi');
            $tvViewer[0].load();
        };

        self.stop = function() {
            logger.debug('HDMI Stop');
            if(self.videoStartTimer)
                clearTimeout(self.videoStartTimer);
            
            $tvViewer[0].pause();
            self.inUse = false;            
        };

        self.setMute = function(muted) {
            if(muted)
            {
                $tvViewer.attr("Pcmaudio", "none");
                $tvViewer.attr("Compaudio", "none");
                $tvViewer.attr("Multiaudio", "none");
            } else {
                var discreetAudioChannels = UserConfigurationController.audio().discreetAudioChannels.join(';').toLowerCase() || 'none';

                $tvViewer.attr("Pcmaudio", ((UserConfigurationController.audio().globalAudioChannel === 'hdmi' && discreetAudioChannels) ? discreetAudioChannels : UserConfigurationController.audio().globalAudioChannel));
                $tvViewer.attr("Compaudio", ((UserConfigurationController.audio().globalAudioChannel === 'hdmi' && discreetAudioChannels) ? discreetAudioChannels : UserConfigurationController.audio().globalAudioChannel));
                $tvViewer.attr("Multiaudio", ((UserConfigurationController.audio().globalAudioChannel === 'hdmi' && discreetAudioChannels) ? discreetAudioChannels : UserConfigurationController.audio().globalAudioChannel));      
            }
        };

        self.volumeUp = function() {

        };

        self.volumeDown = function() {

        };

        self.show = function() {
            $tvViewer.css('visibility', 'visible');
            $('#' + self.parentId).css('background-color', 'transparent'); 
        };

        self.hide = function() {
            self.clearVideo();

            $tvViewer.css('visibility', 'hidden');            
            $(self.parentId).css('background-color', '#000000');
        };

        self.clearVideo = function()
        {
            if($tvViewer.attr('src'))
                $tvViewer.attr('src', '');
        };
    }

    return TVPlayer;
});
rjs.define('models/UrlSandboxUnsupportedHelper',["properties/App.Settings", "system/io/File"], function(Settings, File) {
    function UrlSandboxUnsupporterHelper() {
        var self = this;
        self.urlList = [];

        self.isSandboxSupported = function(mediaAssetUrl) {
            var dfd = $.Deferred();

            if(self.urlList.length == 0)
            {
                loadData().done(function()
                {
                    dfd.resolve(checkUrls(mediaAssetUrl));
                });
            } else {
                dfd.resolve(checkUrls(mediaAssetUrl));
            }          
            
            return dfd;
        };

        function checkUrls(url)
        {
            var matchFound = false;
            var urlLower = url.toLowerCase();
            $.each(self.urlList, function( index, value ) {
                logger.debug("Checking " + url + " for " + value);
                if(urlLower.includes(value.toLowerCase()))
                {
                    matchFound = true; 
                    return false; //break                  
                } 
            });

            if(matchFound)
            {
                logger.debug("Match found, sandbox not supported");
                return false;
            } else {
                logger.debug("Match not found, sandbox assumed supported");
                return true;
            }  

        }

        function loadData()
        {
            var dfd = $.Deferred();
			
			File.read(Settings.application.fileSystem.systemFiles.sandboxUnsupportedUrlsPath).done(function(data)
			{
				if(data!=null && data != "")
				{
					console.debug("Sandbox Unsupported list loaded");
					
					try{
                        var jsonData = JSON.parse(data);

                        self.urlList = jsonData.urls;
					} catch (e)
					{
					
					}
				}else{
					console.debug("No unsupported to load");
				}
				
				dfd.resolve();
			}).fail(function()
            {
                console.debug("No unsupported urls");
                dfd.resolve();

            });
			
			return dfd.promise();
        }


    }

    var helper = new UrlSandboxUnsupporterHelper();
    return helper;
});
rjs.define('system/Guid',[],function() {
	function Guid() {
		var self = this;
		self.newGuid = function() {
			function s4() {
				return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
			}

			return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
		};

	}

	return Guid;
});


rjs.define('gui/views/MediaZoneView',["system/DateTime", "system/io/File", "system/io/Directory", "gui/PlayUrlHelper", "gui/views/LayerBase", "models/MediaTypeConstants", "models/PlayDirectionConstants", "models/PlayStatusConstants", "properties/App.Settings", "gui/controls/VideoPlayer", "controllers/UserConfigurationController", "models/PlaybackStartEndReasons", "gui/controls/WidgetViewer", "controllers/RealTimeEventsController", "gui/controls/TVPlayer","models/UrlSandboxUnsupportedHelper", "controllers/ScreenTakeoverController", "controllers/MediaSyncController", "system/Guid", "system/graphics/GraphicsInterface"], function(DateTime, File, Directory, PlayUrlHelper, LayerBase, MediaTypeConstants, PlayDirection, PlayStatus, Settings, VideoPlayer, UserConfig, PlaybackStartEndReasons, WidgetViewer, RealTimeEventsController, TVPlayer, UrlSandboxUnsupportedHelper, ScreenTakeoverController, MediaSyncController, Guid, GraphicsInterface) {
    /**
     * MediaZoneView
     * ---------------------------------------------
     * A MediaZoneView represents a content zone in a Signagelive layout or the entire zone in a fullscreen playlist.
     * This module is responsible for playing the content that is scheduled a particular zone.
     *
     * You may need to implement a custom version of this module to handle differences in the way media players deal with web content, video playback or TV IN.
     * 
     * @param {ZoneViewModel} The MediaZoneView model
     * @param {PlaybackStartEndReasons} The reason that playback has started (Schedule/Interrupt)
     */
    function MediaZoneView(zoneViewModel, playReason, triggerAdditionalData, layout, zoneIndex, messagePort, playInterruptIndefinitely, callbacks, layoutName) {
       
        var _MediaSyncController = null;
        
        this.parent = new LayerBase(zoneViewModel, layoutName);
        for (var property in this.parent) {

            if (property != 'prototype' & property != 'parent')
                this[property] = this.parent[property];
        }

        var self = this;
        
        self.guid = new Guid().newGuid();
        
        self.viewModel = zoneViewModel;
        self.imageViewerIdSuffix = '_imgViewer';
        self.screenTakeoverOverlay = 'screenTakeover_overlay';
        self.inactiveImageViewerIdSuffix = '_imgViewer1';
        self.activeWebViewerIdSuffix = '_webViewer0';
        self.inactiveWebViewerIdSuffix = '_webViewer1';
        self.layout = layout;

        self.triggerAdditionalData = triggerAdditionalData;
        self.playReason = playReason;
        self.playInterruptIndefinitely = playInterruptIndefinitely;
        self.screenTakeover = ScreenTakeoverController;

        self.syncEnabled = false;
        self.isSyncMaster = false;
        self.syncMasterFailed = false;
        self.syncEvent = null;
        self.syncConfig = null;
        self.takeoverSyncConfig = null;
        var syncStartTimeoutTimer = null;

        self.assetCompleteDone = false;
        self.currentAssetIsVideo = false;
        self.currentScheduledPlaylistMediaAsset = null;
        self.messagePort = messagePort;
        self.playlistRestartTimer = null;
        self.defaultPlayReason = playReason;
        self.isSoftTrigger = false;

        self.feedCache = [];
        
        self.preloadedPlaylistMediaAsset = null;

        var _softTriggerConfig = null;

        self.videoViewer = new VideoPlayer(self.id, self.x, self.y, self.width, self.height, self.viewModel.videoIndex, {
            complete: onVideoComplete,
            fail: onVideoFail,
            playing: onVideoPlaying
        });
        
        self.tvViewer = new TVPlayer(self.id, self.x, self.y, self.width, self.height, {
            playing: onTVPlaying
        });

        self.assetTimerId = 0;
        self.playlistTimerId = 0;
        self.webZoneLoadTimer = 0;
        self.syncFailedTimer = 0;
        self.skipAssetTimer = 0;
        self.assetPreloadTimer = 0;
        self.playlistRestartIntervalTimer = 0;

        self._currentStatus = PlayStatus.STOPPED;
        var _direction = PlayDirection.FORWARDS;

        self.currentPlaylist = null;
        self.currentPlaylistStartTime = null;
        self.currentAssetIndex = -1;
        self.playlistMediaAssetDurationOverride = 0;

        self.currentAssetStartTime = DateTime.now();
        self.nextStartTime = DateTime.now();

        // setup the zone
        // add a cached element for images
        $('#' + this.id).append("<img class='viewer' id='" + self.id + self.imageViewerIdSuffix + "' src='' style='position:absolute;left:0px;top:0px;width:" + this.width + "px;height:" + this.height + "px;visibility:hidden'></img>");
        $('#' + this.id).append("<img class='viewer' id='" + self.id + self.inactiveImageViewerIdSuffix + "' src='' style='position:absolute;left:0px;top:0px;width:" + this.width + "px;height:" + this.height + "px;visibility:hidden'></img>");
        // add a takeover overlay to show/hide
        if($('#' + self.screenTakeoverOverlay).length == 0) {
            $('body').append("<img id='" + self.screenTakeoverOverlay + "' src='' style='position:absolute;left:0px;top:0px;width:" + document.body.clientWidth + "px;height:" + document.body.clientHeight + "px;visibility:hidden; z-index: 9999;'></img>");
        }


        // add two cached elements for web pages
        $('#' + this.id).append("<iframe class='viewer' id='" + this.id + self.activeWebViewerIdSuffix + "' sandbox='allow-same-origin allow-scripts allow-forms allow-presentation' frameborder='0' scrolling='no' src='' style='overflow:hidden;position:absolute;left:0px;top:0px;width:" + this.width + "px;height:" + this.height + "px;visibility:hidden'></iframe>");
        $('#' + this.id).append("<iframe class='viewer' id='" + this.id + self.inactiveWebViewerIdSuffix + "' sandbox='allow-same-origin allow-scripts allow-forms allow-presentation' frameborder='0' scrolling='no' src='' style='overflow:hidden;position:absolute;left:0px;top:0px;width:" + this.width + "px;height:" + this.height + "px;visibility:hidden'></iframe>");

        $('#' + this.id).append(self.videoViewer.html);
        $('#' + this.id).append(self.tvViewer.html);

        // WidgetViewer IFrame
        try {
            self.widgetViewer = new WidgetViewer(self.id, self.x, self.y, self.width, self.height, self.layout.externalId, { ready: onWidgetReady,  moveNext: onWidgetMoveNextRequested, prepareVideo: onPrepareWidgetVideo, stopped: onVideoStopped });
            //$('#' + this.id).append(self.widgetViewer.html);
            self.widgetViewer.appendToCanvas();

        } catch (e) {
            logger.error('Error configuring WidgetViewer: ' + JSON.stringify(e, undefined, 2));
        }

        self.videoViewer.addedToCanvas();
        self.tvViewer.addedToCanvas();

        // event handlers for starting and stopping video
        self.videoStart = null;
        self.videoComplete = null;

        var syncFailWaitTime = 20000;
        var syncStartWaitTime = 60000;

        //Sync
        var sync = null;

        self.enableSync = function (syncConfig, takeoverSyncConfig, clientDetails)
        {
            logger.debug("Enabling Sync");

            if (syncConfig && syncConfig.enabled || takeoverSyncConfig)
            {
                _MediaSyncController = MediaSyncController;

                self.syncConfig = syncConfig;
                if (takeoverSyncConfig) {
                    self.takeoverSyncConfig = takeoverSyncConfig;
                }
                self.syncEnabled = true;

                var syncAddress = (syncConfig) ? syncConfig.configurationParameters.broadcast_address : takeoverSyncConfig.syncSettings.broadcastAddress;
                var syncPort = (syncConfig) ? parseInt(syncConfig.configurationParameters.receive_port) : parseInt(takeoverSyncConfig.syncSettings.udpPort);
                var syncGroupId = (syncConfig) ? syncConfig.groupId : takeoverSyncConfig.id;
                logger.debug("Sync Group: " + syncGroupId + " Sync address: " + syncAddress + ", sync port: " + syncPort);

                if (syncConfig && syncConfig.defaultMaster == clientDetails.signageliveSerialNumber || takeoverSyncConfig && takeoverSyncConfig.master == clientDetails.signageliveSerialNumber)
                {
                    logger.debug("Player is sync master");
                    self.isSyncMaster = true;
                } else 
                {
                    logger.debug("Player is sync slave");
                    self.isSyncMaster = false;
                }

                if(takeoverSyncConfig) {
                    _MediaSyncController.startSync(takeoverSyncConfig, syncGroupId, syncAddress, syncPort, onSyncTakeoverEvent, self.isSyncMaster).then(function(syncDetails) {
                        sync = syncDetails;
                    });
                } else {
                    sync = new BSSyncManager(syncGroupId.toString(), syncAddress, syncPort);
                }

                if (takeoverSyncConfig) {
                    logger.info('Schedule Takeover Sync');

                    if (_MediaSyncController.isSyncRunning()) {
                        logger.info('Sync already Ready! Starting Sync Timer!');
                        if (self.screenTakeover.takeoverInProgress == false) {
                            self.screenTakeover.startSyncTimer().then(function (startPlayback) {
                                if (startPlayback) {
                                    // Send the event so the slaves start playing
                                    logger.info('Telling slaves to start screen takeover');
                                    self.startScreenTakeover();
                                } else {
                                    // retry ... used when the takeover group has changed and we have disconnected from the original master
                                    setTimeout(function () {
                                        self.enableSync(syncConfig, takeoverSyncConfig, clientDetails);
                                    }, 1000);
                                }
                            });
                        }
                    } else {
                        // Start Master
                        if (self.isSyncMaster) {
                            logger.info('Player is master');
                            _MediaSyncController.startMaster(takeoverSyncConfig.id, onSyncTakeoverEvent, false).then(function (result) { // Do not start straight away
                                logger.info('Sync Master Ready! Starting Sync Timer!');
                                if (self.screenTakeover.takeoverInProgress == false) {
                                    self.screenTakeover.startSyncTimer().then(function (startPlayback) {
                                        if (startPlayback) {
                                            // Send the event so the slaves start playing
                                            logger.info('Telling slaves to start screen takeover');
                                            self.startScreenTakeover();
                                        }
                                    });
                                }
                            });
                        }
                        // Start Slave
                        else {
                            _MediaSyncController.startSlave(takeoverSyncConfig.id, onSyncTakeoverEvent).then(function (result) {
                                // TODO: Improve this so that the master tells the slave when to start the sync takeover
                                syncStartTimeoutTimer = setTimeout(syncStartTimedOut, self.secondsToWaitForMasterOnStartup * 1000); //Timeout for the slaves to wait for the master
                            });
                        }
                    }
                }
            
                if(syncConfig) {
                    sync.onsyncevent = onSyncEvent;
                }
            }
        }

        self.scheduleTakeoverSync = function(takeoverSyncConfig, clientDetails) {
            var dfd = $.Deferred();
            if(takeoverSyncConfig) {
                self.enableSync(null, takeoverSyncConfig, clientDetails);
            }
            dfd.resolve();
            return dfd;
        };

        var onSyncTakeoverEvent = function (e) {
            console.log("onSyncTakeoverEvent");
            var takeoverEvent = JSON.parse(e.id);

            //Sync event received so we have started syncing
            if (!self.isSyncMaster && syncStartTimeoutTimer) {
                clearTimeout(syncStartTimeoutTimer);
                syncStartTimeoutTimer = null;
            }

            var assetIndex = parseInt(takeoverEvent.index);
            logger.info('onTakeoverSyncEvent - Sync event received - index is: ' + assetIndex);
            logger.info('onTakeoverSyncEvent - Sync event received - event is: ' + JSON.stringify(takeoverEvent));

            //  is there an issue here where sometimes the slaves do not get their content and instead show a black screen?
            try {
                clearAssetTimer();
                clearSyncFailTimer();

                if (self.syncMasterFailed) {
                    self.syncMasterFailed = false;
                }

                self.syncEvent = e;
                self.screenTakeover.setSyncEvent(e);

                self.sendPlayTakeoverSignal(takeoverEvent);
            } catch (e) {
                console.error(e);
            }
        };

        var onSyncEvent = function(e) {
            clearAssetTimer();
            clearSyncFailTimer();
            var assetIndex = parseInt(e.id);

            logger.debug('Sync event received - index is: ' + assetIndex);

            if ((self.assetCompleteDone == false) && (self.currentAssetIndex != -1)) {
                logger.debug('Asset complete not done, logging proof of play');
                logProofOfPlayOnAssetComplete(self.playReason);

                // send real time events
                if (self.currentScheduledPlaylistMediaAsset) {
                    RealTimeEventsController.sendMediaAssetComplete(self.currentScheduledPlaylistMediaAsset, self.currentAssetStartTime, DateTime.now(), self.playReason, self.playReason);
                }
            }

            if (self.syncMasterFailed) {
                self.syncMasterFailed = false;
            }

            self.syncEvent = e;
            self.syncPlayAsset(assetIndex);
        };

        var onSyncFailed = function() {

            logger.error("Sync failed");
            self.videoViewer.clearSync();
            self.syncMasterFailed = true;
            self.syncFailedTimer = 0;
            self.moveNext(self);
        };

        self.softTrigger = function(softTriggerConfig) {
            if (softTriggerConfig != null && softTriggerConfig.pma != null & softTriggerConfig.pma.mediaAsset != null) {
                logger.info('Soft Trigger - ' + softTriggerConfig.pma.mediaAsset.name);

                _softTriggerConfig = softTriggerConfig;
            }
        };

        // Master starts playing the takeover
        self.playScreenTakeover = function () {
            self.screenTakeover.synchronize({
                type: (self.currentScreenTakeover.type) ? self.currentScreenTakeover.type : "TAKEOVER_START",
                layoutId: self.currentScreenTakeover.layoutId,
                index: (self.currentScreenTakeover.index) ? self.currentScreenTakeover.index : -1
            });
        };
        
        self.stopScreenTakeover = function () {
            self.playingTakeoverSync = false;
            logger.info('Stopping Screen Takeover');
            self.screenTakeover.stop();
        };

        // When the slaves get a takeover event
        self.sendPlayTakeoverSignal = function (e) {
            self.screenTakeover.synchronize({
                type: e.type,
                layoutId: e.layoutId,
                index: e.index
            });
        };
        
        // NOTE: We always start a screen takeover from the first asset in that playlist
        self.startScreenTakeover = function () {
            self.playingTakeoverSync = true;
            logger.info('Playing Screen Takeover');
            self.screenTakeover.setSyncInProgress(true);
            var event = self.screenTakeover.getNextTakeover();
            self.currentScreenTakeover = event.takeover;
            try {
                if (self.isSyncMaster) {
                    self.screenTakeover.sendTakeoverEvent({
                        type: 'TAKEOVER_START',
                        layoutId: self.currentScreenTakeover.layoutId,
                        index: -1
                    });
                }
            } catch (e) {
                logger.warning("Error trying to start screen takeovers");
                self.screenTakeover.setSyncInProgress(false);
            }
            return self.currentScreenTakeover;
        };

        self.moveToAssetMainZone = function (assetIndex) {
            logger.info('Playing Screen Takeover Next Asset: ' + assetIndex);
            self.currentAssetIndex = assetIndex;
            self.moveNext(self);
        };

        self.resumeScheduledContentFromAsset = function (assetIndex, playReason) {
            logger.info('Resuming Scheduled Content From Index ' + assetIndex);
            self.playReason = playReason;
            self.defaultPlayReason = playReason;
            self.currentAssetIndex = (assetIndex >= 0) ? assetIndex : -1;
            self.play();
        };

        var zoneHasDecoder = false;
        var playlistHasPlayedVideo = false;
    
        function canPlayVideo() 
        {
            logger.debug("canPlayVideo called, zoneHasDecoder: " + zoneHasDecoder + ", videos currently playing: " + Settings.application.criticalSections.videosPlaying + ", maxConcurrentVideos: " + Settings.application.features.maxConcurrentVideos);
            var result = true;

            if (zoneHasDecoder)
                return true;

            if (Settings.application.criticalSections.videosPlaying < Settings.application.features.maxConcurrentVideos) 
            {
                //Reserve a video decoder
                Settings.application.criticalSections.videosPlaying++;

                //Remember this
                zoneHasDecoder = true;
                playlistHasPlayedVideo = true;

                result = true;
            } else {
                result = false;
            }

            return result;
        }

        /**
         * Get zone element
         * @return {[type]} [description]
         */
        function zoneScreenElementSelector() {
            return '#' + self.id;
        }

        function screenTakeoverOverlayElementSelector() {
            return '#' + self.screenTakeoverOverlay;
        }

        function hideScreenTakeoverOverlay() {
            setTimeout(function() {
                if($(screenTakeoverOverlayElementSelector()).css('visibility') !== 'hidden') {
                    logger.debug("Hiding Screen Takeover Overlay");
                    $(screenTakeoverOverlayElementSelector()).css('visibility', 'hidden');
                    $(screenTakeoverOverlayElementSelector()).attr('src', ''); //unset the source
                }
            }, 1000); //See if this is still fine for webOS 6.0
        };

        /**
         * Get image viewer
         * @return {[type]} [description]
         */
        function imageViewerElementSelector() {
            return '#' + self.id + self.imageViewerIdSuffix;
        }


        function inActiveImageViewerElementSelector() {
            return '#' + self.id + self.inactiveImageViewerIdSuffix;
        }

        /**
         * Get active Web Viewer
         * @return {[type]} [description]
         */
        function activeWebViewerElementSelector() {
            return '#' + self.id + self.activeWebViewerIdSuffix;
        }

        /**
         * Get Inactive web viewer
         * @return {[type]} [description]
         */
        function inActiveWebViewerElementSelector() {
            return '#' + self.id + self.inactiveWebViewerIdSuffix;
        }

        /**
         * Swap web viewers
         * @return {[type]} [description]
         */
        function swapWebViewers() {
            var temp = self.activeWebViewerIdSuffix;
            self.activeWebViewerIdSuffix = self.inactiveWebViewerIdSuffix;
            self.inactiveWebViewerIdSuffix = temp;
        }

        /**
         * Swap image viewers
         * @return {[type]} [description]
         */
        function swapImageViewers() {
            var temp = self.imageViewerIdSuffix;
            self.imageViewerIdSuffix = self.inactiveImageViewerIdSuffix;
            self.inactiveImageViewerIdSuffix = temp;
        }

        function swapWidgetViewers() {
           /*
            var temp = self.widgetViewer;
            self.widgetViewer = self.secondaryWidgetViewer;
            self.secondaryWidgetViewer = temp;
           */
        }

                
        function onWidgetMoveNextRequested() {
            logger.info('Moving next on request from widget asset');
            clearAssetTimer();
            self.moveNext(self);
        }

        function onPrepareWidgetVideo() {
            self.widgetViewer.playVideo();
        }

        function onVideoStopped() {
            self.widgetViewer.stopVideo();
        }

        /**
         * Gets the next playlist media asset to play taking into account validity
         * @return {[type]} [description]
         */
        function getNextPlaylistMediaAsset(currentTime) {

            var nextPlaylistMediaAsset = null;

            if (self.preloadedPlaylistMediaAsset != null)
            {
                nextPlaylistMediaAsset = self.preloadedPlaylistMediaAsset;
                self.preloadedPlaylistMediaAsset = null;
                return nextPlaylistMediaAsset;
            }         

            // has there been a soft trigger?
            if (_softTriggerConfig != null) {
                // was this sent in the validity zone at the end of the current asset?
                var now = new Date();
                var diff = now - _softTriggerConfig.timeLogged
                if (diff <= _softTriggerConfig.validity) {
                    nextPlaylistMediaAsset = _softTriggerConfig.pma;
                    _softTriggerConfig = null;
                    self.playReason = PlaybackStartEndReasons.INTERRUPT;
                    self.isSoftTrigger = true;
                    return nextPlaylistMediaAsset;
                }
            } else {
                self.playReason = self.defaultPlayReason;
                self.isSoftTrigger = false;
            }

            var nextMediaAssetIndex = self.currentAssetIndex;

            var currentPMA = self.currentPlaylist.playlistMediaAssets[nextMediaAssetIndex];

            if (!currentTime)
                currentTime = DateTime.now();

            //The current asset is a feed, we may need to get the next item from the feed before moving next
            //currentPMA will be null on first run!
            if (currentPMA && currentPMA.feed && currentPMA.isCurrentlyValid(currentTime)) {
                logger.debug('Current media asset is a feed, check for assets in the same grouping');
                logger.debug('Feed: ' + currentPMA.feed.name);
                var nextValidFeedAsset = null;
                var feed = self.feedCache[currentPMA.mediaAssetId].feed;
                var currentGrouping = self.feedCache[currentPMA.mediaAssetId].currentGrouping;

                logger.debug('Assets to Play: ' + feed.assetsToPlay + ", Valid items played: " + self.feedCache[currentPMA.mediaAssetId].validItemsPlayed);
                if (feed.assetsToPlay == 0 || self.feedCache[currentPMA.mediaAssetId].validItemsPlayed < feed.assetsToPlay) {
                    if (_direction == PlayDirection.FORWARDS) {

                        var nextFeedIndex = self.feedCache[currentPMA.mediaAssetId].feedIndex + 1;

                        for (var i = nextFeedIndex; i < feed.feedMediaAssets.length; i++) { //if we hit the end of the feed it is automatically a different group
                            var candidatePlaylistMediaAsset = feed.feedMediaAssets[i];
                            if (candidatePlaylistMediaAsset && candidatePlaylistMediaAsset.isCurrentlyValid(currentTime) && feedAssetIsAvailable(candidatePlaylistMediaAsset)) {
                                //If the grouping changes, we stop;
                                if (currentGrouping != candidatePlaylistMediaAsset.grouping) {
                                    break;
                                }
                                self.feedCache[currentPMA.mediaAssetId].validItemsPlayed++;
                                nextValidFeedAsset = candidatePlaylistMediaAsset;
                                self.feedCache[currentPMA.mediaAssetId].feedIndex = i; //Save feedcache position

                                break;
                            } else if (currentGrouping != candidatePlaylistMediaAsset.grouping) {
                                break;
                            }
                        }
                    } else { //Backwards
                        var nextFeedIndex = self.feedCache[currentPMA.mediaAssetId].feedIndex - 1;
                        for (var i = nextFeedIndex; i > -1; i--) { //if we hit 0 it is automatically a different group
                            var candidatePlaylistMediaAsset = feed.feedMediaAssets[i];
                            if (candidatePlaylistMediaAsset && candidatePlaylistMediaAsset.isCurrentlyValid(currentTime) && feedAssetIsAvailable(candidatePlaylistMediaAsset)) {
                                //If the grouping changes, we stop;
                                if (currentGrouping != candidatePlaylistMediaAsset.grouping) {
                                    break;
                                }
                                self.feedCache[currentPMA.mediaAssetId].validItemsPlayed++;
                                nextValidFeedAsset = candidatePlaylistMediaAsset;
                                self.feedCache[currentPMA.mediaAssetId].feedIndex = i; //Save feedcache position

                                break;
                            } else if (currentGrouping != candidatePlaylistMediaAsset.grouping) {
                                break;
                            }
                        }
                    }
                }

                if (nextValidFeedAsset) {
                    logger.debug('Taking next asset from feed due to grouping: ' + self.currentAssetIndex);
                    if (nextValidFeedAsset.feed.playMode === "random") {
                        self.feedCache[nextValidFeedAsset.feed.mediaAssetId].feedItemAvailable[nextValidFeedAsset.externalId] = false;
                        resetAvailableFeedItemsForFeed(nextValidFeedAsset.feed);
                    }
                    return nextValidFeedAsset;
                }

            }

            logger.debug('Current media asset index: ' + self.currentAssetIndex);
            if (_direction == PlayDirection.FORWARDS) {
                logger.debug('Moving forwards');
                nextMediaAssetIndex = self.currentAssetIndex + 1;
                if (nextMediaAssetIndex >= self.currentPlaylist.playlistMediaAssets.length)
                {
                    nextMediaAssetIndex = 0;
                    
                    //Notify any listeners that playback is complete
                    if(callbacks.playlistComplete != null)
                    {
                        var stopPlayback = callbacks.playlistComplete(zoneIndex);
                        
                        if(stopPlayback)
                        {
                            self.pause();
                            return;
                        }                          
                    } 
                }
                    
            } else {
                logger.debug('Moving backwards');
                nextMediaAssetIndex = self.currentAssetIndex - 1;
                if (nextMediaAssetIndex < 0)
                {
                    nextMediaAssetIndex = self.currentPlaylist.playlistMediaAssets.length - 1;

                    //Notify any listeners that playback is complete
                    if(callbacks.playlistComplete != null)
                    {
                        var stopPlayback = callbacks.playlistComplete(zoneIndex);

                        if(stopPlayback)
                        {
                            self.pause();
                            return;
                        }     
                    } 
                }                
            }

            nextPlaylistMediaAsset = self.currentPlaylist.playlistMediaAssets[nextMediaAssetIndex];
            if (nextPlaylistMediaAsset && nextPlaylistMediaAsset.feed && nextPlaylistMediaAsset.isCurrentlyValid(currentTime)) 
            {
                nextPlaylistMediaAsset = getNextAssetFromFeed(nextPlaylistMediaAsset, currentTime);
            }

            if (nextPlaylistMediaAsset) {
                logger.debug('Current media asset index: ' + self.currentAssetIndex);
                logger.debug('Next media asset index: ' + nextMediaAssetIndex);
                self.currentAssetIndex = nextMediaAssetIndex;
                logger.debug('New current media asset index: ' + self.currentAssetIndex);
            } else {
                logger.debug("Playlist asset was a feed but contained no valid assets - move next");
                self.currentAssetIndex = self.currentAssetIndex + 2;
            }


            // if the next asset in the playlist is not valid - perform a search of the playlist to find the next valid asset.
            if (!nextPlaylistMediaAsset || (nextPlaylistMediaAsset && (nextPlaylistMediaAsset.isCurrentlyValid(currentTime) == false))) {
                logger.debug('Select Playlist media asset is not valid finding another');
                var startIndex = self.currentAssetIndex;

                var nextValidAsset = null;
                if (_direction == PlayDirection.FORWARDS) {
                    // Check from the current asset to the end of the playlist
                    for (var i = startIndex; i < self.currentPlaylist.playlistMediaAssets.length; i++) {
                        var candidatePlaylistMediaAsset = self.currentPlaylist.playlistMediaAssets[i];
                        if (candidatePlaylistMediaAsset && candidatePlaylistMediaAsset.isCurrentlyValid(currentTime)) {
                            // If a feed, get next asset
                            if (candidatePlaylistMediaAsset.feed) {
                                nextValidAsset = getNextAssetFromFeed(candidatePlaylistMediaAsset, currentTime);
                                // getNextAssetFromFeed can return null if there are no valid assets so we need to check we have an asset before
                                // breaking out of this loop
                                if (nextValidAsset) {
                                    self.currentAssetIndex = i;
                                    break;
                                }     
                            } else {
                                // Not a feed - we know this asset is valid so we can set it as the next valid asset and break out of the search
                                nextValidAsset = candidatePlaylistMediaAsset;
                                self.currentAssetIndex = i;                
                                break;
                            }
                        }
                    }
                    // Check from the beginning to the start position, if a valid asset was not found at the end
                    if (nextValidAsset == null) {
                        for (var i = 0; i < startIndex; i++) {
                            var candidatePlaylistMediaAsset = self.currentPlaylist.playlistMediaAssets[i];
                            if (candidatePlaylistMediaAsset.isCurrentlyValid(currentTime)) {

                                // If a feed, get next asset
                                if (candidatePlaylistMediaAsset.feed) {
                                    nextValidAsset = getNextAssetFromFeed(candidatePlaylistMediaAsset, currentTime);
                                    // getNextAssetFromFeed can return null if there are no valid assets so we need to check we have an asset before
                                    // breaking out of this loop
                                    if (nextValidAsset) {
                                        self.currentAssetIndex = i;
                                        break;
                                    }      
                                } else {
                                    // Not a feed - we know this asset is valid so we can set it as the next valid asset and break out of the search
                                    nextValidAsset = candidatePlaylistMediaAsset;
                                    self.currentAssetIndex = i;                 
                                    break;
                                }
                            }
                        }
                    }
                } else {
                    logger.debug('Looking backwards...');
                    // check to the beginning of the playlist from the start position
                    for (var i = startIndex; i >= 0; i--) {
                        var candidatePlaylistMediaAsset = self.currentPlaylist.playlistMediaAssets[i];
                        if (candidatePlaylistMediaAsset.isCurrentlyValid(currentTime)) {
                            // If a feed, get next asset
                            if (candidatePlaylistMediaAsset.feed) {
                                nextValidAsset = getNextAssetFromFeed(candidatePlaylistMediaAsset, currentTime);
                                // getNextAssetFromFeed can return null if there are no valid assets so we need to check we have an asset before
                                // breaking out of this loop
                                if (nextValidAsset) {
                                    self.currentAssetIndex = i;
                                    break;
                                }
                            } else {
                                // Not a feed - we know this asset is valid so we can set it as the next valid asset and break out of the search
                                nextValidAsset = candidatePlaylistMediaAsset;
                                self.currentAssetIndex = i;
                                break;
                            }
                        }
                    }

                    if (nextValidAsset == null) {
                        // Check from the current asset to the end of the playlist
                        for (var i = self.currentPlaylist.playlistMediaAssets.length - 1; i > startIndex; i--) {
                            var candidatePlaylistMediaAsset = self.currentPlaylist.playlistMediaAssets[i];
                            if (candidatePlaylistMediaAsset.isCurrentlyValid(currentTime)) {
                                // If a feed, get next asset
                                if (candidatePlaylistMediaAsset.feed) {
                                    nextValidAsset = getNextAssetFromFeed(candidatePlaylistMediaAsset, currentTime);
                                    // getNextAssetFromFeed can return null if there are no valid assets so we need to check we have an asset before
                                    // breaking out of this loop
                                    if (nextValidAsset) {
                                        self.currentAssetIndex = i;
                                        break;
                                    }
                                } else {
                                    // Not a feed - we know this asset is valid so we can set it as the next valid asset and break out of the search
                                    nextValidAsset = candidatePlaylistMediaAsset;
                                    self.currentAssetIndex = i;
                                    break;
                                }
                            }
                        }
                    }
                }
                logger.debug('Rolled forwards - current media asset index: ' + self.currentAssetIndex);
                nextPlaylistMediaAsset = nextValidAsset;
            }

            if (nextPlaylistMediaAsset && nextPlaylistMediaAsset.feed && nextPlaylistMediaAsset.feed.playMode === "random") {
                self.feedCache[nextPlaylistMediaAsset.feed.mediaAssetId].feedItemAvailable[nextPlaylistMediaAsset.externalId] = false;
                resetAvailableFeedItemsForFeed(nextPlaylistMediaAsset.feed);
            }            

            return nextPlaylistMediaAsset;
        }

                // Determines if we are at the end of the playlist
        // Utilizes a fail fast approach to exit out as soon as we have an answer
        function getNextAssetIndexAndCheckIfPlaylistIsComplete() {
            var nextPlaylistMediaAsset = null;
            var nextMediaAssetIndex = self.currentAssetIndex;            
            var currentTime = DateTime.now();  
            
            if (self.currentPlaylist == null) {
                return {
                    isComplete: true,
                    nextAssetIndex: 0
                };
            }

            var currentPMA = self.currentPlaylist.playlistMediaAssets[nextMediaAssetIndex];

            if (currentPMA && currentPMA.feed && currentPMA.isCurrentlyValid(currentTime)) {
                logger.debug('Current media asset is a feed, check for assets in the same grouping');
                logger.debug('Feed: ' + currentPMA.feed.name);
                var nextValidFeedAsset = null;
                var feed = self.feedCache[currentPMA.mediaAssetId].feed;
                var currentGrouping = self.feedCache[currentPMA.mediaAssetId].currentGrouping;

                logger.debug('Assets to Play: ' + feed.assetsToPlay + ", Valid items played: " + self.feedCache[currentPMA.mediaAssetId].validItemsPlayed);
                if (feed.assetsToPlay == 0 || self.feedCache[currentPMA.mediaAssetId].validItemsPlayed < feed.assetsToPlay) {
                    if (_direction == PlayDirection.FORWARDS) {

                        var nextFeedIndex = self.feedCache[currentPMA.mediaAssetId].feedIndex + 1;

                        for (var i = nextFeedIndex; i < feed.feedMediaAssets.length; i++) { //if we hit the end of the feed it is automatically a different group
                            var candidatePlaylistMediaAsset = feed.feedMediaAssets[i];
                            if (candidatePlaylistMediaAsset.isCurrentlyValid(currentTime)) {
                                //If the grouping changes, we stop;
                                if (currentGrouping != candidatePlaylistMediaAsset.grouping) {
                                    break;
                                }
                                nextValidFeedAsset = candidatePlaylistMediaAsset;

                                break;
                            } else if (currentGrouping != candidatePlaylistMediaAsset.grouping) {
                                break;
                            }
                        }
                    } else { //Backwards
                        var nextFeedIndex = self.feedCache[currentPMA.mediaAssetId].feedIndex - 1;
                        for (var i = nextFeedIndex; i > -1; i--) { //if we hit 0 it is automatically a different group
                            var candidatePlaylistMediaAsset = feed.feedMediaAssets[i];
                            if (candidatePlaylistMediaAsset.isCurrentlyValid(currentTime)) {
                                //If the grouping changes, we stop;
                                if (currentGrouping != candidatePlaylistMediaAsset.grouping) {
                                    break;
                                }
                                nextValidFeedAsset = candidatePlaylistMediaAsset;

                                break;
                            } else if (currentGrouping != candidatePlaylistMediaAsset.grouping) {
                                break;
                            }
                        }
                    }
                }

                // This is a feed and the a feed item to play was found so the playlist has not finished
                if (nextValidFeedAsset) {
                    return {
                        isComplete: false,
                        nextAssetIndex: nextMediaAssetIndex
                    };
                }
            }

            var nextMediaAssetIndex = self.currentAssetIndex;
            logger.debug('Current media asset index: ' + self.currentAssetIndex);
            if (_direction == PlayDirection.FORWARDS) {
                logger.debug('Moving forwards');
                nextMediaAssetIndex = self.currentAssetIndex + 1;
                // If we are at the end of the playlist just return true
                if (nextMediaAssetIndex >= self.currentPlaylist.playlistMediaAssets.length) {
                    return {
                        isComplete: true,
                        nextAssetIndex: 0
                    };
                }

            } else {
                logger.debug('Moving backwards');
                nextMediaAssetIndex = self.currentAssetIndex - 1;
                // If we are at the end of the playlist just return true
                if (nextMediaAssetIndex < 0) {
                    return {
                        isComplete: true,
                        nextAssetIndex: self.currentPlaylist.playlistMediaAssets.length - 1
                    };
                } 
            }

            // Get the next PMA
            nextPlaylistMediaAsset = self.currentPlaylist.playlistMediaAssets[nextMediaAssetIndex];
            // If it is valid then the playlist has not finished
            if (nextPlaylistMediaAsset && nextPlaylistMediaAsset.isCurrentlyValid() == true) {
                return {
                    isComplete: false,
                    nextAssetIndex: nextMediaAssetIndex
                };
            }
            else
            {
                // if the next asset in the playlist is not valid - perform a search of the playlist to find the next valid asset.
                logger.debug('Select Playlist media asset is not valid finding another');
                var startIndex = nextMediaAssetIndex;

                if (_direction == PlayDirection.FORWARDS) {
                    // Check from the current asset to the end of the playlist
                    for (var i = startIndex; i < self.currentPlaylist.playlistMediaAssets.length; i++) {
                        // Found a valid asset further along - Playlist has not finished
                        var candidatePlaylistMediaAsset = self.currentPlaylist.playlistMediaAssets[i];
                        if (candidatePlaylistMediaAsset.isCurrentlyValid()) {
                            return {
                                isComplete: false,
                                nextAssetIndex: i
                            };
                        }
                    }

                    // No valid assets found so the playlist has finished
                    return {
                        isComplete: true,
                        nextAssetIndex: 0
                    };

                } else {
                    logger.debug('Looking backwards...');
                    // check to the beginning of the playlist from the start position

                    for (var i = startIndex; i >= 0; i--) {
                        var candidatePlaylistMediaAsset = self.currentPlaylist.playlistMediaAssets[i];
                        if (candidatePlaylistMediaAsset.isCurrentlyValid()) {
                            return {
                                isComplete: false,
                                nextAssetIndex: i
                            };
                        }
                    }

                    // No valid assets found so the playlist has finished
                    return {
                        isComplete: true,
                        nextAssetIndex: 0
                    };
                }
            }
        }

        function getNextAssetFromFeed(currentPMA, currentTime) {
            logger.debug('Getting next asset from feed');
            var nextValidAsset = null;

            var feed = self.feedCache[currentPMA.mediaAssetId].feed;
            var currentGrouping = self.feedCache[currentPMA.mediaAssetId].currentGrouping;
            var currentFeedIndex = self.feedCache[currentPMA.mediaAssetId].feedIndex;

            if (!currentTime)
                currentTime = DateTime.now()

            if (_direction == PlayDirection.FORWARDS) {
                logger.debug('Current feed index: ' + currentFeedIndex);
                var nextFeedIndex = currentFeedIndex + 1;
                logger.debug('Assets to Play: ' + feed.assetsToPlay + ", Valid items played: " + self.feedCache[currentPMA.mediaAssetId].validItemsPlayed);
                //If we have a max number of assets to play, check we have not exceeded it
                if (feed.assetsToPlay == 0 || self.feedCache[currentPMA.mediaAssetId].validItemsPlayed < feed.assetsToPlay) {
                    for (var i = nextFeedIndex; i < feed.feedMediaAssets.length; i++) {
                        logger.debug('Checking asset at feed index: ' + i);
                        var candidatePlaylistMediaAsset = feed.feedMediaAssets[i];
                        if (candidatePlaylistMediaAsset.isCurrentlyValid(currentTime)) {
                            //Increment the valid items played so we can stop if we have limited it
                            self.feedCache[currentPMA.mediaAssetId].validItemsPlayed++;
                            nextValidAsset = candidatePlaylistMediaAsset;
                            currentFeedIndex = i;
                            logger.debug('Setting current feed index to: ' + currentFeedIndex);
                            break;
                        }
                    }
                }

                if (!nextValidAsset) {
                    self.feedCache[currentPMA.mediaAssetId].validItemsPlayed = 0;

                    if (feed.playMode === "random") {
                        feed.randomize();
                        currentFeedIndex = feed.feedMediaAssets.length;
                    } 

                    //Start again at 0 and continue until we get to the index we started at
                    for (var i = 0; i <= currentFeedIndex; i++) {
                        logger.debug('Checking asset at feed index: ' + i);
                        var candidatePlaylistMediaAsset = feed.feedMediaAssets[i];
                        if (candidatePlaylistMediaAsset && candidatePlaylistMediaAsset.isCurrentlyValid(currentTime) && feedAssetIsAvailable(candidatePlaylistMediaAsset)) {
                            self.feedCache[currentPMA.mediaAssetId].validItemsPlayed = 1;
                            nextValidAsset = candidatePlaylistMediaAsset;
                            currentFeedIndex = i;
                            break;
                        }
                    }
                }

            } else //Backwards
            {
                var nextFeedIndex = currentFeedIndex - 1;
                //If we have a max number of assets to play, check we have not exceeded it
                if (feed.assetsToPlay == 0 || self.feedCache[currentPMA.mediaAssetId].validItemsPlayed < feed.assetsToPlay) {
                    for (var i = nextFeedIndex; i >= 0; i--) {
                        var candidatePlaylistMediaAsset = feed.feedMediaAssets[i];
                        if (candidatePlaylistMediaAsset.isCurrentlyValid(currentTime) && feedAssetIsAvailable(candidatePlaylistMediaAsset)) {
                            //Increment the valid items played so we can stop if we have limited it
                            self.feedCache[currentPMA.mediaAssetId].validItemsPlayed++;
                            nextValidAsset = candidatePlaylistMediaAsset;
                            currentFeedIndex = i;
                            break;
                        }
                    }
                }

                if (!nextValidAsset) {
                    self.feedCache[currentPMA.mediaAssetId].validItemsPlayed = 0;

                    if (feed.playMode === "random") {
                        feed.randomize();
                        currentFeedIndex = feed.feedMediaAssets.length;
                    }                    

                    //Start again at end of feed and continue until we get to the index we started at
                    for (var i = feed.feedMediaAssets.length - 1; i >= currentFeedIndex; i--) {
                        var candidatePlaylistMediaAsset = feed.feedMediaAssets[i];
                        if (candidatePlaylistMediaAsset && candidatePlaylistMediaAsset.isCurrentlyValid(currentTime)) {
                            self.feedCache[currentPMA.mediaAssetId].validItemsPlayed = 1;
                            nextValidAsset = candidatePlaylistMediaAsset;
                            currentFeedIndex = i;
                            break;
                        }
                    }
                }
            }

            //If we found a valid asset, save the positiong and return
            if (nextValidAsset) {
                logger.debug('Saving current feed index on feed in cache: ' + currentFeedIndex);
                self.feedCache[currentPMA.mediaAssetId].feedIndex = currentFeedIndex;
                self.feedCache[currentPMA.mediaAssetId].currentGrouping = nextValidAsset.grouping;

                //To-do - shuffle assets if index is 0 again and playmode is random!
            }

            return nextValidAsset;
        }

        /**
         * Callback on video complete
         * @return {[type]} [description]
         */
        function onVideoComplete() {
            logger.debug('Video Complete');
            self.assetCompleteDone = true;
            
            // NOTE: Only Log Proof of Play in regards to players not currently in a takeover with a takeover configuration or those that do not have a screen takeover configuration
            if(!self.screenTakeover || (self.screenTakeover && !self.screenTakeover.takeoverInProgress)) {
                // Log PoP
                logProofOfPlayOnAssetComplete(self.playReason);
            }

            // send real time events

            var pma = null;
            if(self.currentScheduledPlaylistMediaAsset)
                pma = self.currentScheduledPlaylistMediaAsset;
            else
            {
                pma = { mediaAsset: self.viewModel.defaultMediaAsset };
            }

            RealTimeEventsController.sendMediaAssetComplete(pma, self.currentAssetStartTime, DateTime.now(), self.playReason, self.playReason);

            // clear the time out here in case the video/stream is shorter than the duration specfied in the playlist
            clearAssetTimer();

            var mediaAsset = pma.mediaAsset;

            logger.debug("assetComplete - Callback ID: " + self.id + " details are: " + self.currentAssetIndex + " and " + mediaAsset.name);

            // If this is a screentakeover and this is the master we need to check if this is the end of the playlist. This is not important for slaves.
            var playlistCheckResult = {
                isComplete: false,
                nextAssetIndex: 0
            };

            if (self.screenTakeover.takeoverInProgress && self.screenTakeover.isMaster) {
                playlistCheckResult = getNextAssetIndexAndCheckIfPlaylistIsComplete();
            }

            logger.debug("assetComplete - Callback ID: " + self.id + " details are: " + self.currentAssetIndex + " and " + mediaAsset.name);
            // Trigger playback (?)
            $("#" + self.id).trigger("zone_asset_complete", {
                index: self.currentAssetIndex,
                name: mediaAsset.name,
                contentType: mediaAsset.contentType,
                playbackMethod: "standardPlayback",
                playlistEnded: playlistCheckResult.isComplete,
                nextAssetIndex: playlistCheckResult.nextAssetIndex
            });
            
            if (self.syncEnabled && !self.syncMasterFailed && !self.screenTakeover.running) {
                logger.debug("Sync is enabled");

                if (self.isSyncMaster) {
                    logger.debug("Player is master - call playNextAssetSync()");
                    playNextAssetSync();
                } else {
                    //Do nothing - wait for sync event
                    logger.debug("Player is slave - wait for sync event");

                    //Set the timeout for sync failing
                    //self.syncFailedTimer = setTimeout(onSyncFailed, syncFailWaitTime);
                }
            } else if (self.screenTakeover.running && self.screenTakeover.takeoverInProgress && !self.syncMasterFailed) {  //issue with self.takeoverSyncConfig being null
                logger.debug("Sync is enabled");
                if (self.screenTakeover.isMaster) {  //SyncMaster was getting set to false (?)
                    logger.debug("Player is master - wait for zone_asset_complete CB");
                } else {
                    logger.debug("Player is slave - wait for sync event");
                }
            } else {
                logger.debug("Not synced, move next normally");
                self.moveNext(self);
            }
        }

        /**
         * Callback on video failure
         * @return {[type]} [description]
         */
        function onVideoFail() {
            logger.debug('Video fail');

            // remember this asset has failed, but only if we are not syncing - check when in sync group
            if (self.syncEnabled == false && self.screenTakeover.running == false) {
                self.currentScheduledPlaylistMediaAsset.mediaAsset.hasFailed = true;
            }

            //This counts as asset complete as we don't log proof of play if an asset fails, we just want to move next.
            self.assetCompleteDone = true;

            var mediaAsset = self.currentScheduledPlaylistMediaAsset.mediaAsset;

            logger.debug("assetComplete - Callback ID: " + self.id + " details are: " + self.currentAssetIndex + " and " + mediaAsset.name);

            // If this is a screentakeover and this is the master we need to check if this is the end of the playlist. This is not important for slaves.
            var playlistCheckResult = {
                isComplete: false,
                nextAssetIndex: 0
            };

            if (self.screenTakeover.takeoverInProgress && self.screenTakeover.isMaster) {
                playlistCheckResult = getNextAssetIndexAndCheckIfPlaylistIsComplete();
            }
            
            // Trigger playback (?)
            $("#" + self.id).trigger("zone_asset_complete", {
                index: self.currentAssetIndex,
                name: mediaAsset.name,
                contentType: mediaAsset.contentType,
                playbackMethod: "standardPlayback",
                playlistEnded: playlistCheckResult.isComplete,
                nextAssetIndex: playlistCheckResult.nextAssetIndex
            });

            clearAssetTimer();

            //If sync we need to do something else
            if (self.syncEnabled && !self.syncMasterFailed && !self.screenTakeover.running) {
                if (self.isSyncMaster) {
                    logger.debug("Player is master - call playNextAssetSync()");
                    playNextAssetSync();
                } else {
                    //Do nothing - wait for sync event
                    logger.debug("Player is slave - wait for sync event");

                    //Set the timeout for sync failing
                    //self.syncFailedTimer = setTimeout(onSyncFailed, syncFailWaitTime);
                }
            } else if (self.screenTakeover.running && self.screenTakeover.takeoverInProgress && !self.syncMasterFailed) {  //issue with self.takeoverSyncConfig being null
                logger.debug("Sync is enabled");
                if (self.screenTakeover.isMaster) {  //SyncMaster was getting set to false (?)
                    logger.debug("Player is master - wait for zone_asset_complete CB");
                } else {
                    logger.debug("Player is slave - wait for sync event");
                }
            } else {
                self.moveNext(self);
            }


        }

        function onVideoPlaying()
        {
            setTimeout(function(){
                self.tvViewer.hide();
                self.widgetViewer.hide();
                $(zoneScreenElementSelector()).children(inActiveImageViewerElementSelector()).css('visibility', 'hidden');
                $(zoneScreenElementSelector()).children(imageViewerElementSelector()).css('visibility', 'hidden');
                try {
                    var mediaAsset = self.currentScheduledPlaylistMediaAsset.mediaAsset;
                    logger.debug(`MVZ ${self.guid}: Asset Playing: Callback ID: ${self.id} details are: ${self.currentAssetIndex}  and ${mediaAsset.name}`);
                    $("#" + self.id).trigger("zone_asset_playing", {
                        index: self.currentAssetIndex,
                        name: mediaAsset.name,
                        contentType: mediaAsset.contentType
                    });
                } catch (e) {};    
            },175);
        }

        function onTVPlaying()
        {
            //setTimeout(function(){
                self.videoViewer.hide();
                self.widgetViewer.hide();
                $(zoneScreenElementSelector()).children(inActiveImageViewerElementSelector()).css('visibility', 'hidden');
                $(zoneScreenElementSelector()).children(imageViewerElementSelector()).css('visibility', 'hidden');
                            
            //},175);
        }

        function playWeb(mediaAsset,duration,xframeoptions)
        {
            // Hide the image viewer
            $(zoneScreenElementSelector()).children(imageViewerElementSelector()).css('visibility', 'hidden');
            $(zoneScreenElementSelector()).children(inActiveImageViewerElementSelector()).css('visibility', 'hidden');

            //Clear video in case we will be showing a video
            if(!Settings.application.features.supportsSeamlessTransitions)
            {
                self.tvViewer.hide();
                self.videoViewer.hide();
                self.widgetViewer.hide();
            }

            //xframeoptions = true;
            if (xframeoptions) {
                stopWebViewers();

                self.tvViewer.hide();
                self.videoViewer.hide();
                self.widgetViewer.hide();

                self.messagePort.PostBSMessage({ action: "create_HTML_Widget", index: zoneIndex, url: mediaAsset.remoteUrl, RecX: self.x, RecY: self.y, RecWidth: self.width, RecHeight: self.height, Rotate: UserConfig.rotationAngle() });
                
                // set the timer
                setAssetPlayTimer(duration);

                logger.debug(`MVZ ${self.guid}: Asset Playing: Callback ID: ${self.id} details are: ${self.currentAssetIndex}  and ${mediaAsset.name}`);
                $("#" + self.id).trigger("zone_asset_playing", {
                    index: self.currentAssetIndex,
                    name: mediaAsset.name,
                    contentType: mediaAsset.contentType
                });

                if (self.currentScheduledPlaylistMediaAsset != null)
                    RealTimeEventsController.sendMediaAssetStart(self.currentScheduledPlaylistMediaAsset, DateTime.now());
            } else {
                hideHtmlWidget();
                var loaded = false;
                var isCancelled = false;

                $(zoneScreenElementSelector()).children(inActiveWebViewerElementSelector()).off('load').on('load', function() {
                    var $this = $(this);                 

                    if (isCancelled == false && $this.attr('src') != 'about:blank') {

                        loaded = true;

                        if(Settings.application.features.supportsSeamlessTransitions)
                        {
                            setTimeout(function(){
                                self.tvViewer.hide();
                                self.videoViewer.hide();
                                self.widgetViewer.hide();
                            },150);                            
                        }

                        $(zoneScreenElementSelector()).children(inActiveWebViewerElementSelector()).off('load'); // Once loaded stop listening for loaded event

                        // clear the timeout timer
                        clearWebContentTimeoutTimer();

                        // hide the other web control
                        $(zoneScreenElementSelector()).children(activeWebViewerElementSelector()).css('visibility', 'hidden');
                        $(zoneScreenElementSelector()).children(activeWebViewerElementSelector()).attr('src', 'about:blank');

                        // hide the image and video controls
                        $(zoneScreenElementSelector()).children(imageViewerElementSelector()).css('visibility', 'hidden');

                        // hide the other web control
                        $(zoneScreenElementSelector()).children(activeWebViewerElementSelector()).css('visibility', 'hidden');
                        $(zoneScreenElementSelector()).children(activeWebViewerElementSelector()).attr('src', 'about:blank');

                        // Finally show the new iframe
                        $(zoneScreenElementSelector()).children(inActiveWebViewerElementSelector()).css('visibility', 'visible');

                        hideScreenTakeoverOverlay();

                        // swap the references to the viewers
                        swapWebViewers();

                        // set the timer
                        setAssetPlayTimer(duration);

                        logger.debug(`MVZ ${self.guid}: Asset Playing: Callback ID: ${self.id} details are: ${self.currentAssetIndex}  and ${mediaAsset.name}`);
                        $("#" + self.id).trigger("zone_asset_playing", {
                            index: self.currentAssetIndex,
                            name: mediaAsset.name,
                            contentType: mediaAsset.contentType
                        });

                        if (self.currentScheduledPlaylistMediaAsset != null)
                            RealTimeEventsController.sendMediaAssetStart(self.currentScheduledPlaylistMediaAsset, DateTime.now());
                    }
                });

                // set the url
                if ($(zoneScreenElementSelector()).children(inActiveWebViewerElementSelector()).attr('src') !== mediaAsset.remoteUrl) {

                    /// handle assets that don't load
                    self.webZoneLoadTimer = setTimeout(function() {
                        if (!loaded) {

                            logger.info('Web Zone load timed out');
                            try {
                                // prevent the asset from loading
                                isCancelled = true;

                                // remove the broken web viewer - I'm not sure why this is required however if the iframe is left in the document it is not usable again (this does not happen in chrome or FF)
                                // by removing it and adding one stright back in all of the content plays as expected
                                $(zoneScreenElementSelector()).children(inActiveWebViewerElementSelector()).css('visibility', 'hidden');
                                $(zoneScreenElementSelector()).children(inActiveWebViewerElementSelector()).attr('src', 'about:blank');
                                $(zoneScreenElementSelector()).children(inActiveWebViewerElementSelector()).remove();

                                // add another - clean viewer
                                $('#' + self.id).append("<iframe class='viewer' id='" + self.id + self.inactiveWebViewerIdSuffix + "' sandbox='allow-same-origin allow-scripts allow-forms allow-presentation' frameborder='0' scrolling='no' src='' style='overflow:hiiden;position:absolute;left:0px;top:0px;width:" + self.width + "px;height:" + self.height + "px;visibility:hidden'></iframe>");

                                // swap the references to the viewers
                                swapWebViewers();
                            } catch (err) {
                                logger.error("Error on web zone load time out: " + JSON.stringify(e));
                            }
                            //cancelled - move to the next asset
                            assetComplete(self);
                        }
                    }, 10000);
                    $(zoneScreenElementSelector()).children(inActiveWebViewerElementSelector()).attr('src', self.buildWebWidgetURL(mediaAsset.remoteUrl));
                }
            }
        }

         

        /**
         * Main method that handles playback of different media asset types
         * @param  {MediaAssetModel} mediaAsset [description]
         * @param  {number} duration   [description]
         * @return {[type]}            [description]
         */
        self.playAsset = function(mediaAsset, duration) {
            logger.debug('Play Asset ' + mediaAsset.name);
            var assetType = mediaAsset.contentType;
            var previousAssetIsVideo = self.currentAssetIsVideo;
            self.currentAssetIsVideo = false;
            
            var muted = UserConfig.isMuted();

            var assetStartTime = DateTime.now();

            // Trigger playback (?)
            logger.debug("playAsset - Callback ID: " + self.id + " details are: " + self.currentAssetIndex + " and " + mediaAsset.name);
            $("#" + self.id).trigger("zone_asset_start", {
                index: self.currentAssetIndex,
                name: mediaAsset.name,
                contentType: mediaAsset.contentType
            });

            //If start on hour we need to ensure we only play assets for the alloted time
            if (self.currentPlaylist && self.currentPlaylist.startOnHour && !self.playlistMediaAssetDurationOverride) {
                //logger.debug("Calculated start time of this asset:" + self.nextStartTime.toLocaleString());

                if (duration == 0)
                    duration = mediaAsset.duration;

                //Time the asset should end:
                var endTime = new Date(self.nextStartTime.getTime() + duration);
                //Reset the millis to 0
                endTime.setSeconds(endTime.getSeconds(), 0);
                //logger.debug("We want this asset to end at:" + endTime.toLocaleString());
                self.nextStartTime = endTime;

                var calculatedDuration = endTime.getTime() - DateTime.now().getTime();
                var maxDuration = duration + 1000;

                if (calculatedDuration > maxDuration)
                    logger.error("DURATION IS TOO LONG. SOMETHING WENT WRONG. Calculated duration: " + calculatedDuration);

                //Set duration
                duration = calculatedDuration;
            }

            switch (assetType) {
                case MediaTypeConstants.IMAGE:

                    //if(previousAssetIsVideo && self.videoComplete != null)
                    //   self.videoComplete(self);

                    // remove event handlers
                    $(zoneScreenElementSelector()).children(imageViewerElementSelector()).off();

                    // chain the load handler
                    $(zoneScreenElementSelector()).children(imageViewerElementSelector()).on('load', function(eventData) {

                        logger.debug('Image loaded: ' + eventData.currentTarget.src);

                        showImageControl(duration);
                        $(zoneScreenElementSelector()).children(inActiveImageViewerElementSelector()).css('visibility', 'hidden');
                        swapImageViewers();

                        logger.debug(`MVZ ${self.guid}: Asset Playing: Callback ID: ${self.id} details are: ${self.currentAssetIndex}  and ${mediaAsset.name}`);
                        $("#" + self.id).trigger("zone_asset_playing", {
                            index: self.currentAssetIndex,
                            name: mediaAsset.name,
                            contentType: mediaAsset.contentType
                        });
                    });

                    $(zoneScreenElementSelector()).children(imageViewerElementSelector()).on('error', function() {
                        logger.error('Error loading image');
                        if (self.screenTakeover.running == false)
                            self.currentScheduledPlaylistMediaAsset.mediaAsset.hasFailed = true;

                        self.skipAssetTimer = setTimeout(function() {
                            self.skipAsset();
                        }, 50);
                    });

                    PlayUrlHelper.getMediaAssetPlayUrl(mediaAsset).done(function(url) {
                        if ($(zoneScreenElementSelector()).children(imageViewerElementSelector()).attr('src') == url) {
                            logger.debug('Url the same' + url);
                            showImageControl(duration);
                        } else {
                            logger.debug('Setting image to ' + url);
                            $(zoneScreenElementSelector()).children(imageViewerElementSelector()).attr('src', url);
                        }
                    });
                    break;
                case MediaTypeConstants.WEB:
                    var alwaysEmbedWebpages = UserConfig.alwaysEmbedWebpages().enabled;

                    //Check for lower power devices here
                    if(!Settings.application.features.supportsSeamlessTransitions && !UserConfig.gaplessWidgetTransitions()) {
                        self.tvViewer.clearVideo();
                        self.videoViewer.clearVideo();
                        self.widgetViewer.hide();
                    }

                    if (UserConfig.interactiveKiosk().enabled && playReason == PlaybackStartEndReasons.INTERRUPT) {
                        if (self.currentPlaylist.playlistMediaAssets.length == 1 && self.playInterruptIndefinitely) {
                            duration = 86400000; //1 Day
                        }
                        logger.debug("Playlist length: " + self.currentPlaylist.playlistMediaAssets.length + " Playindefinitely: " + self.playInterruptIndefinitely + " Duration: " + duration);
                        playWeb(mediaAsset, duration, !alwaysEmbedWebpages);
                    } else if (alwaysEmbedWebpages) {
                        playWeb(mediaAsset, duration, false);
                    } else if (mediaAsset.xframechecked) { //if we have already checked for xframe options we don't need to do it again
                        playWeb(mediaAsset, duration, mediaAsset.xframeoptions);
                    } else {
                        UrlSandboxUnsupportedHelper.isSandboxSupported(mediaAsset.remoteUrl).done(function(result)
                        {
                            if(result)
                            {
                                $.ajax({
                                    traditional: true,
                                    url: mediaAsset.remoteUrl,
                                    type: 'HEAD',
                                    async: true
                                }).done(function(message, text, jqXHR) {
                                    var xframeoptions = jqXHR.getResponseHeader('X-Frame-Options');
                                    mediaAsset.xframechecked = true;
                                    mediaAsset.xframeoptions = xframeoptions;
        
                                    //Check for a content security policy
                                    if (!xframeoptions) {
                                        var csp = jqXHR.getResponseHeader('content-security-policy');
                                        logger.debug("Web asset: content-security-policy: " + csp);
        
                                        if (csp)
                                            mediaAsset.xframeoptions = true;
                                    }
        
                                    logger.debug("Web asset: Play in external widget: " + mediaAsset.xframeoptions);
        
                                    playWeb(mediaAsset, duration, mediaAsset.xframeoptions);
        
                                }).fail(function() {
                                    //Web page appears to have disallowed HEAD requests
                                    mediaAsset.xframechecked = true;
                                    mediaAsset.xframeoptions = true;
                                    playWeb(mediaAsset, duration, true);
                                });

                            } else {
                                logger.debug("Web asset (sandbox not supported): Play in external widget: " + mediaAsset.xframeoptions);
                                //Sandbox is not supported - treat as xframe options
                                mediaAsset.xframechecked = true;
                                mediaAsset.xframeoptions = true;
                                playWeb(mediaAsset, duration, mediaAsset.xframeoptions);
                            }
                        });    
                    }

                    break;
                    // WIDGET
                case MediaTypeConstants.WIDGET:

                    logger.debug("Loading Widget");

                    var loaded = false;
                    var isCancelled = false;

                    if (UserConfig.interactiveKiosk().enabled && playReason == PlaybackStartEndReasons.INTERRUPT && self.currentPlaylist.playlistMediaAssets.length == 1 && self.playInterruptIndefinitely) {
                           //For an indefinite kiosk interrupt, no refresh
                        duration = 86400000; //1 Day 
                    }

                    //Check for lower power devices here
                    if(!Settings.application.features.supportsSeamlessTransitions && !UserConfig.gaplessWidgetTransitions())
                    {
                        self.videoViewer.clearVideo();
                        self.widgetViewer.hide();
                    }
                     //HDMI must be unloaded before playing video to avoid audio equalizer issues;
                    self.tvViewer.clearVideo();

                    getWidgetStartFile(mediaAsset).done(function(potentialStartFile) {
                        playWidget(potentialStartFile, duration, mediaAsset);
                    }).fail(function() {
                        if (self.screenTakeover.running == false)
                            self.currentScheduledPlaylistMediaAsset.mediaAsset.hasFailed = true;

                        isCancelled = true;

                        self.skipAssetTimer = setTimeout(function() {
                            self.skipAsset();
                        }, 50);
                    });

            
                    break;
                case MediaTypeConstants.CAPTURE:             

                    if(Settings.application.features.canDisplayVideoInput)
                    {
                        if (canPlayVideo()) {

                            self.currentAssetIsVideo = true;
                            stopWebViewers();

                            if(!Settings.application.features.supportsSeamlessTransitions && !UserConfig.gaplessWidgetTransitions())
                            {
                                self.widgetViewer.hide();
                            }

                            hideScreenTakeoverOverlay();
                            //Videos must be unloaded before playing HDMI to avoid audio equalizer issues;
                            self.videoViewer.hide();
                            
                            // attempt to play video (this will initialize it and automatically start playback)
                            self.tvViewer.play();

                            self.tvViewer.setMute(muted);

                            if (self.videoStart != null) {
                                self.videoStart(self);
                            }

                            // so we should set the timer (add half a second for initialisation)
                            //setAssetPlayTimer(duration);
                            setTVAssetPlayTimer(duration);

                            logger.debug(`MVZ ${self.guid}: Asset Playing: Callback ID: ${self.id} details are: ${self.currentAssetIndex}  and ${mediaAsset.name}`);
                            $("#" + self.id).trigger("zone_asset_playing", {
                                index: self.currentAssetIndex,
                                name: mediaAsset.name,
                                contentType: mediaAsset.contentType
                            });

                            if (self.currentScheduledPlaylistMediaAsset != null)
                                RealTimeEventsController.sendMediaAssetStart(self.currentScheduledPlaylistMediaAsset, DateTime.now());
                        } else {
                            logger.debug("Not enough free video decoders available, skipping video");
                            //Too many concurrent videos playing
                            self.skipAssetTimer = setTimeout(function() {
                                self.skipAsset();
                            }, 50);
                        }
                    } else{
                        logger.debug("Video input not supported");
                        self.currentScheduledPlaylistMediaAsset.mediaAsset.hasFailed = true;
                        
                        self.skipAssetTimer = setTimeout(function() {
                            self.skipAsset();
                        }, 50);
                    }

                    break;
                case MediaTypeConstants.IPTV:
                case MediaTypeConstants.VIDEO:
                    if (canPlayVideo()) {
                        self.currentAssetIsVideo = true;
                        // hide all children
                        stopWebViewers();

                        //Remove decoder from video viewer
                        if(!Settings.application.features.supportsSeamlessTransitions && !UserConfig.gaplessWidgetTransitions())
                        {
                            self.widgetViewer.hide();
                        } 
                        //HDMI must be unloaded before playing video to avoid audio equalizer issues;
                        self.tvViewer.hide();

                        // Play video
                        if (self.syncEnabled && !self.syncMasterFailed)
                            self.videoViewer.play(mediaAsset, self.syncEvent);
                        else if (self.screenTakeover.takeoverInProgress) {
                            self.videoViewer.play(mediaAsset, self.screenTakeover.syncEvent);
                        } else {
                            self.videoViewer.play(mediaAsset);
                        }

                        self.videoViewer.setMute(muted);

                        if (self.videoStart != null) {
                            self.videoStart(self);
                        }

                        if (duration !== 0) {
                            setVideoAssetPlayTimer(duration);
                        }

                        if (self.currentScheduledPlaylistMediaAsset != null)
                            RealTimeEventsController.sendMediaAssetStart(self.currentScheduledPlaylistMediaAsset, DateTime.now());

                    } else {
                        //logger.debug('VideosPlaying: ' + Settings.application.criticalSections.videosPlaying + ', maxConcurrentVideos: ' + Settings.application.features.maxConcurrentVideos);
                        logger.debug("Not enough free video decoders available, skipping video");
                        //Too many concurrent videos playing
                        self.skipAssetTimer = setTimeout(function() {
                            self.skipAsset();
                        }, 50);
                    }

                    break;
            }
            
            //Prefetch the next asset to reduce time starting to play it
            if (self.currentPlaylist && self.currentPlaylist.startOnHour) {
                self.assetPreloadTimer = setTimeout(function() {
                    self.preloadedPlaylistMediaAsset = null;
                    var assetEndTime = null;

                    logger.debug("Asset start time: " + assetStartTime.toString());

                    if (self.nextStartTime)
                        assetEndTime = self.nextStartTime;
                    else {
                        switch (assetType) {
                            case MediaTypeConstants.IMAGE:
                            case MediaTypeConstants.IPTV:
                            case MediaTypeConstants.WEB:
                            case MediaTypeConstants.WIDGET:
                                assetEndTime = new Date(assetStartTime.getTime() + duration);
                                break;
                            case MediaTypeConstants.VIDEO:
                                if (duration && duration != 0)
                                    assetEndTime = new Date(assetStartTime.getTime() + duration);
                                else
                                    assetEndTime = self.videoViewer.videoEndTime;
                                break;
                        }
                    }

                    //If we haven't been able to calculate the asset end time we cannot preload
                    if (!assetEndTime)
                        return;

                    logger.debug("Current asset will end at " + assetEndTime.toString());
                    //Get asset due to play after this one. 
                    var nextPlaylistMediaAsset = getNextPlaylistMediaAsset(self.videoViewer.videoEndTime);

                    //We won't preload anymore
                    if (nextPlaylistMediaAsset != null) {
                        self.preloadedPlaylistMediaAsset = nextPlaylistMediaAsset;
                    } else {
                        logger.debug("Next asset is null ");
                    }
                }, 800);
            }

            if (self.syncEnabled && !self.isSyncMaster)
                clearSyncFailTimer();
        }

        function getWidgetStartFile(mediaAsset) {
            var dfd = $.Deferred();

            const remoteUrl = mediaAsset.remoteUrl;
            const widgetGuid = mediaAsset.localFileName.replace('.wgt', '');
            const widgetPath = Settings.application.fileSystem.root + "/" + Settings.application.fileSystem.widgetDirectory + "/" + widgetGuid;
            const path = Settings.application.fileSystem.storageDirectory + "/";
            const configFullURI = widgetPath.replace(path, "") + "/config.xml";

            $.ajax({
                    url: configFullURI,
                    type: 'GET',
                    dataType: "xml",
                    async: false
                })
                // On config loaded
                .done(function(data, textStatus, jqXHR) {
                    logger.debug("Config xml loaded: " + data);

                    // Check data is not null
                    if (data != null) {
                        // Get Start File Name from config.xml
                        var startFileName = null;
                        try {
                            startFileName = $(data).children('widget').children('content').attr('src');
                        } catch (err) {
                            logger.warning("Start file not found in widget configuration: " + err.message);
                        }

                        // Determine start file virtual path which will be needed to check if it exists and ultimately load it
                        var potentialStartFile = widgetPath + "/" + startFileName;
                        var startFileFound = false;
                        logger.debug("Potential Start File: " + potentialStartFile);

                        // Check that we have a valid start file name
                        if (!(startFileName === undefined || startFileName == null)) {
                            // Determine if start file exists - note this checks the filesystem
                            // not the local file manifest which is why we use existsFull method
                            File.existsFull(potentialStartFile)
                                .done(function(exists) {
                                    // File exists
                                    if (exists) {
                                        startFileFound = true;

                                        // Load Start File
                                        potentialStartFile = potentialStartFile.replace("storage/sd/", ""); // remove the beginning of path to make it relative to app
                                        logger.debug("Found start file: " + potentialStartFile);
                                        dfd.resolve(potentialStartFile);
                                    }
                                    // File does not exist - fail and move next
                                    else {
                                        // Log error and move next
                                        logger.error("Start file was not found. Invalid widget.");
                                        dfd.reject();
                                    }
                                });
                        } else {
                            logger.debug("No start file in widget config. Attempting to find a default start file");

                            // Get files and check in this order: index.htm, index.html, index.svg, index.xhtml, index.xht
                            Directory.listFiles(widgetPath)
                                // On list files success
                                .done(function(files) {
                                    // Iterate over all files looking for a matching start file. If found store and break.
                                    for (var i = 0; i < files.length; ++i) {
                                        switch (files[i]) {
                                            case "index.htm":
                                            case "index.html":
                                            case "index.svg":
                                            case "index.xhtml":
                                            case "index.xht":
                                                startFileFound = true;
                                                startFileName = files[i];
                                                break;
                                        }
                                    }

                                    // Matching start file found
                                    if (startFileFound) {
                                      // var path = Settings.application.fileSystem.storageDirectory;
                                      //   potentialStartFile = widgetPath.replace(path, "") + "/" + startFileName;
                                      potentialStartFile = widgetPath.replace("storage/sd/", "") + "/" + startFileName;
                                        logger.debug("Found start file: " + potentialStartFile);
                                        dfd.resolve(potentialStartFile);
                                    }
                                    // No start file found
                                    else {
                                        // Log error and move next
                                        logger.error("Start file was not found. Invalid widget.");
                                        dfd.reject();
                                    }
                                });
                        }
                    }
                    // Config file data is null therefore widget in valid. Mark as failed and move next
                    else {
                        // Log error and move next
                        logger.error("Config file data is null. Invalid Widget.");
                        dfd.reject();
                    }

                })
                // On AJAX request failure
                .fail(function(jqXHR, textStatus, errorThrown) {
                    // Log error and move next
                    logger.error("Error loading widget config.");
                    dfd.reject();
                });

            return dfd;
        }

        self.skipAsset = function() {
            // TODO: Add Trigger completion here
                // NOTE: Consider how STs may be affected by this (if at all)
                if (self.syncEnabled && !self.syncMasterFailed && !self.screenTakeover.running) { // NOTE: Testing in relation to takeovers
                logger.debug("Sync is enabled");

                var mediaAsset = self.currentScheduledPlaylistMediaAsset.mediaAsset;

                logger.debug("assetComplete - Callback ID: " + self.id + " details are: " + self.currentAssetIndex + " and " + mediaAsset.name);

                // If this is a screentakeover and this is the master we need to check if this is the end of the playlist. This is not important for slaves.
                var playlistCheckResult = {
                    isComplete: false,
                    nextAssetIndex: 0
                };

                if (self.screenTakeover.takeoverInProgress && self.screenTakeover.isMaster) {
                    playlistCheckResult = getNextAssetIndexAndCheckIfPlaylistIsComplete();
                }

                // Trigger playback (?)
                $("#" + self.id).trigger("zone_asset_complete", {
                    index: self.currentAssetIndex,
                    name: mediaAsset.name,
                    contentType: mediaAsset.contentType,
                    playbackMethod: "standardPlayback",
                    playlistEnded: playlistCheckResult.isComplete,
                    nextAssetIndex: playlistCheckResult.nextAssetIndex
                });
                
                if (self.isSyncMaster) {
                    logger.debug("Player is master - call playNextAssetSync()");
                    playNextAssetSync();
                } else if (self.screenTakeover.running && self.screenTakeover.takeoverInProgress && !self.syncMasterFailed) {  //issue with self.takeoverSyncConfig being null
                    logger.debug("Sync is enabled");
                    if (self.screenTakeover.isMaster) {  //SyncMaster was getting set to false (?)
                        logger.debug("Player is master - wait for zone_asset_complete CB");
                        // Or I could play asset takeover sync here? NOTE:
                    } else {
                        logger.debug("Player is slave - wait for sync event");
                    }
                } else {
                    //Do nothing - wait for sync event
                    logger.debug("Player is slave - wait for sync event");

                    //Set the timeout for sync failing
                    //self.syncFailedTimer = setTimeout(onSyncFailed, syncFailWaitTime);
                }
            } else {
                logger.debug("Not synced, move next normally");
                self.moveNext(self);
            }
        };

        function playWidget(src, duration, mediaAsset) {

            self.widgetViewer.play(self.buildWebWidgetURL(src),duration, mediaAsset);

            if (self.currentScheduledPlaylistMediaAsset != null) {
                setTimeout(function () {
                    logger.debug(`MVZ ${self.guid}: Asset Playing: Callback ID: ${self.id} details are: ${self.currentAssetIndex}  and ${mediaAsset.name}`);
                    $("#" + self.id).trigger("zone_asset_playing", {
                        index: self.currentAssetIndex,
                        name: mediaAsset.name,
                        contentType: mediaAsset.contentType
                    });
                RealTimeEventsController.sendMediaAssetStart(self.currentScheduledPlaylistMediaAsset, DateTime.now());
                }, 250);
            }
        }

        function onWidgetReady(duration)
        {
            // Ensure image control is hidden
            $(zoneScreenElementSelector()).children(imageViewerElementSelector()).css('visibility', 'hidden');
            $(zoneScreenElementSelector()).children(inActiveImageViewerElementSelector()).css('visibility', 'hidden');
            
            setTimeout(function(){
                self.tvViewer.hide();
                self.videoViewer.hide();
            },100);
      

            // hide the other web control
            stopWebViewers();

            setAssetPlayTimer(duration);

            if (self.currentScheduledPlaylistMediaAsset) {
                setTimeout(function () {
                var mediaAsset = self.currentScheduledPlaylistMediaAsset.mediaAsset;
                logger.debug(`MVZ ${self.guid}: Asset Playing: Callback ID: ${self.id} details are: ${self.currentAssetIndex} and ${mediaAsset}`);
                $("#" + self.id).trigger("zone_asset_playing", {
                    index: self.currentAssetIndex,
                    name: mediaAsset.name,
                    contentType: mediaAsset.contentType
                });
                }, 250);
            }
        }

        /**
         * Show image control
         * @param  {number} duration [description]
         * @return {[type]}          [description]
         */
        function showImageControl(duration) {
            logger.debug('showImageControl(' + duration + ')');

            // show the image control
            $(zoneScreenElementSelector()).children(imageViewerElementSelector()).css('visibility', 'visible');
            $(zoneScreenElementSelector()).children(inActiveImageViewerElementSelector()).css('visibility', 'hidden');

            // hide the web controls
            stopWebViewers();

            setTimeout(function()
            {
                self.tvViewer.hide();
                self.videoViewer.hide();
                self.widgetViewer.hide();    
                hideScreenTakeoverOverlay();
            },50)

            // set the timer
            setAssetPlayTimer(duration);

            if (self.currentScheduledPlaylistMediaAsset != null)
                RealTimeEventsController.sendMediaAssetStart(self.currentScheduledPlaylistMediaAsset, DateTime.now());
        }

        /**
         * [setAssetPlayTimer description]
         * @param {[type]} duration [description]
         */
        function setAssetPlayTimer(duration) {
            self.assetTimerId = setTimeout(function() {
                logger.debug('Asset play timer elapsed');
                assetComplete(self);
            }, duration);
        }

        function setVideoAssetPlayTimer(duration) {
            self.assetTimerId = setTimeout(function() {
                logger.debug('Video play timer elapsed');
                self.videoViewer.stop();
            }, duration);
        }

        function setTVAssetPlayTimer(duration) {
            self.assetTimerId = setTimeout(function() {
                logger.debug('HDMI play timer elapsed');
                self.tvViewer.stop();
                assetComplete(self);
            }, duration);
        }

        /**
         * Once an asset has finished playing this callback is called and this moves to the next asset to play
         * @param  {[type]} sender [description]
         * @return {[type]}        [description]
         */
        function assetComplete(sender) {
            logger.debug('Asset Complete');
            self.assetCompleteDone = true;
            
            // NOTE: Only Log Proof of Play in regards to players not currently in a takeover with a takeover configuration or those that do not have a screen takeover configuration
            if(!self.screenTakeover || (self.screenTakeover && !self.screenTakeover.takeoverInProgress)) {
                // Log PoP
                logProofOfPlayOnAssetComplete(self.playReason);
            }

            // send real time events
            RealTimeEventsController.sendMediaAssetComplete(self.currentScheduledPlaylistMediaAsset, self.currentAssetStartTime, DateTime.now(), self.playReason, self.playReason);

            var mediaAsset = self.currentScheduledPlaylistMediaAsset.mediaAsset;

            logger.debug("assetComplete - Callback ID: " + self.id + " details are: " + self.currentAssetIndex + " and " + mediaAsset.name);

            // If this is a screentakeover and this is the master we need to check if this is the end of the playlist. This is not important for slaves.
            var playlistCheckResult = {
                isComplete: false,
                nextAssetIndex: 0
            };

            if (self.screenTakeover.takeoverInProgress && self.screenTakeover.isMaster) {
                playlistCheckResult = getNextAssetIndexAndCheckIfPlaylistIsComplete();
            }

            // Trigger playback (?)
            $("#" + self.id).trigger("zone_asset_complete", {
                index: self.currentAssetIndex,
                name: mediaAsset.name,
                contentType: mediaAsset.contentType,
                playbackMethod: "standardPlayback",
                playlistEnded: playlistCheckResult.isComplete,
                nextAssetIndex: playlistCheckResult.nextAssetIndex
            });

            if (self.syncEnabled && !self.syncMasterFailed && !self.screenTakeover.running) {
                logger.debug("Sync is enabled");

                if (self.isSyncMaster) {
                    logger.debug("Player is master - call playNextAssetSync()");
                    playNextAssetSync();
                } else {
                    //Do nothing - wait for sync event
                    logger.debug("Player is slave - wait for sync event");

                    //Set the timeout for sync failing
                    //self.syncFailedTimer = setTimeout(onSyncFailed, syncFailWaitTime);
                }
            } else if (self.screenTakeover.running && self.screenTakeover.takeoverInProgress && !self.syncMasterFailed) {  //issue with self.takeoverSyncConfig being null
                logger.debug("Sync is enabled");
                if (self.screenTakeover.isMaster) {  //SyncMaster was getting set to false (?)
                    logger.debug("Player is master - wait for zone_asset_complete CB");
                } else {
                    logger.debug("Player is slave - wait for sync event");
                }  
            } else {
                logger.debug("Not synced, move next normally");
                self.moveNext(sender);
            }
        }

        /**
         * Logs proof of play
         * @return {PlaybackStartEndReasons} The reason playback has ended
         */
        function logProofOfPlayOnAssetComplete(endReason) {
            try {
                if (window.proofOfPlayLogger != null) {
                    var playlistMediaAsset = null;
                    var mediaAsset = self.currentScheduledPlaylistMediaAsset;

                    if (mediaAsset == null) // Validity with no valid items
                        return;

                    if (mediaAsset.mediaAsset) {
                        mediaAsset = self.currentScheduledPlaylistMediaAsset.mediaAsset;
                        playlistMediaAsset = self.currentScheduledPlaylistMediaAsset;
                    }

                    var playReason = (self.playReason !== PlaybackStartEndReasons.PRELOAD_LAYOUT) ? self.playReason : PlaybackStartEndReasons.TAKEOVER;
                    endReason = (endReason !== PlaybackStartEndReasons.PRELOAD_LAYOUT) ? endReason : PlaybackStartEndReasons.TAKEOVER;

                    logger.info("PoP Log: Asset Name: " + mediaAsset.name + ", Start Time: " + self.currentAssetStartTime.toISOString() + ", End Time: " + DateTime.now().toISOString() + ", Play Reason: " + playReason + ", End Reason: " + endReason);
                    window.proofOfPlayLogger.debug(self.layout, self.viewModel, self.currentPlaylist, playlistMediaAsset, mediaAsset, self.currentAssetStartTime.toISOString(), DateTime.now().toISOString(), playReason, endReason);
                }
            } catch (e) {
                logger.error('Error logging Proof of Play: ' + JSON.stringify(e));
            }

        }

        self.logProofOfPlay = function () {
            // Log PoP
            logProofOfPlayOnAssetComplete(self.playReason); 
        };

        /**
         * Start playlist loop
         * @return {[type]} [description]
         */
        function startPlaylistLoop() {
            logger.debug("Start Playlist Loop - setting current asset index to -1");
            self.preloadedPlaylistMediaAsset = null;
            var endTime = null;

            if (self.currentPlaylist.startOnHour) {
                var startTime = DateTime.now();

                //If we are starting at HH:00:00 or HH:00:01 we will just start playing.
                if ((startTime.getMinutes() == 0 && startTime.getSeconds() <= 1) == false) {
                    //logger.debug("Start on hour enabled. Current time: " + startTime);
                    startTime.setMinutes(0, 0, 0);
                    //logger.debug("Beginning playlist from: " + startTime);               

                    //Next hour        
                    endTime = new Date(startTime);
                    endTime.setHours(endTime.getHours() + 1);

                    while (startTime < DateTime.now()) {
                        var asset = getNextPlaylistMediaAsset(startTime);

                        var moveNextTime = 10000;

                        //If asset is null, we will move forward 10 seconds.
                        if (asset) {
                            if (asset.duration > 0)
                                moveNextTime = asset.duration;
                            else
                                moveNextTime = asset.mediaAsset.duration;

                            if (moveNextTime == 0)
                                moveNextTime = 10000;

                            //logger.debug("Asset at time: " + startTime + " is " + asset.mediaAsset.name);
                            //logger.debug("Duration is calculated as: " + moveNextTime + "ms");
                        }

                        //Get new time
                        startTime = new Date(startTime.getTime() + moveNextTime);

                        //Stop before getting the asset
                        if (startTime > DateTime.now()) {
                            self.nextStartTime = startTime;
                            self.playlistMediaAssetDurationOverride = startTime.getTime() - DateTime.now().getTime();
                            break;
                        }
                    }

                    self.currentAssetIndex--;

                    if (self.currentAssetIndex < 0)
                        self.currentAssetIndex = self.currentPlaylist.playlistMediaAssets.length - 1;
                } else {
                    //Calculate the next playlist restart
                    startTime.setMinutes(0, 0, 0);
                    endTime = new Date(startTime);
                    endTime.setHours(endTime.getHours() + 1);
                    self.nextStartTime = startTime;
                }
            }

            // NOTE: Consider changes here as we only want sync'd play for takeover config but during the takeover itself
            if (self.syncEnabled && !self.takeoverSyncConfig) {
                logger.debug("Sync is enabled");

                if (self.isSyncMaster) {
                    logger.debug("Player is master - call playNextAssetSync()");
                    playNextAssetSync();
                } else {
                    //Do nothing - wait for sync event
                    logger.debug("Player is slave - wait for sync event - will wait for " + syncStartWaitTime + "ms");

                    //self.syncFailedTimer = setTimeout(onSyncFailed, syncStartWaitTime);
                }
            } else {
                logger.debug("Not synced, play normally");
                self.moveNext(self);
            }

            if (self.currentPlaylist.startOnHour) {
                if ((DateTime.now().getTime() + 30000) < endTime.getTime()) {
                    self.playlistRestartIntervalTimer = setInterval(function() {
                        if ((DateTime.now().getTime() + 30000) > endTime.getTime()) {
                            clearTimeout(self.playlistRestartIntervalTimer);

                            restartOnNextHour(endTime);
                        }
                    }, 15000);
                } else {
                    restartOnNextHour(endTime);
                }

            }
        }



        function restartOnNextHour(endTime) {
            var millisToNextHour = endTime.getTime() - DateTime.now().getTime();
            logger.debug("Setting a playlist restart for " + millisToNextHour + " milliseconds from now");

            self.playlistRestartTimer = setTimeout(function() {
                stopPlaylistLoop(PlaybackStartEndReasons.SCHEDULE);
                self.playlistRestartTimer = null;
                startPlaylistLoop();
            }, millisToNextHour);

        }





        /*
        //        //Reserve a video decoder
        Settings.application.criticalSections.videosPlaying++;

                        //Remember this
                        zoneHasDecoder = true;
                        playlistHasPlayedVideo = true;
          */
        /**
         * Stop the current playlist loop
         * @param {PlaybackStartEndReasons} Reason for stopping playback
         * @return {[type]} [description]
         */
        function stopPlaylistLoop(endReason) {
            logger.debug("Stopping playlist loop");

            // clear timers
            clearAllTimers();

            // remove images and web
            $(zoneScreenElementSelector()).children(imageViewerElementSelector()).css('visibility', 'hidden');
            $(zoneScreenElementSelector()).children(inActiveImageViewerElementSelector()).css('visibility', 'hidden');

            stopWebViewers();

            // Stop and Hide video but do not move next
            self.videoViewer.stopAndHideDoNotMoveNext();
            self.videoViewer.clearVideo();
            self.tvViewer.clearVideo();

            // Log that the current asset has stopped playing
            logProofOfPlayOnAssetComplete(endReason);

            // send real time events
            if(self.currentScheduledPlaylistMediaAsset)
            {
                RealTimeEventsController.sendMediaAssetComplete(self.currentScheduledPlaylistMediaAsset, self.currentAssetStartTime, DateTime.now(), self.playReason, endReason);
            }

            if (zoneHasDecoder) {
                zoneHasDecoder = false;
                playlistHasPlayedVideo = false;
                if(Settings.application.criticalSections.videosPlaying > 0) {
                    Settings.application.criticalSections.videosPlaying--;
                }
            }
        }

        function clearPlaylistRestartIntervalTimer() {
            clearInterval(self.playlistRestartIntervalTimer);
        }

        function clearSyncFailTimer() {
            // stop the asset timer
            clearTimeout(self.syncFailedTimer);
        }

        /**
         * Clear asset timer
         * @return {[type]} [description]
         */
        function clearAssetTimer() {
            // stop the asset timer
            clearTimeout(self.assetTimerId);
        }

        /**
         * Clear playlist timer
         * @return {[type]} [description]
         */
        function clearPlaylistTimer() {
            // stop the playlist timer
            clearTimeout(self.playlistTimerId);
        }

        /**
         * Clear web timer
         * @return {[type]} [description]
         */
        function clearWebContentTimeoutTimer() {
            // clear the web content load timeout timer
            clearTimeout(self.webZoneLoadTimer);
        }

        function clearPlaylistRestartTimer() {
            clearTimeout(self.playlistRestartTimer);
        }

        function clearSkipAssetTimer() {
            clearTimeout(self.skipAssetTimer);
        }

        function clearPreloadTimer() {
            clearTimeout(self.assetPreloadTimer);
        }


        /**
         * Clear all timers
         * @return {[type]} [description]
         */
        function clearAllTimers() {
            clearAssetTimer();
            clearPlaylistTimer();
            clearWebContentTimeoutTimer();
            clearSyncFailTimer();
            clearPlaylistRestartTimer();
            clearPlaylistRestartIntervalTimer();
            clearSkipAssetTimer();
            clearPreloadTimer();
        }

        /**
         * Move to the next media asset
         * @param  {[type]} sender [description]
         * @return {[type]}        [description]
         */
        self.moveNext = function(sender) {

            self.assetCompleteDone = false;

            try {
                if (self.currentPlaylist == null) {

                    // there is no current playlist - play the default media asset
                    self.playAsset(self.viewModel.defaultMediaAsset, 86400000);
                    self.currentAssetStartTime = DateTime.now();
                } else {
                    // reset/stop any previous assets
                    if (self.currentScheduledPlaylistMediaAsset != null) {
                        var assetType = self.currentScheduledPlaylistMediaAsset.mediaAsset.contentType;
                        switch (assetType) {
                            case MediaTypeConstants.CAPTURE:
                                //self.tvViewer.stop();
                                if (self.syncEnabled) {              
                                    self.videoViewer.stop(true);
                                }
                                break;
                            case MediaTypeConstants.IPTV:
                                self.videoViewer.stop(true);
                                break;
                            case MediaTypeConstants.VIDEO:

                                if (self.syncEnabled) {              
                                    self.videoViewer.stop(true);
                                }
                            
                        }
                    }

                    logger.debug('Get next asset');
                    self.currentScheduledPlaylistMediaAsset = getNextPlaylistMediaAsset();
                    logger.debug('Get next asset done');
                    // has a valid media asset been found?
                    if (self.currentScheduledPlaylistMediaAsset != null) {
                        self.currentAssetStartTime = DateTime.now();
                        // is this a video asset?

                        var canPlay = true;

                        //Do a quick check for resolution
                        if (Settings.application.features.canDisplay4kContent == false || Settings.application.features.canDisplay8kContent == false) {
                            if (self.currentScheduledPlaylistMediaAsset.mediaAsset.metaData) {
                                if (self.currentScheduledPlaylistMediaAsset.mediaAsset.metaData.width) {
                                    var width = self.currentScheduledPlaylistMediaAsset.mediaAsset.metaData.width;
                                    var height = self.currentScheduledPlaylistMediaAsset.mediaAsset.metaData.height;
                                    var max = Math.max(width, height);

                                    if ((max > 1920 && !Settings.application.features.canDisplay4kContent) || (max > 3840 && !Settings.application.features.canDisplay8kContent)) {
                                        canPlay = false;
                                        self.currentScheduledPlaylistMediaAsset.mediaAsset.hasFailed = true;
                                        logger.debug("Content resolution is too large for this device - setting to failed.");
                                    }
                                }
                            }
                        }

                        if (canPlay) {
                            //We may want to end the current asset early
                            var duration = self.playlistMediaAssetDurationOverride;
                            if (duration == 0)
                                duration = self.currentScheduledPlaylistMediaAsset.duration;

                            self.playAsset(self.currentScheduledPlaylistMediaAsset.mediaAsset, duration);

                            self.playlistMediaAssetDurationOverride = 0;
                        } else {
                            self.assetTimerId = setTimeout(function() {
                                self.skipAsset();
                            }, 50);
                        }

                    } else {
                        logger.debug('No valid media asset');

                        // hide invalid media
                        hideWebViewers();
                        $(zoneScreenElementSelector()).children(imageViewerElementSelector()).css('visibility', 'hidden');
                        self.currentScheduledPlaylistMediaAsset = null;
                        // set a timer to wait for the next valid asset
                        self.assetTimerId = setTimeout(function() {
                            //Wait a quarter second before moving next again
                            self.moveNext(self);
                        }, 250);
                    }
                }
            } catch (e) {
                logger.error('Error whilst moving next ' + e.message);
            }
        };

        function stopWebViewers() {
            hideWebViewers();
            $(zoneScreenElementSelector()).children(activeWebViewerElementSelector()).attr('src', 'about:blank');
            $(zoneScreenElementSelector()).children(inActiveWebViewerElementSelector()).attr('src', 'about:blank');
        }

        function hideWebViewers() {
            $(zoneScreenElementSelector()).children(inActiveWebViewerElementSelector()).css('visibility', 'hidden');
            $(zoneScreenElementSelector()).children(activeWebViewerElementSelector()).css('visibility', 'hidden');
            hideHtmlWidget();
        }

        function hideHtmlWidget() {
            var msg = self.messagePort.PostBSMessage({ action: "delete_HTML_Widget", index: zoneIndex });
        }

        /**
         * Set current playlist
         */
        function setCurrentPlaylist() {
            // is there a playlist already playing?
            if (self.currentPlaylist != null) {
                // stop the playlist loop
                stopPlaylistLoop();
            }

            // find the current schedule
            var currentScheduledPlaylist = null;
            $(self.viewModel.scheduledPlaylists).each(function(i, scheduledPlaylist) {
                try {
                    if (scheduledPlaylist.start <= DateTime.now() && scheduledPlaylist.end >= DateTime.now()) {
                        currentScheduledPlaylist = scheduledPlaylist;
                    }
                } catch (e) {

                }
            });

            // if there a playlist scheduled play it, otherwise play the default
            var playlistTimeout = 0;
            if (currentScheduledPlaylist != null) {
                self.currentPlaylist = currentScheduledPlaylist.playlist;
                self.currentPlaylistStartTime = currentScheduledPlaylist.start;
                playlistTimeout = currentScheduledPlaylist.end.subtract(DateTime.now());
            } else {
                self.currentPlaylist = self.viewModel.defaultPlaylist;

                // find the start of the next scheduled - if there is one

                // find the next scheduled playlist in the future
                var candidateNextScheduledPlaylist = null;

                $(self.viewModel.scheduledPlaylists).each(function(i, scheduledPlaylist) {
                    try {
                        if (scheduledPlaylist.start > DateTime.now()) {
                            if (candidateNextScheduledPlaylist == null)
                                candidateNextScheduledPlaylist = scheduledPlaylist;
                            else if (scheduledPlaylist.start < candidateNextScheduledPlaylist.start)
                                candidateNextScheduledPlaylist = scheduledPlaylist;
                        }
                    } catch (e) {

                    }
                });

                // if there is a candidate set the timeout of the default to the start of it - otherwise play for 12 hours
                if (candidateNextScheduledPlaylist != null)
                    playlistTimeout = candidateNextScheduledPlaylist.start.subtract(DateTime.now());
                else {
                    // if there are no scheduled playlists in the future don't time out
                    playlistTimeout = -1;
                }
            }

            // is there a playlist
            if (self.currentPlaylist != null) {
                logger.debug('There are ' + self.currentPlaylist.playlistMediaAssets.length + ' assets in the playlist');

                //Setup the feed cache
                self.feedCache = [];

                $(self.currentPlaylist.playlistMediaAssets).each(function(index, playlistMediaAsset) {

                    if (playlistMediaAsset.feed) {
                        logger.debug('Playlist Media Asset has feed');

                        //If random feed, randomise before we start
                        if(playlistMediaAsset.feed.playMode === "random")
                            playlistMediaAsset.feed.randomize();

                        if (!self.feedCache[playlistMediaAsset.feed.mediaAssetId]) {
                            logger.debug('Adding feed to feedCache');
                            self.feedCache[playlistMediaAsset.feed.mediaAssetId] = {
                                feed: playlistMediaAsset.feed,
                                feedIndex: -1,
                                currentGrouping: 0,
                                validItemsPlayed: 0,
                                feedItemAvailable: {}
                            };

                            // Set all assets available
                            $(playlistMediaAsset.feed.feedMediaAssets).each(function(index, fma) {
                                self.feedCache[playlistMediaAsset.feed.mediaAssetId].feedItemAvailable[fma.externalId] = true;
                            });
                        }
                    }
                });

                // start playing the playlist
                startPlaylistLoop();

                // has a timeout been set?
                if (playlistTimeout > 0) {
                    // set the timer to play the schedule at the appropriate time
                    self.playlistTimerId = setTimeout(function() {
                        setCurrentPlaylist();
                    }, playlistTimeout);
                }
            } else {
                // there is no current playlist - play the default media asset
                self.playAsset(self.viewModel.defaultMediaAsset, 86400000);
            }
        }

        function feedAssetIsAvailable(pma) {
            return (pma.feed && self.feedCache[pma.feed.mediaAssetId].feedItemAvailable[pma.externalId] === true)
        }
        
        function resetAvailableFeedItemsForFeed(feed) {
            // We need to make sure available feed asset items only consider those that are currently valid(?), otherwise it doesn't reset the items correctly
            var validFeedItems = [];
            
            self.feedCache[feed.mediaAssetId].feed.feedMediaAssets.forEach(function(fma) {
                if(fma.isCurrentlyValid()) {
                    validFeedItems[fma.externalId] = true;
                };
            });

            var hasAvailableFeedAssetsRemaining = Object.keys(self.feedCache[feed.mediaAssetId].feedItemAvailable).some(function(fmaId) {
                if(validFeedItems[fmaId]) {
                    return self.feedCache[feed.mediaAssetId].feedItemAvailable[fmaId] === true;
                }
            });
        
            if (hasAvailableFeedAssetsRemaining === false) {
                Object.keys(self.feedCache[feed.mediaAssetId].feedItemAvailable).forEach(function(fmaId) {
                    self.feedCache[feed.mediaAssetId].feedItemAvailable[fmaId] = true;
                });
            }
        }
        

        /**
         * Play next asset as part of sync
         */
        self.syncPlayAsset = function(index) {
            logger.debug('Setting current asset index to: ' + index);
            self.currentAssetIndex = index;
            logger.debug('Calling move next');
            self.moveNext(self);
        }

        /**
         * Master only - this will send the sync signal which will then call syncPlayAsset
         */
        function playNextAssetSync() {
            logger.debug('Sending sync event with index: ' + self.currentAssetIndex.toString());

            sync.SetMaster(1);
            sync.Synchronize(self.currentAssetIndex.toString(), 250);
        }

        /**
         * Play current playlist
         * @return {[type]} [description]
         */
        self.play = function() {
            logger.debug('Playing MediaZoneView');
            setCurrentPlaylist();
            self._currentStatus = PlayStatus.PLAYING;
        };

        self.setStartingAssetIndex = function(index) {
            self.currentAssetIndex = index;
        }

        self.registerAssetCompleteEventListener = function (cb) {
            logger.debug("zone_asset_complete - Callback ID: " + self.id);
            $('#' + self.id).off("zone_asset_complete").on("zone_asset_complete", function (e, args) {
                // Provide current index details
                cb(self.currentAssetIndex, self.layout.externalId, self.id, args.playlistEnded, args.nextAssetIndex);
            });
        };

        self.removeAssetCompleteEventListener = function(cb) {
            logger.debug(`MVZ ${self.guid}: REMOVE zone_asset_complete - Callback ID:  ${self.id}`);
            $('#' + self.id).off("zone_asset_complete");
        };

        self.registerAssetStartEventListener = function (cb) {
            logger.debug("zone_asset_start  - Callback ID: " + self.id);
            $('#' + self.id).off("zone_asset_start").on("zone_asset_start", function (e, args) {
                // Provide current index details
                cb(self.currentAssetIndex, self.layout.externalId, self.id);
            });
        };

        self.removeAssetStartEventListener = function(cb) {
            logger.debug(`MVZ ${self.guid}: REMOVE zone_asset_start  - Callback ID:  ${self.id}`);
            $('#' + self.id).off("zone_asset_start");
        };

        self.registerAssetPlayingEventListener = function(cb) {
            logger.debug(`MVZ ${self.guid}: zone_asset_playing - Callback ID:  ${self.id}`);
            $('#' + self.id).off("zone_asset_playing").on("zone_asset_playing", function(e, args) {
                // Provide current index details
                cb(self.playReason, self, e.currentTarget.id);
            });
        };

        self.removeAssetPlayingEventListener = function(cb) {
            logger.debug(`MVZ ${self.guid}: REMOVE zone_asset_playing - Callback ID:  ${self.id}`);
            $('#' + self.id).off("zone_asset_playing");
        };

        /**
         * Stop Playlist
         * @param {PlaybackStartEndReasons} reason for stopping playlist
         * @return {[type]} [description]
         */
        self.stop = function(endReason) {
            logger.debug('Stopping MediaZoneView');
            stopPlaylistLoop(endReason);
            self._currentStatus = PlayStatus.STOPPED;

            // NOTE: Previously, this was stopping too frequently when we have a valid takeover config, just to re-establish it. Therefore only stop the controller if sync is enabled (e.g. standard sync) and if there isnt a takeover configuration
            if ((self.syncEnabled && !self.screenTakeover._takeoverGroupConfig)) {
                // Consider if legit?
                sync.Close();
            }
        };

        /**
         * Pause
         * @return {[type]} [description]
         */
        self.pause = function() {
            self._currentStatus = PlayStatus.PAUSED;
            clearAssetTimer();
        };

        /**
         * Skip
         * @return {[type]} [description]
         */
        self.skip = function() {
            clearAssetTimer();

            if (_direction != PlayDirection.FORWARDS) {
                logger.debug('Change of play direction to forwards');
                _direction = PlayDirection.FORWARDS;
            }

            self.moveNext(self);
        };

        /**
         * Back
         * @return {[type]} [description]
         */
        self.back = function() {
            clearAssetTimer();

            if (_direction != PlayDirection.BACKWARDS) {
                logger.debug('Change of play direction to backwards');
                _direction = PlayDirection.BACKWARDS;
            }

            self.moveNext(self);
        };

        /**
         * Mute
         * @param {[type]} muted [description]
         */
        self.setMute = function(muted) {
            self.videoViewer.setMute(muted);
            self.tvViewer.setMute(muted);
        };

        /**
         * Volume Up
         * @return {[type]} [description]
         */
        self.volumeUp = function() {
            self.videoViewer.volumeUp();
        };

        /**
         * Volume Down
         * @return {[type]} [description]
         */
        self.volumeDown = function() {
            self.videoViewer.volumeDown();
        };

        /**
         * Build Web / Widget URL with additional data from web based trigger
         */
        self.buildWebWidgetURL = function(originalUrl) {
            var url = decodeURI(originalUrl);

            if (self.triggerAdditionalData != null) {
                if (url.includes('?')) {
                    url = url + '&';
                } else {
                    url = url + '?';
                }

                self.triggerAdditionalData.forEach(function(data) {
                    logger.debug('Appending additional data ' + data.Key + ":" + data.Value);
                    url = url + data.Key + "=" + data.Value + "&";
                });

                url = url.substr(0, url.lastIndexOf('&'));
            }

            logger.info('Built URL with additional data: ' + encodeURI(url));
            return encodeURI(url);
        };

    }

    return MediaZoneView;
});
rjs.define('gui/views/SVGConstants',{
	XmlNS:'http://www.w3.org/2000/svg'
});

rjs.define('gui/views/SVGLayerBase',["gui/views/LayerBase", "gui/views/SVGConstants"], function(LayerBase, SVGConstants) {
	function SVGLayerBase(zoneViewModel, layoutName) {
		// init the parent object
		this.parent = new LayerBase(zoneViewModel, layoutName);
		// copy all properties so they are available
		for (var property in this.parent) {

			if (property != 'prototype' & property != 'parent')
				this[property] = this.parent[property];
		}

		this.svgElem = null;
		this.bgElement = null;

		this.svgElem = document.createElementNS(SVGConstants.XmlNS, "svg");
		this.svgElem.setAttributeNS(null, "viewBox", "0 0 " + this.width + " " + this.height);
		this.svgElem.setAttributeNS(null, "width", this.width);
		this.svgElem.setAttributeNS(null, "height", this.height);
		this.svgElem.setAttributeNS(null,"preserveAspectRatio","none");
		this.svgElem.style.display = "block";

		var baseElement = document.getElementById(this.id);
		baseElement.appendChild(this.svgElem);
	}

	return SVGLayerBase;
});

rjs.define('gui/viewmodels/TextAnimationEnum',[],function(){
	return{
		NONE:'none',
		SCROLL_RIGHT_TO_LEFT:'scroll_right_to_left',
		SCROLL_LEFT_TO_RIGHT:'scroll_left_to_right',
		SCROLL_TOP_TO_BOTTOM:'scroll_top_to_bottom',
		SCROLL_BOTTOM_TO_TOP:'scroll_bottom_to_top',
		FADE:'fade'		
	}
});

rjs.define('gui/views/TextZoneView',["gui/views/SVGConstants", "gui/PlayUrlHelper", "gui/views/SVGLayerBase", "gui/viewmodels/TextAnimationEnum", "models/MediaTypeConstants", "models/MimeTypeMediaTypeMappings", "properties/App.Settings", "system/io/File", "models/RssChannelModel", "controllers/UserConfigurationController", "models/RssChannelCache"], function(SVGConstants, PlayUrlHelper, SVGLayerBase, TextAnimation, MediaTypeConstants, MimeTypeMediaTypeMappings, Settings, File, RssChannel, UserConfigurationController, RssChannelCache) {
    function TextZoneView(zoneViewModel, layoutName) {
        this.parent = new SVGLayerBase(zoneViewModel, layoutName);
        for (var property in this.parent) {

            if (property != 'prototype' & property != 'parent')
                this[property] = this.parent[property];
        }

        var self = this;
        self.viewModel = zoneViewModel;

        this.itemPadding = 30;

        var animating = false;
        var animationRefreshRate = 1000 / 60;

        if (!window.requestAnimationFrame) {
            window.requestAnimationFrame = window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame;
        }

        var rotation = 0;
        var x = self.viewModel.x;
        var y = self.viewModel.y;
        var width = self.viewModel.width;
        var height = self.viewModel.height;
        var padding =  Math.round(self.viewModel.height/10.0);

        if (UserConfigurationController.isRotated() && Settings.application.features.maxSupportedOutputs == 1 && Settings.application.features.deviceSeries < 5) 
        {
            rotation = UserConfigurationController.rotationAngle();
            
            var tmp = width;
            width = height;
            height = tmp;

            if(rotation == 90)
            {
                var cTmp = x;
                x = y;
                x = window.innerWidth - (x + width);
                y = cTmp;

                x = x + padding;
                width = width - (2 * padding);

                rotation = 270;

            } else if (rotation == 270)
            {
                var cTmp = x;
                x = y;
                y = cTmp;

                x = x + padding;
                width = width - (2 * padding);

                rotation = 90;
            }
        } else {
            y = y + padding;
            height = height - (2 * padding);
        }
        
        var bsTicker = null;

        self.play = function() {

            bsTicker = new BSTicker(x, y, width, height,rotation);
            logger.debug("Setting font to: " + self.viewModel.configurationParameters.font);
            bsTicker.SetFont(self.viewModel.configurationParameters.font);
            // bsTicker.SetAutoPop(true); - Temporarily disabled this as a workaround for boot-looping issue caused by 9.0.145.1 BSOS (SB - 04/06/24).

            logger.debug("Loading the RSS Feed");

            loadRSSFeed();
            
            logger.debug("Setting font colour");
            if (self.viewModel.configurationParameters.font_color != undefined & self.viewModel.configurationParameters.font_color != null) {
                    var colourHex = self.viewModel.configurationParameters.font_color;
                    // compensate for SL bug that removes leading zeros on hex values!!!!!!!!
                    switch (colourHex.length) {
                        case 2:
                            colourHex = '00000' + colourHex.substring(1);
                            break;
                        case 3:
                            colourHex = '0000' + colourHex.substring(1);
                            break;
                        case 4:
                            colourHex = '000' + colourHex.substring(1);
                            break;
                        case 5:
                            colourHex = '00' + colourHex.substring(1);
                            break;
                        case 6:
                            colourHex = '0' + colourHex.substring(1);
                            break;
                    }
                colourHex = colourHex.replace("#","0x")
                bsTicker.SetForegroundColor(parseInt(colourHex));
            } else
                bsTicker.SetForegroundColor(0x000000);

                          // load the background - if there is one
             if (self.viewModel.backgroundAsset != null && MimeTypeMediaTypeMappings.getMediaType(self.viewModel.backgroundAsset.mimeType) == MediaTypeConstants.IMAGE) {
             	self.bgElement = document.createElementNS(SVGConstants.XmlNS, "image");
             	self.bgElement.setAttributeNS(null, "x", 0);
             	self.bgElement.setAttributeNS(null, "y", 0);
             	self.bgElement.setAttributeNS(null, "width", this.width);
             	self.bgElement.setAttributeNS(null, "height", this.height);

             	self.bgElement.setAttributeNS(null, "preserveAspectRatio", "none");

             	PlayUrlHelper.getMediaAssetPlayUrl(self.viewModel.backgroundAsset).done(function(url) {
             		self.bgElement.setAttributeNS('http://www.w3.org/1999/xlink', "href", url);
            	});
             	this.svgElem.appendChild(self.bgElement);

                 //Set the BS Ticker to be transparent
                 bsTicker.SetBackgroundColor(0,0,0,0);
             }

            bsTicker.SetSeparatorString("   ");

            var speed = parseInt(self.viewModel.configurationParameters.speed);
            speed = speed * 50;

            bsTicker.SetPixelsPerSecond(speed);

    
            logger.debug("Adding items to the ticker");
            var itemsInTicker = 0;

            //Initial items
            $(itemQueue).each(function(index, item) {
                
                if(index < 63)
                {
                    itemsInTicker++;
                    bsTicker.AddString(item.title);
                }
            });

            //Add a few extra if necessary to ensure screen is used
            if(itemsInTicker < 10)
            {
                while(itemsInTicker < 10)
                {
                    var nextItem = GetNextItem();
    
                    if(nextItem)
                    {
                        bsTicker.AddString(nextItem.title);
                        itemsInTicker++;
                    } else {
                        break;
                    }               
                }
            }

            //As each item goes off screen we add another
            bsTicker.ontickerstringremoved = function(stringId)
            {
                var nextItem = GetNextItem();

                if(nextItem)
                {
                    bsTicker.AddString(nextItem.title);
                } else {
                    bsTicker.AddString("    ");
                }
            };
  

        };

        self.stop = function() {
            logger.debug('Stopping TextZoneView');
            if(bsTicker) {
                bsTicker.Shutdown();
            }

            animating = false;
        };
        var opacity = 0;
        var isFadingOut = false;


        function getCurrentRSSFeedID()
        {
            var rssAsset = null;
            // get the current playlist
            var currentPlaylist = getCurrentPlaylist();
            if (currentPlaylist != null) {
                $(currentPlaylist.playlistMediaAssets).each(function(i, playlistMediaAsset) {
                    if (playlistMediaAsset.mediaAsset.contentType == MediaTypeConstants.RSS) {
                        rssAsset = playlistMediaAsset.mediaAsset;
                    }
                });
            } else {
                rssAsset = self.viewModel.defaultMediaAsset;
            }

            return rssAsset.externalId;
        }

    
        function loadRSSFeed()
        {     
            loadFeedDataFromCache(getCurrentRSSFeedID());
        }

        function loadFeedDataFromCache(assetId) {
            logger.debug('Loading feed from cache');

            try {
                var rssChannel = RssChannelCache[assetId];
                if (rssChannel != null) {
                    $(rssChannel.items).each(function(i, item) {
                        itemQueue.push(item);
                    });
                }

                return rssChannel;

            } catch (e) {
                logger.error('Error loading RSS Channel from cache: ' + e.message);
            }
        }

        function getCurrentPlaylist() {
            var currentPlaylist = null;
            $(self.viewModel.scheduledPlaylists).each(function(i, scheduledPlaylist) {
                try {
                    if (scheduledPlaylist.start <= DateTime.now() && scheduledPlaylist.end >= DateTime.now()) {
                        currentPlaylist = scheduledPlaylist.playlist;
                    }
                } catch (e) {

                }
            });

            if (currentPlaylist == null) {
                currentPlaylist = self.viewModel.defaultPlaylist;
            }

            return currentPlaylist;
        }

        var itemQueue = [];

        function GetNextItem()
        {
            if(itemQueue.length > 0)
            {
                //Get next item from the queue
                return itemQueue.shift();
            } else{
                //Empty queue, reload the current feed, it may have updated
                var assetId = getCurrentRSSFeedID();

                if(assetId)
                {
                    var rssChannel = RssChannelCache[assetId];
                    if (rssChannel != null) {
                        $(rssChannel.items).each(function(i, item) {
                            itemQueue.push(item);
                        });
                    }
                }

                if(itemQueue.length > 0)
                    return itemQueue.shift(); 
                else 
                    return null;
            }
        }


    }

    return TextZoneView;
});
rjs.define('controllers/InterruptController',["models/ContentModel", "models/InterruptModel", "models/ActionModel", "models/TriggerModel", "properties/App.Settings","system/serialport/SerialPortInterface"], function(ContentModel, InterruptModel, ActionModel, TriggerModel, Settings, SerialPortInterface) {
	function InterruptController(){
		var self = this;

		var _mediaPlayer = null;
		var _interrupts = null;
		var _scheduleTimerId = null;
		var _isPlayingInterrupt = false;
		var _highPriorityInterruptPlaying = false;
		var _indefiniteInterruptPlaying = false;
		var _currentlyPlayingInterrupt = null;
		var _interruptPlayingCallback = null;

		/**
		 * Initialize the interrupt Controller
		 * @param  {Reference to media player}
		 * @param  {Interrupt Model}
		 * @return {JQuery Promise}
		 */
		self.initialize = function(mediaPlayer, interrupts){
			var dfd = $.Deferred();

			setTimeout(function()
			{
				_mediaPlayer = mediaPlayer;
				_interrupts = interrupts;

				//Close serial ports to refresh
				SerialPortInterface.closePorts();

				for (var i = 0; i < _interrupts.length; i++)
				{
					var interrupt = _interrupts[i];
					
					if(interrupt.trigger.type == "SerialEvent")
					{
						SerialPortInterface.receiveData(interrupt.trigger.serialDevice.externalId,interrupt.trigger.serialDevice.config,serialEventReceived);
					}
				}
						
				dfd.resolve();

			}, 50);

			return dfd;
		};

		function serialEventReceived(serialDeviceID, port, serialData)
		{
			logger.debug("Received serial command: " + serialData);

			self.triggerInterrupt(String(serialData),"SerialEvent",false,false,null,serialDeviceID).fail(function()
			{
				logger.debug("Serial interrupt not found, try cancelling");
				//Interrupt not found, is it a cancel?
				self.triggerCancel(String(serialData),"SerialEvent",serialDeviceID);
			}).done(function()
			{
				logger.debug("Serial interrupt successfully triggered");
			});
		}

		self.triggerCancel = function(cancelKey, triggerType ,serialDeviceID)
		{
			var dfd = $.Deferred();

			setTimeout(function() {

				if(_currentlyPlayingInterrupt != null && _currentlyPlayingInterrupt.trigger.cancelKey)
				{
					var currentInterruptCancelKey = _currentlyPlayingInterrupt.trigger.cancelKey.trim();
					cancelKey = cancelKey.trim();

					//Make sure both are lowercase for keyboard
					if(triggerType == "KeyboardEvent")
					{
						currentInterruptCancelKey = currentInterruptCancelKey.toLowerCase();
						cancelKey = cancelKey.toLowerCase();
					}

					if(currentInterruptCancelKey == cancelKey && triggerType == _currentlyPlayingInterrupt.trigger.type)
					{
						self.cancelInterrupt().always(function(){
							dfd.resolve();
						});
					}
				}else {
					dfd.reject("No interrupt is playing");
				}				
			},5);

			return dfd;
		}

		function interruptIsCurrentlyPlaying(triggerKey, triggerType, serialDeviceID)
		{
			var result = false;

			if(_currentlyPlayingInterrupt)
			{
				
				logger.debug("An interrupt is playing");

				if (_currentlyPlayingInterrupt.trigger.triggerKey.toLowerCase() === triggerKey.toLowerCase() && _currentlyPlayingInterrupt.trigger.type === triggerType  && _currentlyPlayingInterrupt.trigger.type != "SerialEvent")
				{
					logger.debug("Trigger type and key match");
					result = true;
				} else if(triggerType == "SerialEvent"  && _currentlyPlayingInterrupt.trigger.type == triggerType && _currentlyPlayingInterrupt.trigger.triggerKey.trim() == triggerKey.trim() && _currentlyPlayingInterrupt.trigger.serialDeviceId == serialDeviceID)
				{
					logger.debug("Serial trigger match");
					result = true;
				} else {
					logger.debug("No interrupt trigger match");
					result = false;
				}

			}

			logger.debug("interruptIsCurrentlyPlaying = " +result);

			return result;
		}

		/**
		 * Attempts to trigger an interrupt associated with the provided trigger key and trigger type
		 * @param  triggerKey - The Trigger Key
		 * @param  triggerType - The Trigger Type (KeyboardEvent)
		 * @param  playIndefinitely - Whether or not to play the interrupt content until cancelled or until specified duration
		 * @param  highPriority - If true this interrupt will take precendence over low priority interrupts and will also trigger over other
		 *                        high priority interrupts
		 * @return JQuery Promise
		 */
		self.triggerInterrupt = function(triggerKey, triggerType, playIndefinitely, highPriority, triggerAdditionalData,serialDeviceID)
		{
			var dfd = $.Deferred();

			try
			{
				setTimeout(function() {
					
					// Only trigger the interrupt if:
					// 1: No current interrupt is playing
					// 2: An interrupt is playing but the new interrupt is high priority (high priority can only be overriden by other high priority interrupts) and it isn't already playing
					// 3: The current interrupt is low priority (low priority can be overridden by other low priorty and high priorty interrupts) and it isn't already playing
					if (!_isPlayingInterrupt || ((_isPlayingInterrupt && highPriority) || (_isPlayingInterrupt && !_highPriorityInterruptPlaying)) && !interruptIsCurrentlyPlaying(triggerKey,triggerType,serialDeviceID))
					{
						var interruptToPlay = getInterruptToPlay(triggerKey, triggerType, serialDeviceID);

						if (interruptToPlay !== null)
						{
							// clear the timeout
							clearTimeout(_scheduleTimerId);
							_scheduleTimerId = null;

							// Remember that an interrupt is playing
							_isPlayingInterrupt = true;
							_indefiniteInterruptPlaying = playIndefinitely;

							if (highPriority == "undefined")
							{
								_highPriorityInterruptPlaying = true;
							}
							else
							{
								_highPriorityInterruptPlaying = highPriority;
							}

							// Start to play interrupt
							playInterrupt(interruptToPlay.action.layout, triggerAdditionalData, playIndefinitely);
							_currentlyPlayingInterrupt = interruptToPlay;

							// If not set to play indeinitely then play until for the duration of the interrupt content
							if (!playIndefinitely && (interruptToPlay.action.duration > 0))
							{
								_scheduleTimerId = setTimeout(self.cancelInterrupt, interruptToPlay.action.duration * 1000)
							}

							dfd.resolve();
						}
						else
						{
							logger.warning("Unable to find requested interrupt. Interrupt Key: " + triggerKey + ", Type: " + triggerType);
							dfd.reject();
						}
					}
					else
					{
						logger.warning("Unable to trigger interrupt because an interrupt that cannot be cancelled is currently playing");
						dfd.reject();
					}

				}, 50);
			}
			catch (e)
			{
				logger.error("Error trying to play interrupt.");
				dfd.reject();
			}
			
			return dfd;
		};

		/**
		 * Returns true if an interrupt is currently playing, false otherwise
		 * @return {Boolean}
		 */
		self.isPlayingInterrupt = function()
		{
			return _isPlayingInterrupt;
		}

		self.currentInterruptIsPlayToLength = function()
		{
			return _isPlayingInterrupt && (_scheduleTimerId == null) && (_indefiniteInterruptPlaying == false);
		}

		/**
		 * Returns true if the current Interrupt can be overriden, false otherwise
		 * @return {Boolean}
		 */
		self.currentInterruptIsHighPriority = function()
		{
			return _highPriorityInterruptPlaying;
		}

		/**
		 * Used to cancel the current interrupt
		 * @return {JQuery Promise}
		 */
		self.cancelInterrupt = function()
		{
			var dfd = $.Deferred();

			// Cancel Interrupt
			setTimeout(function() {
				_currentlyPlayingInterrupt = null;
				// clear the timeout
				clearTimeout(_scheduleTimerId);

				if (_isPlayingInterrupt) {
					playScheduledContent();
					_isPlayingInterrupt = false;
					_highPriorityInterruptPlaying = false;
					if (_interruptPlayingCallback) {
						_interruptPlayingCallback();
					}
					logger.debug("Interrupt Cancelled");
					dfd.resolve();
				}
				else {
					logger.warning("Attempted to cancel interrupt but no interrupt is currently playing or current interrupt cannot be cancelled");
					dfd.reject();
				}
				
			}, 50);

			return dfd;
		};

		self.setCallback = function(cb) {
			_interruptPlayingCallback = cb;
		}
 
		/**
		 * Start playing a layout
		 * @param  {Layout Model of the interrupt to play}
		 */
		function playInterrupt(layoutToPlay, triggerAdditionalData, playIndefinitely)
		{
			_mediaPlayer.playInterrupt(layoutToPlay, triggerAdditionalData, playIndefinitely);
		}

		/**
		 * Switch to the default scheduled content
		 */
		function playScheduledContent()
		{
			// Load content
			_mediaPlayer.playScheduledContent();
		}

		/**
		 * Find an interrupt matching trigger key and trigger type
		 * @param  {The trigger key}
		 * @param  {The trigger type}
		 * @return {The interrupt model of matching interrupt of null if none found}
		 */
		function getInterruptToPlay(triggerKey, triggerType)
		{
			var interruptToPlay = null;

			for (var i = 0; i < _interrupts.length; i++)
			{
				var interrupt = _interrupts[i];
				
				if (interrupt.trigger.triggerKey.toLowerCase() === triggerKey.toLowerCase() && interrupt.trigger.type === triggerType  && interrupt.trigger.type != "SerialEvent")
				{
					interruptToPlay = interrupt;
					break;
				} else if(triggerType == "SerialEvent"  && interrupt.trigger.type == triggerType && interrupt.trigger.triggerKey.trim() == triggerKey.trim())
				{
					interruptToPlay = interrupt;
					break;
				}
			}

			return interruptToPlay;
		}
	}
	
	return new InterruptController();
}); 
rjs.define('gui/views/LayoutView',["properties/App.Settings", "gui/PlayUrlHelper", "gui/viewmodels/LayoutViewModel", "gui/views/MediaZoneView", "gui/views/TextZoneView", "system/io/File", "controllers/UserConfigurationController", "models/PlaybackStartEndReasons", "controllers/WidgetEventController", "controllers/InterruptController","system/network/TCPClientInterface", "controllers/ScreenTakeoverController"], function(Settings, PlayUrlHelper, LayoutViewModel, MediaZoneView, TextZoneView, File, UserConfigurationController, PlaybackStartEndReasons, WidgetEventController,InterruptController,TCPClientInterface, ScreenTakeoverController) {
    /**
     * LayoutView
     * @param {number} screenWidth  
     * @param {number} screenHeight 
     */
    function LayoutView(screenWidth, screenHeight, messagePort) {
        var self = this;
        self.zoneViews = [];
        var _isFullscreen = true;
        self._backgroundImage = null;
        var _backgroundFillColour = "#000000";
        self.layout = null;
        self.mainZone = null;
        self.previousLayoutId = null;
        var _initialized = false;
        var _syncConfig = null;
        var _takeoverSyncConfig = null;
        var _clientDetails = null;

        self._mediaSyncController = null;
        self.layoutName = null;

        var backgroundLayerId = "";

        var _messagePort = messagePort;
        var _schedulableZones = 0;
        var _schedulableZonePlaylistCompleteCount = 0;
        var _schedulableZonePlaylistCompleteArray = [];

        /**
         * Sets the current layout to play
         * @param {LayoutModel} layout
         * @param {PlaybackStartEndReasons} The reason for playback starting
         */
        self.setLayout = function(layout, playReason, triggerAdditionalData, syncConfig, takeoverSyncConfig, clientDetails, mediaSyncController, playInterruptIndefinitely, layoutName) {
            
            if(self._mediaSyncController == null) {
                self._mediaSyncController = mediaSyncController;
            }
            
            if (_initialized == false) {
                logger.debug('Adding event listeners to User Configuration Controller');
                // add user configuration change event listener
                UserConfigurationController.addEventListener(function(property, data) {
                    switch (property) {
                        case "mute":
                            $(self.zoneViews).each(function(index, zone) {
                                if (zone.viewModel.type == 'media')
                                    zone.setMute(data);
                            });
                            break;
                        case "volumeUp":
                            $(self.zoneViews).each(function(index, zone) {
                                if (zone.viewModel.type == 'media')
                                    zone.volumeUp();
                            });
                            break;
                        case "volumeDown":
                            $(self.zoneViews).each(function(index, zone) {
                                if (zone.viewModel.type == 'media')
                                    zone.volumeDown();
                            });
                            break;
                    }
                });
                _initialized = true;
            }

            _syncConfig = syncConfig;
            _takeoverSyncConfig = takeoverSyncConfig;
            _clientDetails = clientDetails;

            // stop the current layout playing. Reason for stopping the previous layout is the play reason for this layout
            stop(playReason);

            // Initialize Widget Event Controller
            WidgetEventController.initialize();           

            // enable video playback
            Settings.application.criticalSections.videoAvailable = true;

            self.layout = new LayoutViewModel(layout, screenWidth, screenHeight);

            // If we are the takeover view then we need to alter the layout externalId naming to prevent conflict w/ 2 takeover views having the same ID reference
            self.layoutName = (layoutName) ? self.layout.externalId + layoutName : self.layout.externalId;

            backgroundLayerId = "layout_" + self.layoutName + "_bg";

            // NOTE: Depending whether its a takeover or not it should prepend the canvas or the layout background
            if(self.layoutName.indexOf('takeover') > -1) {
                $('#canvas').prepend('<div id="layout_' + self.layoutName + '">'); // Add the div for all zones in this layout before any existing layouts so they are effectively behind the current layout in the view tree
            } else {
                $('#canvas').append('<div id="layout_' + self.layoutName + '">'); // Add the div for all zones in this layout before any existing layouts so they are effectively behind the current layout in the view tree
            }
            
            $("#layout_" + self.layoutName).append("<canvas id='" + backgroundLayerId + "' style='position:absolute;left:0px;top:0px;width:" + self.layout.width + "px;height:" + self.layout.height + "px;z-index: -999;'></canvas>"); // background for the layout

            _isFullscreen = (self.layout.zones.length == 1 && self.layout.zones[0].width == self.layout.width && self.layout.zones[0].height == self.layout.height);

            var layoutBackgroundCanvas = document.getElementById(backgroundLayerId);
            var backgroundCanvasContext = layoutBackgroundCanvas.getContext("2d");
            backgroundCanvasContext.globalAlpha = 1;

            // reset the stored background image
            self._backgroundImage = null;
            // add the background
            if (layout.backgroundAsset != null) {
                logger.debug('Loading background asset ' + layout.backgroundAsset.name);

                PlayUrlHelper.getMediaAssetPlayUrl(layout.backgroundAsset).done(function(url) {
                    self._backgroundImage = new Image();
                    self._backgroundImage.addEventListener('load', loadBackground.bind(null, playReason, triggerAdditionalData, playInterruptIndefinitely, layout), false);

                    self._backgroundImage.onerror = function() {
                        logger.error('Error loading background image');
                    };

                    logger.debug('Loading background image file ' + url);
                    self._backgroundImage.src = url;
                });

            } else {

                if (_isFullscreen) {
                    // set to black
                    _backgroundFillColour = "#000000";

                } else {
                    // set to white
                    _backgroundFillColour = "#ffffff";
                }

                preloadBackground(playReason);

                logger.debug("Test - adding click event");

                layoutBackgroundCanvas.onclick = function() {
                    logger.debug("CLICK");
                };

                Settings.application.criticalSections.videosPlaying = 0;
                loadZones(playReason, triggerAdditionalData, playInterruptIndefinitely, layout);
            }

        };

        function loadBackground(playReason, triggerAdditionalData, playInterruptIndefinitely, layout) {
            logger.debug('Backgroud image loaded - draw it');

            try {
                $('#layoutBackgroundCanvas').css('visibility', 'hidden');
                preloadBackground(playReason);
            } catch (e) {
                logger.error('Error whilst drawing background: ' + e.message);
            }

            // Load Zones
            loadZones(playReason, triggerAdditionalData, playInterruptIndefinitely, layout);
        }

        function preloadBackground(playReason) {
            layoutBackgroundCanvas = document.getElementById(backgroundLayerId);
            backgroundCanvasContext = layoutBackgroundCanvas.getContext("2d");
            // NOTE: Need to explicitly set the height and width of the canvas object as otherwise it defaults to 150x300 therefore when clearRect is called the zone it takes is incorrect / scaled up
            backgroundCanvasContext.canvas.height = self.layout.height;
            backgroundCanvasContext.canvas.width = self.layout.width;

            // if(playReason !== "Preloading Layout") {
                drawBackground();
            // }
        };

        function drawBackground() {
            if(self._backgroundImage !== null) {
                backgroundCanvasContext.drawImage(self._backgroundImage, 0, 0, self.layout.width, self.layout.height);
            } else {
                backgroundCanvasContext.fillStyle = _backgroundFillColour;
                backgroundCanvasContext.fillRect(0, 0, self.layout.width, self.layout.height);
            }
        };

        self.softTrigger = function(softTriggerConfig) {
            self.mainZone.softTrigger(softTriggerConfig);
        };

        /**
         * Loads each media zone in the layout view
         * @param {PlaybackStartEndReasons} The reason for playback starting
         * @return {[type]} [description]
         */
        function loadZones(playReason, triggerAdditionalData, playInterruptIndefinitely, layout) {
            _schedulableZones = 0;
            _schedulableZonePlaylistCompleteCount = 0;
            _schedulableZonePlaylistCompleteArray = [];

            // add the zones to the presentation
            $(self.layout.zones).each(function(index, zoneViewModel) {
                var zoneView = null;
                var callbacks = {};

                if(playReason == PlaybackStartEndReasons.INTERRUPT)
                {
                    callbacks.playlistComplete = windowPlaylistComplete;
                    _schedulableZonePlaylistCompleteArray[index] = false;
                }
                
                switch (zoneViewModel.type) {
                    case "media":
                        zoneView = new MediaZoneView(zoneViewModel, playReason, triggerAdditionalData, layout, index, messagePort, playInterruptIndefinitely, callbacks, self.layoutName);
                        zoneView.setMute(UserConfigurationController.isMuted());
                        zoneView.videoStart = function(zone) {
                            try {

                                // Cut out canvas - only required if canvas is in front of the layout
                                if (!Settings.application.features.canvasBehindLayout) {
                                    logger.debug('Cutting out Canvas');
                                    var layoutBackgroundCanvas = document.getElementById(backgroundLayerId);
                                    var backgroundCanvasContext = layoutBackgroundCanvas.getContext("2d");
                                    backgroundCanvasContext.globalAlpha = 0;
                                    backgroundCanvasContext.clearRect(zone.x, zone.y, zone.width, zone.height);
                                }
                            } catch (e) {
                                logger.error('Error whilst clearing background rectangle for video to play: ' + e.message);
                            }
                        };
                        zoneView.videoComplete = function(zone) {
                            try {

                                // Redraw canvas - only required if canvas is in front of the layout
                                if (!Settings.application.features.canvasBehindLayout) {
                                    logger.debug('Restoring Canvas');
                                    var layoutBackgroundCanvas = document.getElementById(backgroundLayerId);
                                    var backgroundCanvasContext = layoutBackgroundCanvas.getContext("2d");
                                    backgroundCanvasContext.globalAlpha = 1;

                                    if (self._backgroundImage != null) {
                                        var bgImageToScreenXScalar = self._backgroundImage.width / screenWidth;
                                        var bgImageToScreenYScalar = self._backgroundImage.height / screenHeight;

                                        // ensure that the image is not clipped outside of it bounds
                                        var clippingX = Math.max(bgImageToScreenXScalar * zone.x, 0);
                                        var clippingY = Math.max(bgImageToScreenYScalar * zone.y, 0);
                                        var clippingWidth = Math.min(bgImageToScreenXScalar * zone.width, self._backgroundImage.width);
                                        var clippingHeight = Math.min(bgImageToScreenYScalar * zone.height, self._backgroundImage.height);

                                        var placementX = zone.x;
                                        var placementY = zone.y;
                                        var placementWidth = zone.width;
                                        var placementHeight = zone.height;

                                        backgroundCanvasContext.drawImage(self._backgroundImage, clippingX, clippingY, clippingWidth, clippingHeight, placementX, placementY, placementWidth, placementHeight);

                                    } else {
                                        backgroundCanvasContext.fillStyle = _backgroundFillColour;
                                        backgroundCanvasContext.fillRect(zone.x, zone.y, zone.width, zone.height);
                                    }

                                    $(self.zoneViews).each(function(i, zone) {
                                        if (zone.viewModel != zoneViewModel) {
                                            if (zone.viewModel.type == "media" && zone.currentScheduledPlaylistMediaAsset && zone.currentScheduledPlaylistMediaAsset.mediaAsset != null && backgroundCanvasContext != null) {

                                                switch (zone.currentScheduledPlaylistMediaAsset.mediaAsset.contentType) {
                                                    case "video":
                                                    case "capture":
                                                        backgroundCanvasContext.globalAlpha = 0;
                                                        backgroundCanvasContext.clearRect(zone.viewModel.x, zone.viewModel.y, zone.viewModel.width, zone.viewModel.height);
                                                        break
                                                }
                                            }
                                        }
                                    });
                                }
                            } catch (e) {
                                logger.error('Error whilst re-drawing background ' + e.message);
                            }
                        };
                        _schedulableZones++;
                        break;
                    case "text":
                        zoneView = new TextZoneView(zoneViewModel, self.layoutName);
                        break;
                }

                if (zoneView != null) {
                    // add to the collection of layers
                    self.zoneViews.push(zoneView);
                }
            });

            self.mainZone = null;
            // find the 'main' zone - the 'main' zone is the largest zone, or the one closest to the origin
            $(self.zoneViews).each(function(index, zone) {
                if (zone.viewModel.type == 'media') {
                    if (self.mainZone == null) {
                        logger.debug('First Zone selected');
                        self.mainZone = zone;
                    } else {
                        // size of main zone
                        var mainZoneSize = self.mainZone.width * self.mainZone.height;

                        logger.debug('Main Zone Size: ' + mainZoneSize);

                        // size of the current zone
                        var zoneSize = zone.width * zone.height;

                        // select the biggest zone
                        if (zoneSize > mainZoneSize) {
                            logger.debug('1 Zone selected at position ' + zone.x);
                            self.mainZone = zone;
                        }

                        // if the zones are the same size - select the one closest to the origin
                        if (zoneSize == mainZoneSize) {
                            var mainZoneDistanceFromOrigin = Math.sqrt((self.mainZone.x * self.mainZone.x) + (self.mainZone.y * self.mainZone.y));
                            var zoneDistanceFromOrigin = Math.sqrt((zone.x * zone.x) + (zone.y * zone.y));

                            if (zoneDistanceFromOrigin < mainZoneDistanceFromOrigin) {
                                logger.debug('2 Zone selected at position ' + zone.x);
                                self.mainZone = zone;
                            }

                            // if the origins are the same select the one highest in the z-order
                            if (zoneDistanceFromOrigin == mainZoneDistanceFromOrigin) {
                                logger.debug('3 Zone selected at position ' + zone.x);
                                self.mainZone = zone;
                            }
                        }
                    }
                }
            });

            if (Settings.application.features.canTakeoverContent && _takeoverSyncConfig) {
                try {
                    if (playReason !== PlaybackStartEndReasons.PRELOAD_LAYOUT) { //For screen takeovers and preloading of assets we dont want to start playback
                        if(playReason !== PlaybackStartEndReasons.INTERRUPT) {
                            self.mainZone.scheduleTakeoverSync(_takeoverSyncConfig, _clientDetails).then(function (result) {
                                logger.debug('Takeover Sync Started');
                            });
                        }

                        // Start playing scheduled content straight away
                        self.play();
                    }
                } catch (e) {
                    logger.error(e);
                    self.play();
                }
            } else {
                //Enable sync on the main zone
                if (_syncConfig && _syncConfig.enabled) {
                    //Sync
                    self.mainZone.enableSync(_syncConfig, _takeoverSyncConfig, _clientDetails);
                }

                // play the layout once the background has been drawn
                self.play();
            }
        }

        function windowPlaylistComplete(index)
        {
            if(_schedulableZonePlaylistCompleteArray[index] != undefined && _schedulableZonePlaylistCompleteArray[index] == false)
            {
                _schedulableZonePlaylistCompleteCount++;
                _schedulableZonePlaylistCompleteArray[index] =true;
            }
            logger.debug("Schedulable zones complete count: " + _schedulableZonePlaylistCompleteCount + ", Total zones: " + _schedulableZones);

            //Play to length interrupt, so cancel it
            if(_schedulableZonePlaylistCompleteCount == _schedulableZones && InterruptController.currentInterruptIsPlayToLength())
            {
                logger.debug("Cancelling Interrupt");
                InterruptController.cancelInterrupt();
                return true;
            } else{
                //At least one zone needs to complete playback so windows should not stop playback
                return false;
            }
   
        }

        /**
         * Startings playing each media zone in the layout
         * @return {[type]} [description]
         */
        self.play = function() {
            $(self.zoneViews).each(function(index, zone) {
                zone.play();
            });
        }

        /**
         * Startings playing each media zone in the layout from a specific index
         * @return {[type]} [description]
         */
        self.resumeScheduledContentFromAsset = function(assetIndex) {
            drawBackground();
            $(self.zoneViews).each(function(index, zone) {
                zone.resumeScheduledContentFromAsset(assetIndex);
            });
        }

        /**
         * Stops all media zones in the layout view
         * @param {PlaybackStartEndReasons} The reason for stopping playback
         * @return {[type]} [description]
         */
        // NOTE: Refactor this to dynamically stop each MZV ID?
        function stop(endReason) {
            logger.debug('Stopping Layout View');

            // stop each zone
            $(self.zoneViews).each(function(i, zoneView) {
                zoneView.stop(endReason);
            });

            // remove all of the zone views from this layout view
            self.zoneViews = [];

            if(endReason == PlaybackStartEndReasons.INTERRUPT && ScreenTakeoverController.running) {
                ScreenTakeoverController.clearTakeoverTimers();
                ScreenTakeoverController.setSyncInProgress(false);
            }

            // is there a layout already playing?
            if (self.layout != null & self.layout != undefined) {

                WidgetEventController.stop(self.layoutName);

                if(self._backgroundImage) {
                    self._backgroundImage.removeEventListener('load', loadBackground);
                    self._backgroundImage = null;
                }

                // Try and make sure that the canvas object is cleaned up correctly - especially due to S3 player performance
                if (backgroundCanvasContext && endReason == "Preloading Takeover") {
                    backgroundCanvasContext.canvas.height = 0;
                    backgroundCanvasContext.canvas.width = 0;
                    layoutBackgroundCanvas.parentNode.removeChild(layoutBackgroundCanvas);
                    layoutBackgroundCanvas = null;
                    backgroundCanvasContext = null;
                }

                // Remove the layout and all zones
                $('#layout_' + self.layoutName).off().remove();

                // Reset the layoutName reference
                self.layoutName = null;

            }

            logger.debug('Unlocking Video Viewers');
            Settings.application.criticalSections.videosPlaying = 0;

            TCPClientInterface.closeAll();

            logger.debug('Layout View Stopped');
        }

        /**
         * Hide the current layout entirely
         */
        self.hide = function() {
            $("#layout_" + self.layoutName).css('display', 'none');
        };
        
        /**
         * Show the current layout
         */
        self.show = function () {
            $("#layout_" + self.layoutName).css('display', 'block');
        };

        self.clearBackgroundCanvas = function() {
            // Nothing to do here because we are using a layout background specific to this layout group
        };

        self.drawBackgroundCanvas = function() {
            // Nothing to do here because we are using a layout background specific to this layout group
        };

        /**
         * Stops playback of the current layout
         * @return {[type]} [description]
         */
        self.stop = function(endReason) {
            stop(endReason);
        };

        /**
         * Skip to the next asset in the main zone
         * @return {[type]} [description]
         */
        self.skip = function() {
            logger.debug('Layout - skip');

            self.mainZone.skip();
        };

        /**
         * Go back to the previous asset in the main zone
         * @return {[type]} [description]
         */
        self.back = function() {
            logger.debug('Layout - back');

            self.mainZone.back();
        };
    }

    return LayoutView;
});
rjs.define('gui/views/MediaPlayerView',["system/DateTime", "gui/views/LayoutView", "models/PlaybackStartEndReasons", "controllers/MediaSyncController", "system/power/PowerInterface"], function(DateTime, LayoutView, PlaybackStartEndReasons, MediaSyncController, PowerInterface) {
    /**
     * MediaPlayerView
     * --------------------
     * Exposes the Media Player to the rest of the application
     * @param {Object} Function callbacks
     */
    function MediaPlayerView(callbacks, messagePort) {
        var self = this;
        var _callbacks = callbacks;
        var _messagePort = messagePort;
        self.takeoverView = new LayoutView(document.documentElement.clientWidth, document.documentElement.clientHeight, messagePort);
        self.layoutView = new LayoutView(document.documentElement.clientWidth, document.documentElement.clientHeight, messagePort);
        self.secondTakeoverView = new LayoutView(document.documentElement.clientWidth, document.documentElement.clientHeight, messagePort);
		self.secondTakeoverViewPlaying = false;
        self.activeTakeoverViewer = self.takeoverView;
		self.inactiveTakeoverViewer = self.secondTakeoverView;
        var takeoverLayoutIdentifier = "_takeover";
        var _scheduleTimerId = 0;
        var _content = null;
        var _syncConfig = null;
        var _takeoverSyncConfig = null;
        var _clientDetails = null;
        var _mediaSyncController = MediaSyncController;
        var zonesPlaying = 0;

		var TAKEOVER_TRANSITION_DELAY_TIMEOUT = 1000;

        /**
         * Takes a content model and starts playing the currently scheduled content.
         * @param  {[type]} content [description]
         * @return {[type]}         [description]
         */
        self.loadContent = function(content, syncConfig, takeoverSyncConfig, clientDetails) {
            _content = content;
            _syncConfig = syncConfig; //This is for normal sync
            _takeoverSyncConfig = takeoverSyncConfig; //This is for screen takeovers
            _clientDetails = clientDetails;
            setCurrentSchedule();
        };

        /**
         * Takes a layout model representing the interrupt to play
         * @param  {[type]} layout [description]
         * @return {[type]}        [description]
         */
        self.playInterrupt = function(layout, triggerAdditionalData, playInterruptIndefinitely) {
            if (PowerInterface.screenOn === false) {
                logger.warning("playInterrupt() called but screen is off. Ignoring request");
                return;
            }

            self.layoutView.setLayout(layout, PlaybackStartEndReasons.INTERRUPT, triggerAdditionalData, _syncConfig, null, _clientDetails, null, playInterruptIndefinitely);

            // If using interrupts and screen takeovers then we need to make sure the relevant takeover view is stopped ..
			if(self.activeTakeoverViewer && self.activeTakeoverViewer.layout) {
				self.activeTakeoverViewer.stop(PlaybackStartEndReasons.INTERRUPT);
			}
        };

        self.playScreenTakeover = function(layout, playReason, triggerAdditionalData) {

			if (PowerInterface.screenOn === false) {
                logger.warning("playScreenTakeover() called but screen is off. Ignoring request");
                return;
            }

			if(self.secondTakeoverViewPlaying) {
                // Listen for first asset to start playing
				self.activeTakeoverViewer.mainZone.registerAssetPlayingEventListener(onSecondTakeoverPlaying);
			} else {
				// Listen for first asset to start playing
				self.activeTakeoverViewer.mainZone.registerAssetPlayingEventListener(onTakeoverPlaying);
			}

            // Start playback (currently behind the scheduled content)
            self.activeTakeoverViewer.play();
		};

		self.switchActiveTakeoverViewers = function() {
            try {
                logger.debug("MediaPlayerView: Switching active takeover viewers");
                var temp = self.activeTakeoverViewer;
                self.activeTakeoverViewer = self.inactiveTakeoverViewer;
                self.inactiveTakeoverViewer = temp;                
            } catch(e) {
                logger.warning("MediaPlayerView: Issue switching active takeover viewers");
            }
		};

        /**
         * Plays the current scheduled layout based on the existing content model.
         * @return {[type]} [description]
         */
		self.playScheduledContent = function(endReason) {
			setCurrentSchedule(endReason);
		};

        self.playPreloadedContent = function(takeoverSyncConfig, clientDetails, playReason, triggerAdditionalData) {
			if (PowerInterface.screenOn === false) {
                logger.warning("playPreloadedContent() called but screen is off. Ignoring request");
                return;
            }

            // Listen for first asset to start playing
			self.layoutView.mainZone.registerAssetPlayingEventListener(onScheduledContentPlaying);
			// Start playback (currently behind the scheduled content)
			self.layoutView.play(takeoverSyncConfig, clientDetails);
        };

        self.preloadScheduledContent = function(playReason, stopTakeoverViews) {
			if (PowerInterface.screenOn === false) {
                logger.warning("preloadScheduledContent() called but screen is off. Ignoring request");
                return;
            }

            // Stop takeover views immediately - used by LG & Brightsign as they have Screen Capture Overlay during transitions
			if (stopTakeoverViews === true) {
				// Stop the current views
				if (self.activeTakeoverViewer.layout) {
					self.activeTakeoverViewer.stop(playReason);
				}
				
				if (self.inactiveTakeoverViewer.layout) {
					self.inactiveTakeoverViewer.stop(playReason);
				}
			}

            setCurrentSchedule(playReason);
        };

        self.preloadScheduledContentForOverlayPlayers = function(playReason) {
			if (PowerInterface.screenOn === false) {
                logger.warning("preloadScheduledContent() called but screen is off. Ignoring request");
                return;
            }

			

			// Preload the Scheduled Content
            setCurrentSchedule(playReason);
        };


        self.preloadScreenTakeover = function(layout, playReason, triggerAdditionalData, secondTakeoverEvent, stopLayoutView) {
			if (PowerInterface.screenOn === false) {
                logger.warning("preloadScreenTakeover() called but screen is off. Ignoring request");
                return;
            }

			if(secondTakeoverEvent) {
				self.secondTakeoverViewPlaying = true;
			} else {
				self.secondTakeoverViewPlaying = false;
			}

            // Stop layout views immediately - used by LG & Brightsign as they have Screen Capture Overlay during transitions
			if (stopLayoutView === true) {
				if (self.layoutView.layout) {
					self.layoutView.stop(playReason);
				}
				if (self.inactiveTakeoverViewer.layout) {
					self.inactiveTakeoverViewer.stop(playReason);
				}
			}

			self.activeTakeoverViewer.setLayout(layout, playReason, null, null, _takeoverSyncConfig, _clientDetails, null, null, takeoverLayoutIdentifier);

            takeoverLayoutIdentifier = (takeoverLayoutIdentifier == "_takeover") ? "_takeover2" : "_takeover";

            hideTakeoverView();
        };

        function onTakeoverPlaying(playReason) {          
            resetZonesPlaying();
            hideLayoutView();
			self.activeTakeoverViewer.mainZone.removeAssetPlayingEventListener();
			setTimeout(function() {
				self.layoutView.clearBackgroundCanvas();
				self.layoutView.stop(playReason);
                self.inactiveTakeoverViewer.stop(playReason);
                hideOverlay();
				showTakeoverView();
			}, TAKEOVER_TRANSITION_DELAY_TIMEOUT);
		}

		function onScheduledContentPlaying(playReason, zoneView) {
            zonesPlaying++;

			self.layoutView.drawBackgroundCanvas();

			zoneView.removeAssetPlayingEventListener();

            if(zonesPlaying >= self.layoutView.zoneViews.length) {
                resetZonesPlaying();
                setTimeout(function() {
					self.activeTakeoverViewer.stop(playReason);
                    self.inactiveTakeoverViewer.stop(playReason);
                    hideOverlay();
                    playScheduledTickerZones();
                }, TAKEOVER_TRANSITION_DELAY_TIMEOUT);
            }
		}

		function onSecondTakeoverPlaying(playReason) {          
            resetZonesPlaying();
            self.activeTakeoverViewer.mainZone.removeAssetPlayingEventListener();
            setTimeout(function() {
				self.layoutView.clearBackgroundCanvas();
				self.layoutView.stop(playReason);
				self.inactiveTakeoverViewer.stop(playReason);
                hideOverlay();
				showTakeoverView();
			}, TAKEOVER_TRANSITION_DELAY_TIMEOUT);
		}

        function resetZonesPlaying() {
            zonesPlaying = 0;
        };

        function hideOverlay() {
            $('#screenTakeover_overlay').css('visibility', 'hidden');
            $('#screenTakeover_overlay').attr('src', ''); //unset the source
        };

        // NOTE: If scheduled content is a layout then we need to conditionally hide the background when we are ready ..
		function hideLayoutView() {
			if(self.layoutView._backgroundImage) {
				self.layoutView.hide();
			}    
		}

		function hideTakeoverView() {
			if(self.layoutView._backgroundImage) {
				self.activeTakeoverViewer.hide();
			}
		};

		// NOTE: If scheduled content is a layout then we need to hide the takeover view and show the background early ..
		function showLayoutView() {
			if(self.layoutView._backgroundImage) {
				self.layoutView.show();
			}
		};

		function showTakeoverView() {
			if($('#layout_' + self.activeTakeoverViewer.layoutName).css('display') == 'none') {
				self.activeTakeoverViewer.show();
			}
		};

        self.moveToAssetMainZone = function(assetIndex) {
            self.activeTakeoverViewer.mainZone.logProofOfPlay();
            self.activeTakeoverViewer.mainZone.moveToAssetMainZone(assetIndex);
        };
       
        self.resumeScheduledContentFromAsset = function (playReason, assetIndex, triggerAdditionalData) {
            // Listen for all assets to start playing?
            $(self.layoutView.zoneViews).each(function (index, zoneView) {
                if (zoneView.registerAssetPlayingEventListener) {
                    zoneView.registerAssetPlayingEventListener(onScheduledContentPlaying);
                    zoneView.resumeScheduledContentFromAsset(assetIndex[zoneView.id], playReason);
                } else if (zoneView) {
                    // Instead - if the asset is a ticker ..
                    scheduleTickerZone(zoneView);
                }
            });
        };

        var scheduledTickerZoneViews = [];
        function scheduleTickerZone(zoneView) {
            zonesPlaying++; //Register ZV as playing due to onScheduledContentPlaying CB logic for media asset tracking
            
            // Add the ZV to a temp array for tracking as it has not yet started to play
            scheduledTickerZoneViews.push(zoneView);
        };

        // NOTE: Cannot start playing the ticker zones in the resumeScheduledContentFromAsset method due to the TextZoneView scaling logic, meaning that the first ticker plays in a different position to the rest of the playback content
        function playScheduledTickerZones() {
            $(scheduledTickerZoneViews).each(function(index, zoneView) {
                zoneView.play();
            });

            //  .. Then reset the views
            scheduledTickerZoneViews = [];
        };
        
        /**
         * Adjust the dimensions of the currently playing layout view
         * @param  {number} width  
         * @param  {number} height 
         * @return {[type]}        [description]
         */
        self.adjustLayoutViewDimensions = function(width, height) {
			self.layoutView.stop(PlaybackStartEndReasons.ADJUST_LAYOUT_DIMENSIONS);
			self.layoutView = new LayoutView(width, height, _messagePort);
            self.activeTakeoverViewer.stop(PlaybackStartEndReasons.ADJUST_LAYOUT_DIMENSIONS);
            self.activeTakeoverViewer = new LayoutView(width, height, _messagePort);
            self.inactiveTakeoverViewer.stop(PlaybackStartEndReasons.ADJUST_LAYOUT_DIMENSIONS);
            self.inactiveTakeoverViewer = new LayoutView(width, height, _messagePort);
        };

        /**
         * Play all media zones in the layout
         * @return {[type]} [description]
         */
        self.play = function() {
			self.layoutView.play();
        };

        /**
         * Pause the main media zone
         * @return {[type]} [description]
         */
        self.pause = function() {
            // TODO: Implement a pause?
            logger.warning("Media Player View: Pause called but not implemented.");
        };

        /**
         * Stops playback of the layout and all zones within in it
         * @param  {[type]} reason [description]
         * @return {[type]}        [description]
         */
		self.stop = function(reason) {
            self.layoutView.stop(reason);
			if (self.activeTakeoverViewer.layout) {
				self.activeTakeoverViewer.stop(reason);
			}
			if (self.inactiveTakeoverViewer.layout) {
				self.inactiveTakeoverViewer.stop(reason);
			}
		};

        /**
         * Skips to the next asset in the mainz one in the current layout
         * @return {[type]} [description]
         */
        self.skip = function() {
			self.layoutView.skip();
        };

        /**
         * Plays the previous asset in the main zone in the current layout
         * @return {[type]} [description]
         */
        self.back = function() {
			self.layoutView.back();
        };

        /**
         * Rotates the layout view - not implemented
         * @return {[type]} [description]
         */
        self.rotate = function() {
            // TODO: Implement rotation using this method?
            logger.warning("Media Player View: Rotate called but not implemented.");
        };

        self.softTrigger = function(softTriggerConfig) {
            // soft trigger
            if (softTriggerConfig != null)
                _layoutView.softTrigger(softTriggerConfig);
        };

        /**
         * From the content module it iterates over
         * the schedules and determines the scheduled
         * layout that should be playing now.
         */
		function setCurrentSchedule(reason) {

			if(!reason) {
				reason = PlaybackStartEndReasons.SCHEDULE;
			}

            clearTimeout(_scheduleTimerId);

            if (PowerInterface.screenOn === false) {
                logger.warning("setCurrentSchedule() called but screen is off. Ignoring request");
                return;
            }

            // find the current schedule
            var currentSchedule = null;
            $(_content.schedules).each(function(i, schedule) {
                try {
                    if (schedule.start <= DateTime.now() && schedule.end >= DateTime.now()) {
                        currentSchedule = schedule;
                    }
                } catch (e) {

                }
            });

            if (currentSchedule != null) {
                try {
                    $('#userMessage').css('visibility', 'hidden');

                    // load the current scheduled layout
                    self.layoutView.setLayout(currentSchedule.layout, reason, null, _syncConfig, _takeoverSyncConfig, _clientDetails, _mediaSyncController);

                    // Add the settings selection square - Not Used
                    //addSettingsSelectionSquare();

                } catch (e) {
                    logger.error(e.message);
                }
            } else {
                $('#userMessage').css('visibility', 'visible');

                //UserMessageController.postMessage('A valid schedule could not be found please check the date time is correct');
            }

            // give focus to the remote focus anchor to capture events from the remote control
            $('#remoteFocus').focus();

            // calculate when the next layout should play
            var timespanMS = currentSchedule.end.subtract(DateTime.now());

            // set the timer to play the schedule at the appropriate time
            _scheduleTimerId = setTimeout(setCurrentSchedule, timespanMS);
        }

    }

    return MediaPlayerView;
});
rjs.define('controllers/ApplicationUpdateController',["properties/App.Settings", "properties/App.Properties", "system/plugins/DownloadPlugin", "system/io/File", "system/power/PowerInterface"], function(Settings, Properties, DownloadPlugin, File, PowerInterface) {
    /**
     * The application update controller is responsible for checking for and performing application updates.
     */
    function ApplicationUpdateController() {
        var self = this;
        self.updateInProgress = false;

        self.start = function() {
            var dfd = $.Deferred();

            // Check for an update straight away
            checkForUpdate();

            // And then every so often (once per day)
            setInterval(function() {
                checkForUpdate();
            }, (Settings.application.remoteApplicationUpdates.frequency * 1000));

            return dfd;
        };

        function checkForUpdate() {
            if (self.updateInProgress) {
                logger.warning('Skipping update check. Update is already in progress');
                return;
            }

            logger.debug('Checking if application update is required...');

            var remoteUrl = Settings.application.remoteApplicationUpdates.location + Settings.application.remoteApplicationUpdates.configFileName;

            $.ajax({
                url: remoteUrl,
                contentType: 'application/xml',
                timeout: 10000,
                cache: false
            }).done(function(xmlDoc, textStatus, jqXHR) {
                logger.info('Update config downloaded successfully');

                var updatedVersion = $(xmlDoc).find('application version').first().text();
                var updatedBuildNumber = $(xmlDoc).find('application buildnumber').first().text();
                var updatedAppName = $(xmlDoc).find('application name').first().text();

                logger.debug('APP UPDATES: App Name: ' + updatedAppName);

                // Check the new build against the old build to determine if an update is required
                if (updatedBuildNumber > Properties.appBuildNumber) {
                    logger.info('Application update: REQUIRED. Current version: ' + Properties.appBuildNumber + '. New version: ' + updatedBuildNumber);

                    // Do Update
                    var updatedFileName = updatedAppName + '.zip';
                    var remoteFileUrl = Settings.application.remoteApplicationUpdates.location + updatedFileName;
                    var localFileUrl = Settings.application.fileSystem.tempDirectory + "/" + updatedFileName;
                    updateApplication(remoteFileUrl, localFileUrl);
                } else {
                    logger.info('Application update: NOT REQUIRED. Current version: ' + Properties.appBuildNumber + '. New version: ' + updatedBuildNumber);
                }
                // }).fail(function() {
                //     logger.error('Could not read update file');
                //     try {
                //         File.delete(localFile);
                //     } catch (e) {
                //         logger.error('Error whilst removing a failed update config file ' + e.message);
                //     }
                // });
            }).fail(function(remoteFile, localFile) {
                logger.info('Update config download failed');
                // remove the partially downloaded file
                try {
                    File.delete(localFile);
                } catch (e) {
                    logger.error('Error whilst removing a failed update config file ' + e.message);
                }
            });
        }

        function updateApplication(remoteFileUrl, localFileUrl) {
            // We are starting to do an update - skip any checks while this in progress
            self.updateInProgress = true;

            // Download the zipped client
            logger.info('Downloading update file: ' + remoteFileUrl);
            var downloadPlugin = new DownloadPlugin();
            downloadPlugin.downloadFile(remoteFileUrl, localFileUrl).done(function(remoteFile, localFile) {
                logger.info('Update downloaded successfully. Applying update...');
                logger.info('File: ' + localFile);

                // Unzip and reload the app
                var targetDir = Settings.application.fileSystem.storageDirectory;
                File.unzip(localFile, targetDir).done(function() {
                    logger.info('Update applied. Restarting application...');

                    // Delete update file
                    try {
                        File.delete(localFile);
                    } catch (e) {
                        logger.error('Error whilst removing the update file: ' + e.message);
                    }

                    // Reload the application
                    self.updateInProgress = false;
                    // Do a full device reboot. This will ensure the autorun.brs is reloaded.
                    PowerInterface.reboot()
                        .fail(function() {
                            location.reload(); // if the reboot requests then just do a reload of the app - this will reload index.html and all assets.
                        });
                }).fail(function() {
                    logger.error('Error applying update.');
                    self.updateInProgress = false;

                    // remove the partially downloaded file
                    try {
                        File.delete(localFile);
                    } catch (e) {
                        logger.error('Error whilst removing the update file: ' + e.message);
                    }
                });
            }).fail(function(remoteUrl, localFile) {
                logger.info('Error downloading update file');
                self.updateInProgress = false;

                // remove the partially downloaded file
                try {
                    File.delete(localFile);
                } catch (e) {
                    logger.error('Error whilst removing the update file: ' + e.message);
                }
            });
        }
    }

    return ApplicationUpdateController;
});
rjs.define('models/KeyboardShortcutsModel',["system/network/NetworkInterface","properties/App.Settings","models/ConfigurationModel", "system/io/File"],function(NetworkInterface, Settings, ConfigurationModel, File) {
	/**
	 * [KeyboardShortcutsModel description]
	 */
	function KeyboardShortcutsModel() {
		var self = this;
        var _callbacks = null;
        var volume = 50;

		/**
		 * Use this method to setup keyboard shortcuts
		 * @return {[type]} [description]
		 */
		self.setup = function(callbacks) {
            var _callbacks = callbacks;

			// Nothing to do here
            $(document).keydown(function(e){
                logger.debug("Key down: " + e.keyCode);
                switch(e.keyCode)
                {
                    case 73:
                        //i
                        if(e.ctrlKey)
                        {
                            if(_callbacks)
                                _callbacks.showInfo();
                        } else {
                            if(_callbacks)
                                _callbacks.default(e);
                        }
                        break;
                    case 83:
                        //s
                        if(e.ctrlKey)
                        {
                           if(_callbacks)
                              _callbacks.showSetup();
                            
                        } else {
                            if(_callbacks)
                                _callbacks.default(e);
                        }
                        break;
                     default:
                        if(_callbacks)
                            _callbacks.default(e);
                        //Nothing
                        break;
                }             
            });

            $(document).keyup(function(){

            });
		};
		
	}

	return KeyboardShortcutsModel;
});

rjs.define('core/SLWebBasedInterruptsAPIInterface',["properties/App.Settings", "system/DateTime", "controllers/UserConfigurationController", "models/PusherSubscriptionModel"], function(Settings, DateTime, UserConfigurationController, PusherSubscriptionModel)
{
	/**
	 * This modules checks the web-based interrupts API for configuration updates.
	 * The configuration includes the pusher key and details of the channels the player should
	 * listen for updates against.
	 */
	function SLWebBasedInterruptsAPIInterface()
	{
		var self = this;

		var _options = null;
		var _lastModifiedDate = null;
		var _lastConfiguration = null;
		var _configurationCheckTimer = null;

		/**
		 * Initializes the The API Interface. Starts the configuration check process
		 * @param  {Object} options any initialization options
		 * @return {Jquery Promise} Resolved on success or rejected on failure
		 */
		self.initialize = function(lastConfiguration, options)
		{
			logger.debug("Initializing SLWebBasedInterruptsAPIInterface");
			var dfd = $.Deferred();
			
			setTimeout(function()
			{
				// Initialize basic settings
				if (lastConfiguration != null) {
					_lastConfiguration = lastConfiguration;
					_lastModifiedDate = lastConfiguration.lastModifiedDate;
					
				}
				_options = options;

				// API started so resolve initialisation
				dfd.resolve();

			}, 50);

			return dfd;
		};

		self.stop = function()
		{
			_lastModifiedDate = null;
			_lastConfiguration = null;
		}

		/**
		 * Checks for a new configuration
		 * @return {[type]} [description]
		 */
		self.checkForConfigurationUpdate = function()
		{

			getConfigurationLastModifiedDate()
			.done(function(lastModifiedDate) {

				// Do we need to perform an update?
				if (_lastModifiedDate == null || lastModifiedDate.getTime() != _lastModifiedDate.getTime())
				{
					getConfiguration()
					.done(function(pusherConfiguration) {

						// Update last modified date and last configuration
						_lastConfiguration = pusherConfiguration;
						_lastModifiedDate = pusherConfiguration.lastModifiedDate;

						// Store updated pusher settings locally
						//Settings.application.webBasedInterrupts.pusherSettings = pusherConfiguration;n;
						//Settings.application.webBasedInterrupts.pusherSettings = _lastModifiedDate;

						// Do callback
						_options.configurationCallback(pusherConfiguration);

					})
					.fail(function(){
						logger.error("SL Web Based Interrupts API Interface: Error performing configuration update.");

					});
				}
				else
				{
					// No update available - schedule next check
					
				}
			})
			.fail(function() {
				logger.error("SL Web Based Interrupts API Interface: Error checking for configuration update.");
			});
		};

		/**
		 * Calls HEAD /Configuration on the API and returns the Last-Modified Header
		 * @param  {number} Player serial number
		 * @return {Jquery  Promise(DateTime)} The last modified date of the web based interrupt configuration
		 */
		function getConfigurationLastModifiedDate() {
			var dfd = $.Deferred();

			// Do the call
			var apiCall = executeAPICall('playerconfiguration', { type : "HEAD" });

			// On Success
			apiCall.done(function(data, textStatus, jqXHR) {
				var lastModifiedDate = new Date(jqXHR.getResponseHeader('Last-Modified'));
				dfd.resolve(lastModifiedDate);
			});

			// On Failure
			apiCall.fail(function(jqXHR, textStatus, errorThrown) {
				// log the error
				logAPIError(this.url, this.data, jqXHR.status, errorThrown, jqXHR.responseText);
				dfd.reject();
			});

			return dfd;
		}

		/**
		 * Gets the Web-based Interrupt Configuration. Returns the configuration object.
		 * @param  {number} Player Serial Number
		 * @return {Jquery Promise(PusherSubscrptionModel)}
		 */
		function getConfiguration()
		{
			var dfd = $.Deferred();

			// Do the api call
			var apiCall = executeAPICall('playerconfiguration');
			
			// On Success
			apiCall.done(function(data, textStatus, jqXHR) {
				var lastModifiedDate = new Date(jqXHR.getResponseHeader('Last-Modified'));
				data.lastModifiedDate = lastModifiedDate;
				dfd.resolve(new PusherSubscriptionModel(data));
			});
			
			// On Failure
			apiCall.fail(function(jqXHR, textStatus, errorThrown) {
				// log the error
				logAPIError(this.url, this.data, jqXHR.status, errorThrown, jqXHR.responseText);
				dfd.reject();
			});

			return dfd;
		}


		/**
		 * Executes API call using provided operation name. Optionally takes a settings object that overrides default options.
		 * @param  {String} operationName [description]
		 * @param  {number} serialNo	  [description]
		 * @param  {Object} settings      [description]
		 * @return {[type]}               [description]
		 */
		function executeAPICall(operationName, settings) {
			try {

				logger.debug('SL Web-Based Interrupts API: executeAPICall ' + operationName + ', URL: ' + _options.url);

				// Default Settings
				var type = 'GET';
				var async = true;
				var data = null;
				var contentType = 'application/json';
				

				if (settings != undefined)
				{
					if (settings.type != undefined)
						type = settings.type;

					if (settings.async != undefined)
						async = settings.async;

					if (settings.data != undefined)
						data = JSON.stringify(settings.data);
				}

				if (type === 'GET')
				{
					var dataType = 'json';
				}
				
				return $.ajax({
					url : _options.url + "/" + operationName + "/" + _options.serialNo,
					type : type,
					contentType : contentType,
					dataType : dataType,
					async : async,
					data : data,
					timeout : 60000,
					beforeSend : function(xhr, settings) {
						// Can set headers. Not required as this API is unauthenticated
					}
				});
			} catch(e) {
				logger.error('SL Web-Based Interrupts API: Error executing api call ' + e.message);
				return null;
			}
		}

		/**
		 * Logs as much detail as possible about an API Error
		 * @param  {[type]} requestUrl      [description]
		 * @param  {[type]} requestData     [description]
		 * @param  {[type]} responseStatus  [description]
		 * @param  {[type]} responseMessage [description]
		 * @param  {[type]} responseText    [description]
		 * @return {[type]}                 [description]
		 */
		function logAPIError(requestUrl, requestData, responseStatus, responseMessage, responseText) {
			var logData = {
				url : requestUrl,
				requestData : requestData,
				responseStatus : responseStatus,
				responseMessage : responseMessage,
				responseBody : responseText
			};

			logger.error("SL Web-Based Interrupts API: " + JSON.stringify(logData));
		}

		
	}

	return SLWebBasedInterruptsAPIInterface;
});


rjs.define('models/WebBasedInterruptMessageDataModel',[],function() {
	/**
	 * Web-Based Interrupt Message Data Model 
	 * @param {[type]} data [description]
	 */
	function WebBasedInterruptMessageDataModel(data) {
		var self = this;

		self.triggerType = data.triggerType;
		self.triggerData = data.triggerData;
	}

	return WebBasedInterruptMessageDataModel;
});
rjs.define('models/WebBasedInterruptMessageModel',["models/WebBasedInterruptMessageDataModel"], function(WebBasedInterruptMessageDataModel) {
	/**
	 * Web-Based Interrupt Message Model 
	 * @param {[type]} message [description]
	 */
	function WebBasedInterruptMessageModel(message) {
		var self = this;

		self.id = message.id;
		self.type = message.type;
		self.data = new WebBasedInterruptMessageDataModel(message.data);
		self.additionalData = message.data.additionalData;
	}

	return WebBasedInterruptMessageModel;
});
rjs.define('controllers/WebBasedInterruptsController',["controllers/InterruptController", "properties/App.Settings", "core/SLWebBasedInterruptsAPIInterface", "models/WebBasedInterruptMessageModel", "models/PusherSubscriptionModel"], function(InterruptController, Settings, SLWebBasedInterruptsAPIInterface, WebBasedInterruptMessageModel, PusherSubscriptionModel) {
	function WebBasedInterruptsController(){

		var self = this;

		var _started = false;
		var _initialized = false;
		var _isRunning = false;
		var _options = null;
		var _pusherConfiguration = null;
		var _pusher = null;
		var _apiInterface = null;

		/**
		 * [initialize description]
		 * @param  {[type]} options [description]
		 * @return {[type]}         [description]
		 */
		self.initialize = function(options){
			var dfd = $.Deferred();

			_options = options;

			logger.info("Initializing Web based interrupts controller");

			if (_options.serialNo != null)
			{
				// unsubsribe from previous channels
				if (_pusherConfiguration != null)
				{
					cancelSubscriptions(_pusherConfiguration);
				}

				// if we already have a configuration then we can initialize the listeners straight away
				if (options.pusherSettings != null)
				{
					logger.debug("Existing pusher configuration found");
					_pusherConfiguration = new PusherSubscriptionModel(options.pusherSettings);
					configurePusherSubscriptions(_pusherConfiguration);
				}
				
				_initialized = true;
				_started = true;
				dfd.resolve();
			
			}
			else
			{
				setTimeout(function(){
					logger.warning("Error: Could not initialize Web Based Interrupts - Player Serial Number Not defined. Is the player registered?");
					dfd.reject();
				}, 50);
			}

			return dfd;
		};

		/**
		 * [configurationCallbackHandler description]
		 * @param  {[type]} pusherConfiguration [description]
		 * @return {[type]}                     [description]
		 */
		function configurationCallbackHandler(pusherConfiguration)
		{
			logger.info("Web Based Interrupts Controller: Configuration Callback Handler called");

			// unsubsribe from previous channels
			if (_pusherConfiguration != null)
			{
				cancelSubscriptions(_pusherConfiguration);
			}

			_pusherConfiguration = pusherConfiguration;

			// resubscribe based on new configuration
			configurePusherSubscriptions(pusherConfiguration);
		}

		/**
		 * [configurePusherSubscriptions description]
		 * @param  {[type]} pusherConfiguration [description]
		 * @return {[type]}                     [description]
		 */
		function configurePusherSubscriptions(pusherConfiguration)
		{
			logger.info("Web Based Interrupts Controller: Configuring Pusher Subscriptions");

			// We don't want to create a whole new pusher connection if we alredy have one.
			// Disable stats module as this violates CSP on chrome and we don't need it anyway
			if (_pusher === null)
			{
				// Added suppot for non-default Pusher cluster, which can be configured in the App.Settings.
				// If not specified, will default to mt1 (default cluster)
				var pusherCluster = Settings.application.webBasedInterrupts.cluster != null
					? Settings.application.webBasedInterrupts.cluster
					: 'mt1';

				logger.info("Pusher: Initializing new pusher connection");
				_pusher = new Pusher(pusherConfiguration.pusherKey, { 
					cluster: pusherCluster,
					disableStats: true 
				});
			}

			$.each(pusherConfiguration.channels, function(index, channelId){
				var channel = _pusher.subscribe(channelId);
				channel.bind('WebBasedInterrupt', processPusherMessage);
				logger.info("Pusher: Subscribed to channel: " + channelId);
			});
		}

		/**
		 * [unbindPusherSubscriptions description]
		 * @param  {[type]} pusherConfiguration [description]
		 * @return {[type]}                     [description]
		 */
		function cancelSubscriptions(pusherConfiguration)
		{	
			logger.info("Web Based Interrupts Controller: Cancelling Subscriptions");

			$.each(pusherConfiguration.channels, function(index, channelId){
				var channel = _pusher.unsubscribe(channelId);
			});
		}

		/**
		 * [processPusherMessage description]
		 * @param  {[type]} msg [description]
		 * @return {[type]}     [description]
		 */
		function processPusherMessage(msg)
		{
			if (_started)
			{
				logger.info("Pusher message received");
				//Cast message
				var message = new WebBasedInterruptMessageModel(msg);

				if(message.data.triggerData == "STOP")
				{
					//Attempt to cancel the interrupt
					InterruptController.cancelInterrupt();
				} else {
					// Attempt to trigger an interrupt (if one exists) for each pin
					// For the time being play to set duration
					InterruptController.triggerInterrupt(message.data.triggerData, message.data.triggerType, false, true, message.additionalData)
					.done(function() {
						logger.info("Playing Web-based Interrupt for Key: " + message.data.triggerData);
						if (message.data.additionalData !== null) {
							logger.info("Additional Data included with Web-based Interrupt: " + message.additionalData);
						} else {
							logger.debug("No additional data included with Web-based Interrupt");
						}
					})
					.fail(function()
					{
						logger.warning("Attempted to play web-based interrupt but failed. No interrupt found for key: " + message.data.triggerData);
					});
				}
			}
		}

		self.checkForConfigurationUpdate = function()
		{
			var dfd = $.Deferred();

			if (self.isInitialized())
			{
				// API Options
				if (_apiInterface == null)
				{
					_apiInterface = new SLWebBasedInterruptsAPIInterface();			
					var apiOptions = {
						url : _options.url,
						//serialNo : "12345", // Test Serial Number
						serialNo : _options.serialNo,
						configurationCallback : configurationCallbackHandler
					};

					_apiInterface.initialize(_pusherConfiguration, apiOptions)
						.done(function(){
							// Success
							logger.debug("Interrupts API initialized successfully.");
							
							_apiInterface.checkForConfigurationUpdate();

							dfd.resolve();
						})
						.fail(function(){
							// Failed
							logger.error("Error: failed to initialize the Interrupts API..");
							dfd.reject();
						});			
				} else {
					_apiInterface.checkForConfigurationUpdate();
				}
			}

			return dfd;
		};

		
		
		/**
		 * Stops the module
		 * @return {Jquery Promise}
		 */
		self.stop = function()
		{
			var dfd = $.Deferred();

			// Only stop the ocntroller is it is already started
			if (self.isRunning())
			{
				// Stop
				_started = false;
				_initialized = false;

				// unsubsribe from previous channels
				if (_pusherConfiguration != null)
				{
					cancelSubscriptions(_pusherConfiguration);
				}

				if (_apiInterface)
				{
					_apiInterface.stop();				
				}

				dfd.resolve();

			}
			else
			{
				// Controller not started so we can just resolve this
				logger.warning("Stop called on Web-based Interrupts Controller but controller not started.");
				dfd.resolve();
			}

			return dfd;
		};

		self.isRunning = function()
		{
			return _started;
		};


		/**
		 * Returns true if module is initizialized
		 * @return {Boolean}
		 */
		self.isInitialized = function()
		{
			return _initialized;
		};

		/**
		 * Returns true if sensor is available. In this case
		 * if initialization was successful we know the sensor was available.
		 * No other way to test this.
		 * @return {Boolean}
		 */
		self.isAvailable = function()
		{
			if (self.isInitialized())
				return true;
			else
				return false;
		};

		/**
		 * Returns true if supported in this client, false otherwise
		 * @return {Boolean}
		 */
		self.isSupported = function()
		{
			return Settings.application.modules.webBasedInterrupts.supported;
		};

		/**
		 * Returns true if the module is enabled, false otherwise
		 * @return {Boolean}
		 */
		self.isEnabled = function()
		{
			return Settings.application.modules.webBasedInterrupts.supported;
		};

		
	}
	
	return new WebBasedInterruptsController();
}); 
rjs.define('gui/viewmodels/DevicePropertiesViewModel',["properties/App.Settings", "system/io/File", 'system/network/NetworkInterface', 'system/SystemInterface', 'properties/App.Properties', 'models/ConfigurationModel'], function(Settings, File, NetworkInterface, SystemInterface, Properties, ConfigurationModel) {
	/*
		DevicePropertiesViewModel - Used by the DevicePropertiesView
	 */
	function DevicePropertiesViewModel() {
		var self = this;

		// Defaults
		self.signageliveSerialNumber = '';
		self.changeId = '';
		self.macAddress = '';
		self.ipv4_address = 'Unknown';
		self.firmwareVersion = 'Unknown';
		self.modelNumber = 'Unknown';
		self.hardwareId = '';

		//
		// Generates the model
		//
		self.generate = function() {

			var dfd = $.Deferred();

			try {

				// LRead Config file (async)
				File.read(Settings.application.fileSystem.systemFiles.lastConfigFilePath).done(function(data){

					// Load Config
					configuration = new ConfigurationModel(JSON.parse(data));

					// Use Configuration data to get serialNo and changeId
					if(configuration.clientDetails.signageliveSerialNumber)
						self.signageliveSerialNumber = configuration.clientDetails.signageliveSerialNumber;	
					else
						self.signageliveSerialNumber = "";
						
					self.changeId = configuration.changeId;

				})
				.fail(function() {
					logger.info("Device Properties View Model: Error: Unable to read last configuration.");
					self.signageliveSerialNumber = "";	
					self.changeId = "";
				}).always(function()
				{
					// Use Properties file to get app version and build number
					self.clientVersion = Properties.appVersion;
					self.buildNumber = Properties.appBuildNumber;
					self.hardwareId =  Settings.application.api.clientId;

					// Use NetworkInterface to get the Device Mac Address and IPAddress
					NetworkInterface.getMAC().done(function(mac){
						self.macAddress = mac;
					}).always(function() {
						NetworkInterface.getIP().done(function(ip){
							self.ipv4_address = ip;
						}).always(function(){
							SystemInterface.getOSVersion().done(function(firmwareVersion){
								self.firmwareVersion = firmwareVersion;
							}).always(function(){
								SystemInterface.getModel().done(function(modelNumber){
									
									self.modelNumber = modelNumber;
									
									// Resolve model
									dfd.resolve();
								});
							});
						});
					});

				});
			} catch(e) {
				logger.error('Device Properties View Model: Error loading the configuration data: ' + e.message);
				dfd.reject();
			}

			return dfd;
		};

	}

	return DevicePropertiesViewModel;
});

rjs.define('gui/views/DevicePropertiesView',["system/DateTime",'gui/viewmodels/DevicePropertiesViewModel','system/graphics/GraphicsInterface', 'properties/App.Settings'], function(DateTime, DevicePropertiesViewModel, GraphicsInterface, Settings) {
	/*
		Device Properties View
		----------------------------------------
	 */
	function DevicePropertiesView() {

		var self = this;
		self.id = 'devicePropertiesView';
    
    var isSeries5 = Settings.application.features.deviceSeries == 5
    if(isSeries5) {
        var _originalScreens = GraphicsInterface.screens;
        var _screens = $.extend(true, [], _originalScreens);
    };
		
		//
		// Initialize View - This now uses deferred objects so that we can get underlying data like network info Asynchronously
		//
		self.initialize = function() {

			var dfd = $.Deferred();

			// Generate the DevicePropertiesViewModel
			deviceProperties = new DevicePropertiesViewModel();
			deviceProperties.generate().always(function() {

				// Configure HTML

        // Variables
        var InfoBarWidth = '100%';
        var enabledDisplays = _screens.filter(function (screen) {
            if (screen.enabled) {
                return screen;
            };
        });
        var primaryDisplay = enabledDisplays[0];
        var isDOMRotated = (primaryDisplay.transform == "normal") ? false : true;
        var PrimaryVideoModeWidth = Number(primaryDisplay.videoMode.split('x')[0]);
        var PrimaryVideoModeHeight = Number(primaryDisplay.videoMode.split('x')[1]);
        var isUsingMultipleOutputs = (_screens.length > 1);

        // Is it a multi-screen setup? If so, the InfoBarWidth should only be as wide as the primary display's width or height value (depending on whether or not we are using portrait mode)
        if (isUsingMultipleOutputs && primaryDisplay.videoMode !== "auto" && !isDOMRotated) {
            PrimaryVideoModeWidth += 'px';
            InfoBarWidth = PrimaryVideoModeWidth;
        } if (isUsingMultipleOutputs && primaryDisplay.videoMode !== "auto" && isDOMRotated) {
          PrimaryVideoModeHeight += 'px';
            InfoBarWidth = PrimaryVideoModeHeight;
        }

        // Setting the absolute co-ordiantes to the primary screenX and screenY values ensures the Setup View will always show on the first enabled display
        var viewHtml = `<div id='${self.id}' style='position:absolute;left:${primaryDisplay.screenX}px;top:${primaryDisplay.screenY}px;width:${InfoBarWidth};background-color:rgba(0,0,0,0.8);color:#fff;word-wrap:break-word;font-size:1vw;z-index:99;padding-top:2vw'>`;
				viewHtml += "<table style='margin-left:auto;margin-right:auto;text-align:left'>";
				viewHtml += "<tr><td>Current Time:</td><td>" + DateTime.now() + "</td></tr>";
				viewHtml += "<tr><td>Model Number:</td><td>" + deviceProperties.modelNumber + "</td></tr>";
				viewHtml += "<tr><td>Hardware ID:</td><td>" + deviceProperties.hardwareId + "</td></tr>";
				viewHtml += "<tr><td>Firmware Version: </td><td>" + deviceProperties.firmwareVersion + "</td></tr>";
				viewHtml += "<tr><td>Signagelive Serial Number: </td><td>" + deviceProperties.signageliveSerialNumber + "</td></tr>";
				viewHtml += "<tr><td>Signagelive Version: </td><td>" + deviceProperties.clientVersion + "</td></tr>";
				viewHtml += "<tr><td>Signagelive Build: </td><td>" + deviceProperties.buildNumber + "</td></tr>";
				viewHtml += "<tr><td>IP Address: </td><td>" + deviceProperties.ipv4_address + "</td></tr>";
				viewHtml += "<tr><td>MAC Address: </td><td>" + deviceProperties.macAddress + "</td></tr>";		
				viewHtml += "<tr><td>Change ID: </td><td>" + deviceProperties.changeId + "</td></tr>";
				viewHtml += "</table>";
				viewHtml += '</div>';

				// Store in self.html
				self.html = viewHtml;

				// Resolve initialisation
				dfd.resolve();

			});

			return dfd;
		};
	}

	return DevicePropertiesView;
});

/*!
 * Knockout JavaScript library v3.2.0
 * (c) Steven Sanderson - http://knockoutjs.com/
 * License: MIT (http://www.opensource.org/licenses/mit-license.php)
 */

(function(){
var DEBUG=true;
(function(undefined){
    // (0, eval)('this') is a robust way of getting a reference to the global object
    // For details, see http://stackoverflow.com/questions/14119988/return-this-0-evalthis/14120023#14120023
    var window = this || (0, eval)('this'),
        document = window['document'],
        navigator = window['navigator'],
        jQueryInstance = window["jQuery"],
        JSON = window["JSON"];
(function(factory) {
    // Support three module loading scenarios
    if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') {
        // [1] CommonJS/Node.js
        var target = module['exports'] || exports; // module.exports is for Node.js
        factory(target, require);
    } else if (typeof rjs.define === 'function' && rjs.define['amd']) {
        // [2] AMD anonymous module
        rjs.define('lib/knockout',['exports', 'require'], factory);
    } else {
        // [3] No module loader (plain <script> tag) - put directly in global namespace
        factory(window['ko'] = {});
    }
}(function(koExports, require){
// Internally, all KO objects are attached to koExports (even the non-exported ones whose names will be minified by the closure compiler).
// In the future, the following "ko" variable may be made distinct from "koExports" so that private objects are not externally reachable.
var ko = typeof koExports !== 'undefined' ? koExports : {};
// Google Closure Compiler helpers (used only to make the minified file smaller)
ko.exportSymbol = function(koPath, object) {
    var tokens = koPath.split(".");

    // In the future, "ko" may become distinct from "koExports" (so that non-exported objects are not reachable)
    // At that point, "target" would be set to: (typeof koExports !== "undefined" ? koExports : ko)
    var target = ko;

    for (var i = 0; i < tokens.length - 1; i++)
        target = target[tokens[i]];
    target[tokens[tokens.length - 1]] = object;
};
ko.exportProperty = function(owner, publicName, object) {
    owner[publicName] = object;
};
ko.version = "3.2.0";

ko.exportSymbol('version', ko.version);
ko.utils = (function () {
    function objectForEach(obj, action) {
        for (var prop in obj) {
            if (obj.hasOwnProperty(prop)) {
                action(prop, obj[prop]);
            }
        }
    }

    function extend(target, source) {
        if (source) {
            for(var prop in source) {
                if(source.hasOwnProperty(prop)) {
                    target[prop] = source[prop];
                }
            }
        }
        return target;
    }

    function setPrototypeOf(obj, proto) {
        obj.__proto__ = proto;
        return obj;
    }

    var canSetPrototype = ({ __proto__: [] } instanceof Array);

    // Represent the known event types in a compact way, then at runtime transform it into a hash with event name as key (for fast lookup)
    var knownEvents = {}, knownEventTypesByEventName = {};
    var keyEventTypeName = (navigator && /Firefox\/2/i.test(navigator.userAgent)) ? 'KeyboardEvent' : 'UIEvents';
    knownEvents[keyEventTypeName] = ['keyup', 'keydown', 'keypress'];
    knownEvents['MouseEvents'] = ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave'];
    objectForEach(knownEvents, function(eventType, knownEventsForType) {
        if (knownEventsForType.length) {
            for (var i = 0, j = knownEventsForType.length; i < j; i++)
                knownEventTypesByEventName[knownEventsForType[i]] = eventType;
        }
    });
    var eventsThatMustBeRegisteredUsingAttachEvent = { 'propertychange': true }; // Workaround for an IE9 issue - https://github.com/SteveSanderson/knockout/issues/406

    // Detect IE versions for bug workarounds (uses IE conditionals, not UA string, for robustness)
    // Note that, since IE 10 does not support conditional comments, the following logic only detects IE < 10.
    // Currently this is by design, since IE 10+ behaves correctly when treated as a standard browser.
    // If there is a future need to detect specific versions of IE10+, we will amend this.
    var ieVersion = document && (function() {
        var version = 3, div = document.createElement('div'), iElems = div.getElementsByTagName('i');

        // Keep constructing conditional HTML blocks until we hit one that resolves to an empty fragment
        while (
            div.innerHTML = '<!--[if gt IE ' + (++version) + ']><i></i><![endif]-->',
            iElems[0]
        ) {}
        return version > 4 ? version : undefined;
    }());
    var isIe6 = ieVersion === 6,
        isIe7 = ieVersion === 7;

    function isClickOnCheckableElement(element, eventType) {
        if ((ko.utils.tagNameLower(element) !== "input") || !element.type) return false;
        if (eventType.toLowerCase() != "click") return false;
        var inputType = element.type;
        return (inputType == "checkbox") || (inputType == "radio");
    }

    return {
        fieldsIncludedWithJsonPost: ['authenticity_token', /^__RequestVerificationToken(_.*)?$/],

        arrayForEach: function (array, action) {
            for (var i = 0, j = array.length; i < j; i++)
                action(array[i], i);
        },

        arrayIndexOf: function (array, item) {
            if (typeof Array.prototype.indexOf == "function")
                return Array.prototype.indexOf.call(array, item);
            for (var i = 0, j = array.length; i < j; i++)
                if (array[i] === item)
                    return i;
            return -1;
        },

        arrayFirst: function (array, predicate, predicateOwner) {
            for (var i = 0, j = array.length; i < j; i++)
                if (predicate.call(predicateOwner, array[i], i))
                    return array[i];
            return null;
        },

        arrayRemoveItem: function (array, itemToRemove) {
            var index = ko.utils.arrayIndexOf(array, itemToRemove);
            if (index > 0) {
                array.splice(index, 1);
            }
            else if (index === 0) {
                array.shift();
            }
        },

        arrayGetDistinctValues: function (array) {
            array = array || [];
            var result = [];
            for (var i = 0, j = array.length; i < j; i++) {
                if (ko.utils.arrayIndexOf(result, array[i]) < 0)
                    result.push(array[i]);
            }
            return result;
        },

        arrayMap: function (array, mapping) {
            array = array || [];
            var result = [];
            for (var i = 0, j = array.length; i < j; i++)
                result.push(mapping(array[i], i));
            return result;
        },

        arrayFilter: function (array, predicate) {
            array = array || [];
            var result = [];
            for (var i = 0, j = array.length; i < j; i++)
                if (predicate(array[i], i))
                    result.push(array[i]);
            return result;
        },

        arrayPushAll: function (array, valuesToPush) {
            if (valuesToPush instanceof Array)
                array.push.apply(array, valuesToPush);
            else
                for (var i = 0, j = valuesToPush.length; i < j; i++)
                    array.push(valuesToPush[i]);
            return array;
        },

        addOrRemoveItem: function(array, value, included) {
            var existingEntryIndex = ko.utils.arrayIndexOf(ko.utils.peekObservable(array), value);
            if (existingEntryIndex < 0) {
                if (included)
                    array.push(value);
            } else {
                if (!included)
                    array.splice(existingEntryIndex, 1);
            }
        },

        canSetPrototype: canSetPrototype,

        extend: extend,

        setPrototypeOf: setPrototypeOf,

        setPrototypeOfOrExtend: canSetPrototype ? setPrototypeOf : extend,

        objectForEach: objectForEach,

        objectMap: function(source, mapping) {
            if (!source)
                return source;
            var target = {};
            for (var prop in source) {
                if (source.hasOwnProperty(prop)) {
                    target[prop] = mapping(source[prop], prop, source);
                }
            }
            return target;
        },

        emptyDomNode: function (domNode) {
            while (domNode.firstChild) {
                ko.removeNode(domNode.firstChild);
            }
        },

        moveCleanedNodesToContainerElement: function(nodes) {
            // Ensure it's a real array, as we're about to reparent the nodes and
            // we don't want the underlying collection to change while we're doing that.
            var nodesArray = ko.utils.makeArray(nodes);

            var container = document.createElement('div');
            for (var i = 0, j = nodesArray.length; i < j; i++) {
                container.appendChild(ko.cleanNode(nodesArray[i]));
            }
            return container;
        },

        cloneNodes: function (nodesArray, shouldCleanNodes) {
            for (var i = 0, j = nodesArray.length, newNodesArray = []; i < j; i++) {
                var clonedNode = nodesArray[i].cloneNode(true);
                newNodesArray.push(shouldCleanNodes ? ko.cleanNode(clonedNode) : clonedNode);
            }
            return newNodesArray;
        },

        setDomNodeChildren: function (domNode, childNodes) {
            ko.utils.emptyDomNode(domNode);
            if (childNodes) {
                for (var i = 0, j = childNodes.length; i < j; i++)
                    domNode.appendChild(childNodes[i]);
            }
        },

        replaceDomNodes: function (nodeToReplaceOrNodeArray, newNodesArray) {
            var nodesToReplaceArray = nodeToReplaceOrNodeArray.nodeType ? [nodeToReplaceOrNodeArray] : nodeToReplaceOrNodeArray;
            if (nodesToReplaceArray.length > 0) {
                var insertionPoint = nodesToReplaceArray[0];
                var parent = insertionPoint.parentNode;
                for (var i = 0, j = newNodesArray.length; i < j; i++)
                    parent.insertBefore(newNodesArray[i], insertionPoint);
                for (var i = 0, j = nodesToReplaceArray.length; i < j; i++) {
                    ko.removeNode(nodesToReplaceArray[i]);
                }
            }
        },

        fixUpContinuousNodeArray: function(continuousNodeArray, parentNode) {
            // Before acting on a set of nodes that were previously outputted by a template function, we have to reconcile
            // them against what is in the DOM right now. It may be that some of the nodes have already been removed, or that
            // new nodes might have been inserted in the middle, for example by a binding. Also, there may previously have been
            // leading comment nodes (created by rewritten string-based templates) that have since been removed during binding.
            // So, this function translates the old "map" output array into its best guess of the set of current DOM nodes.
            //
            // Rules:
            //   [A] Any leading nodes that have been removed should be ignored
            //       These most likely correspond to memoization nodes that were already removed during binding
            //       See https://github.com/SteveSanderson/knockout/pull/440
            //   [B] We want to output a continuous series of nodes. So, ignore any nodes that have already been removed,
            //       and include any nodes that have been inserted among the previous collection

            if (continuousNodeArray.length) {
                // The parent node can be a virtual element; so get the real parent node
                parentNode = (parentNode.nodeType === 8 && parentNode.parentNode) || parentNode;

                // Rule [A]
                while (continuousNodeArray.length && continuousNodeArray[0].parentNode !== parentNode)
                    continuousNodeArray.shift();

                // Rule [B]
                if (continuousNodeArray.length > 1) {
                    var current = continuousNodeArray[0], last = continuousNodeArray[continuousNodeArray.length - 1];
                    // Replace with the actual new continuous node set
                    continuousNodeArray.length = 0;
                    while (current !== last) {
                        continuousNodeArray.push(current);
                        current = current.nextSibling;
                        if (!current) // Won't happen, except if the developer has manually removed some DOM elements (then we're in an undefined scenario)
                            return;
                    }
                    continuousNodeArray.push(last);
                }
            }
            return continuousNodeArray;
        },

        setOptionNodeSelectionState: function (optionNode, isSelected) {
            // IE6 sometimes throws "unknown error" if you try to write to .selected directly, whereas Firefox struggles with setAttribute. Pick one based on browser.
            if (ieVersion < 7)
                optionNode.setAttribute("selected", isSelected);
            else
                optionNode.selected = isSelected;
        },

        stringTrim: function (string) {
            return string === null || string === undefined ? '' :
                string.trim ?
                    string.trim() :
                    string.toString().replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
        },

        stringStartsWith: function (string, startsWith) {
            string = string || "";
            if (startsWith.length > string.length)
                return false;
            return string.substring(0, startsWith.length) === startsWith;
        },

        domNodeIsContainedBy: function (node, containedByNode) {
            if (node === containedByNode)
                return true;
            if (node.nodeType === 11)
                return false; // Fixes issue #1162 - can't use node.contains for document fragments on IE8
            if (containedByNode.contains)
                return containedByNode.contains(node.nodeType === 3 ? node.parentNode : node);
            if (containedByNode.compareDocumentPosition)
                return (containedByNode.compareDocumentPosition(node) & 16) == 16;
            while (node && node != containedByNode) {
                node = node.parentNode;
            }
            return !!node;
        },

        domNodeIsAttachedToDocument: function (node) {
            return ko.utils.domNodeIsContainedBy(node, node.ownerDocument.documentElement);
        },

        anyDomNodeIsAttachedToDocument: function(nodes) {
            return !!ko.utils.arrayFirst(nodes, ko.utils.domNodeIsAttachedToDocument);
        },

        tagNameLower: function(element) {
            // For HTML elements, tagName will always be upper case; for XHTML elements, it'll be lower case.
            // Possible future optimization: If we know it's an element from an XHTML document (not HTML),
            // we don't need to do the .toLowerCase() as it will always be lower case anyway.
            return element && element.tagName && element.tagName.toLowerCase();
        },

        registerEventHandler: function (element, eventType, handler) {
            var mustUseAttachEvent = ieVersion && eventsThatMustBeRegisteredUsingAttachEvent[eventType];
            if (!mustUseAttachEvent && jQueryInstance) {
                jQueryInstance(element)['bind'](eventType, handler);
            } else if (!mustUseAttachEvent && typeof element.addEventListener == "function")
                element.addEventListener(eventType, handler, false);
            else if (typeof element.attachEvent != "undefined") {
                var attachEventHandler = function (event) { handler.call(element, event); },
                    attachEventName = "on" + eventType;
                element.attachEvent(attachEventName, attachEventHandler);

                // IE does not dispose attachEvent handlers automatically (unlike with addEventListener)
                // so to avoid leaks, we have to remove them manually. See bug #856
                ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
                    element.detachEvent(attachEventName, attachEventHandler);
                });
            } else
                throw new Error("Browser doesn't support addEventListener or attachEvent");
        },

        triggerEvent: function (element, eventType) {
            if (!(element && element.nodeType))
                throw new Error("element must be a DOM node when calling triggerEvent");

            // For click events on checkboxes and radio buttons, jQuery toggles the element checked state *after* the
            // event handler runs instead of *before*. (This was fixed in 1.9 for checkboxes but not for radio buttons.)
            // IE doesn't change the checked state when you trigger the click event using "fireEvent".
            // In both cases, we'll use the click method instead.
            var useClickWorkaround = isClickOnCheckableElement(element, eventType);

            if (jQueryInstance && !useClickWorkaround) {
                jQueryInstance(element)['trigger'](eventType);
            } else if (typeof document.createEvent == "function") {
                if (typeof element.dispatchEvent == "function") {
                    var eventCategory = knownEventTypesByEventName[eventType] || "HTMLEvents";
                    var event = document.createEvent(eventCategory);
                    event.initEvent(eventType, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, element);
                    element.dispatchEvent(event);
                }
                else
                    throw new Error("The supplied element doesn't support dispatchEvent");
            } else if (useClickWorkaround && element.click) {
                element.click();
            } else if (typeof element.fireEvent != "undefined") {
                element.fireEvent("on" + eventType);
            } else {
                throw new Error("Browser doesn't support triggering events");
            }
        },

        unwrapObservable: function (value) {
            return ko.isObservable(value) ? value() : value;
        },

        peekObservable: function (value) {
            return ko.isObservable(value) ? value.peek() : value;
        },

        toggleDomNodeCssClass: function (node, classNames, shouldHaveClass) {
            if (classNames) {
                var cssClassNameRegex = /\S+/g,
                    currentClassNames = node.className.match(cssClassNameRegex) || [];
                ko.utils.arrayForEach(classNames.match(cssClassNameRegex), function(className) {
                    ko.utils.addOrRemoveItem(currentClassNames, className, shouldHaveClass);
                });
                node.className = currentClassNames.join(" ");
            }
        },

        setTextContent: function(element, textContent) {
            var value = ko.utils.unwrapObservable(textContent);
            if ((value === null) || (value === undefined))
                value = "";

            // We need there to be exactly one child: a text node.
            // If there are no children, more than one, or if it's not a text node,
            // we'll clear everything and create a single text node.
            var innerTextNode = ko.virtualElements.firstChild(element);
            if (!innerTextNode || innerTextNode.nodeType != 3 || ko.virtualElements.nextSibling(innerTextNode)) {
                ko.virtualElements.setDomNodeChildren(element, [element.ownerDocument.createTextNode(value)]);
            } else {
                innerTextNode.data = value;
            }

            ko.utils.forceRefresh(element);
        },

        setElementName: function(element, name) {
            element.name = name;

            // Workaround IE 6/7 issue
            // - https://github.com/SteveSanderson/knockout/issues/197
            // - http://www.matts411.com/post/setting_the_name_attribute_in_ie_dom/
            if (ieVersion <= 7) {
                try {
                    element.mergeAttributes(document.createElement("<input name='" + element.name + "'/>"), false);
                }
                catch(e) {} // For IE9 with doc mode "IE9 Standards" and browser mode "IE9 Compatibility View"
            }
        },

        forceRefresh: function(node) {
            // Workaround for an IE9 rendering bug - https://github.com/SteveSanderson/knockout/issues/209
            if (ieVersion >= 9) {
                // For text nodes and comment nodes (most likely virtual elements), we will have to refresh the container
                var elem = node.nodeType == 1 ? node : node.parentNode;
                if (elem.style)
                    elem.style.zoom = elem.style.zoom;
            }
        },

        ensureSelectElementIsRenderedCorrectly: function(selectElement) {
            // Workaround for IE9 rendering bug - it doesn't reliably display all the text in dynamically-added select boxes unless you force it to re-render by updating the width.
            // (See https://github.com/SteveSanderson/knockout/issues/312, http://stackoverflow.com/questions/5908494/select-only-shows-first-char-of-selected-option)
            // Also fixes IE7 and IE8 bug that causes selects to be zero width if enclosed by 'if' or 'with'. (See issue #839)
            if (ieVersion) {
                var originalWidth = selectElement.style.width;
                selectElement.style.width = 0;
                selectElement.style.width = originalWidth;
            }
        },

        range: function (min, max) {
            min = ko.utils.unwrapObservable(min);
            max = ko.utils.unwrapObservable(max);
            var result = [];
            for (var i = min; i <= max; i++)
                result.push(i);
            return result;
        },

        makeArray: function(arrayLikeObject) {
            var result = [];
            for (var i = 0, j = arrayLikeObject.length; i < j; i++) {
                result.push(arrayLikeObject[i]);
            };
            return result;
        },

        isIe6 : isIe6,
        isIe7 : isIe7,
        ieVersion : ieVersion,

        getFormFields: function(form, fieldName) {
            var fields = ko.utils.makeArray(form.getElementsByTagName("input")).concat(ko.utils.makeArray(form.getElementsByTagName("textarea")));
            var isMatchingField = (typeof fieldName == 'string')
                ? function(field) { return field.name === fieldName }
                : function(field) { return fieldName.test(field.name) }; // Treat fieldName as regex or object containing predicate
            var matches = [];
            for (var i = fields.length - 1; i >= 0; i--) {
                if (isMatchingField(fields[i]))
                    matches.push(fields[i]);
            };
            return matches;
        },

        parseJson: function (jsonString) {
            if (typeof jsonString == "string") {
                jsonString = ko.utils.stringTrim(jsonString);
                if (jsonString) {
                    if (JSON && JSON.parse) // Use native parsing where available
                        return JSON.parse(jsonString);
                    return (new Function("return " + jsonString))(); // Fallback on less safe parsing for older browsers
                }
            }
            return null;
        },

        stringifyJson: function (data, replacer, space) {   // replacer and space are optional
            if (!JSON || !JSON.stringify)
                throw new Error("Cannot find JSON.stringify(). Some browsers (e.g., IE < 8) don't support it natively, but you can overcome this by adding a script reference to json2.js, downloadable from http://www.json.org/json2.js");
            return JSON.stringify(ko.utils.unwrapObservable(data), replacer, space);
        },

        postJson: function (urlOrForm, data, options) {
            options = options || {};
            var params = options['params'] || {};
            var includeFields = options['includeFields'] || this.fieldsIncludedWithJsonPost;
            var url = urlOrForm;

            // If we were given a form, use its 'action' URL and pick out any requested field values
            if((typeof urlOrForm == 'object') && (ko.utils.tagNameLower(urlOrForm) === "form")) {
                var originalForm = urlOrForm;
                url = originalForm.action;
                for (var i = includeFields.length - 1; i >= 0; i--) {
                    var fields = ko.utils.getFormFields(originalForm, includeFields[i]);
                    for (var j = fields.length - 1; j >= 0; j--)
                        params[fields[j].name] = fields[j].value;
                }
            }

            data = ko.utils.unwrapObservable(data);
            var form = document.createElement("form");
            form.style.display = "none";
            form.action = url;
            form.method = "post";
            for (var key in data) {
                // Since 'data' this is a model object, we include all properties including those inherited from its prototype
                var input = document.createElement("input");
                input.type = "hidden";
                input.name = key;
                input.value = ko.utils.stringifyJson(ko.utils.unwrapObservable(data[key]));
                form.appendChild(input);
            }
            objectForEach(params, function(key, value) {
                var input = document.createElement("input");
                input.type = "hidden";
                input.name = key;
                input.value = value;
                form.appendChild(input);
            });
            document.body.appendChild(form);
            options['submitter'] ? options['submitter'](form) : form.submit();
            setTimeout(function () { form.parentNode.removeChild(form); }, 0);
        }
    }
}());

ko.exportSymbol('utils', ko.utils);
ko.exportSymbol('utils.arrayForEach', ko.utils.arrayForEach);
ko.exportSymbol('utils.arrayFirst', ko.utils.arrayFirst);
ko.exportSymbol('utils.arrayFilter', ko.utils.arrayFilter);
ko.exportSymbol('utils.arrayGetDistinctValues', ko.utils.arrayGetDistinctValues);
ko.exportSymbol('utils.arrayIndexOf', ko.utils.arrayIndexOf);
ko.exportSymbol('utils.arrayMap', ko.utils.arrayMap);
ko.exportSymbol('utils.arrayPushAll', ko.utils.arrayPushAll);
ko.exportSymbol('utils.arrayRemoveItem', ko.utils.arrayRemoveItem);
ko.exportSymbol('utils.extend', ko.utils.extend);
ko.exportSymbol('utils.fieldsIncludedWithJsonPost', ko.utils.fieldsIncludedWithJsonPost);
ko.exportSymbol('utils.getFormFields', ko.utils.getFormFields);
ko.exportSymbol('utils.peekObservable', ko.utils.peekObservable);
ko.exportSymbol('utils.postJson', ko.utils.postJson);
ko.exportSymbol('utils.parseJson', ko.utils.parseJson);
ko.exportSymbol('utils.registerEventHandler', ko.utils.registerEventHandler);
ko.exportSymbol('utils.stringifyJson', ko.utils.stringifyJson);
ko.exportSymbol('utils.range', ko.utils.range);
ko.exportSymbol('utils.toggleDomNodeCssClass', ko.utils.toggleDomNodeCssClass);
ko.exportSymbol('utils.triggerEvent', ko.utils.triggerEvent);
ko.exportSymbol('utils.unwrapObservable', ko.utils.unwrapObservable);
ko.exportSymbol('utils.objectForEach', ko.utils.objectForEach);
ko.exportSymbol('utils.addOrRemoveItem', ko.utils.addOrRemoveItem);
ko.exportSymbol('unwrap', ko.utils.unwrapObservable); // Convenient shorthand, because this is used so commonly

if (!Function.prototype['bind']) {
    // Function.prototype.bind is a standard part of ECMAScript 5th Edition (December 2009, http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf)
    // In case the browser doesn't implement it natively, provide a JavaScript implementation. This implementation is based on the one in prototype.js
    Function.prototype['bind'] = function (object) {
        var originalFunction = this, args = Array.prototype.slice.call(arguments), object = args.shift();
        return function () {
            return originalFunction.apply(object, args.concat(Array.prototype.slice.call(arguments)));
        };
    };
}

ko.utils.domData = new (function () {
    var uniqueId = 0;
    var dataStoreKeyExpandoPropertyName = "__ko__" + (new Date).getTime();
    var dataStore = {};

    function getAll(node, createIfNotFound) {
        var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
        var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null") && dataStore[dataStoreKey];
        if (!hasExistingDataStore) {
            if (!createIfNotFound)
                return undefined;
            dataStoreKey = node[dataStoreKeyExpandoPropertyName] = "ko" + uniqueId++;
            dataStore[dataStoreKey] = {};
        }
        return dataStore[dataStoreKey];
    }

    return {
        get: function (node, key) {
            var allDataForNode = getAll(node, false);
            return allDataForNode === undefined ? undefined : allDataForNode[key];
        },
        set: function (node, key, value) {
            if (value === undefined) {
                // Make sure we don't actually create a new domData key if we are actually deleting a value
                if (getAll(node, false) === undefined)
                    return;
            }
            var allDataForNode = getAll(node, true);
            allDataForNode[key] = value;
        },
        clear: function (node) {
            var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
            if (dataStoreKey) {
                delete dataStore[dataStoreKey];
                node[dataStoreKeyExpandoPropertyName] = null;
                return true; // Exposing "did clean" flag purely so specs can infer whether things have been cleaned up as intended
            }
            return false;
        },

        nextKey: function () {
            return (uniqueId++) + dataStoreKeyExpandoPropertyName;
        }
    };
})();

ko.exportSymbol('utils.domData', ko.utils.domData);
ko.exportSymbol('utils.domData.clear', ko.utils.domData.clear); // Exporting only so specs can clear up after themselves fully

ko.utils.domNodeDisposal = new (function () {
    var domDataKey = ko.utils.domData.nextKey();
    var cleanableNodeTypes = { 1: true, 8: true, 9: true };       // Element, Comment, Document
    var cleanableNodeTypesWithDescendants = { 1: true, 9: true }; // Element, Document

    function getDisposeCallbacksCollection(node, createIfNotFound) {
        var allDisposeCallbacks = ko.utils.domData.get(node, domDataKey);
        if ((allDisposeCallbacks === undefined) && createIfNotFound) {
            allDisposeCallbacks = [];
            ko.utils.domData.set(node, domDataKey, allDisposeCallbacks);
        }
        return allDisposeCallbacks;
    }
    function destroyCallbacksCollection(node) {
        ko.utils.domData.set(node, domDataKey, undefined);
    }

    function cleanSingleNode(node) {
        // Run all the dispose callbacks
        var callbacks = getDisposeCallbacksCollection(node, false);
        if (callbacks) {
            callbacks = callbacks.slice(0); // Clone, as the array may be modified during iteration (typically, callbacks will remove themselves)
            for (var i = 0; i < callbacks.length; i++)
                callbacks[i](node);
        }

        // Erase the DOM data
        ko.utils.domData.clear(node);

        // Perform cleanup needed by external libraries (currently only jQuery, but can be extended)
        ko.utils.domNodeDisposal["cleanExternalData"](node);

        // Clear any immediate-child comment nodes, as these wouldn't have been found by
        // node.getElementsByTagName("*") in cleanNode() (comment nodes aren't elements)
        if (cleanableNodeTypesWithDescendants[node.nodeType])
            cleanImmediateCommentTypeChildren(node);
    }

    function cleanImmediateCommentTypeChildren(nodeWithChildren) {
        var child, nextChild = nodeWithChildren.firstChild;
        while (child = nextChild) {
            nextChild = child.nextSibling;
            if (child.nodeType === 8)
                cleanSingleNode(child);
        }
    }

    return {
        addDisposeCallback : function(node, callback) {
            if (typeof callback != "function")
                throw new Error("Callback must be a function");
            getDisposeCallbacksCollection(node, true).push(callback);
        },

        removeDisposeCallback : function(node, callback) {
            var callbacksCollection = getDisposeCallbacksCollection(node, false);
            if (callbacksCollection) {
                ko.utils.arrayRemoveItem(callbacksCollection, callback);
                if (callbacksCollection.length == 0)
                    destroyCallbacksCollection(node);
            }
        },

        cleanNode : function(node) {
            // First clean this node, where applicable
            if (cleanableNodeTypes[node.nodeType]) {
                cleanSingleNode(node);

                // ... then its descendants, where applicable
                if (cleanableNodeTypesWithDescendants[node.nodeType]) {
                    // Clone the descendants list in case it changes during iteration
                    var descendants = [];
                    ko.utils.arrayPushAll(descendants, node.getElementsByTagName("*"));
                    for (var i = 0, j = descendants.length; i < j; i++)
                        cleanSingleNode(descendants[i]);
                }
            }
            return node;
        },

        removeNode : function(node) {
            ko.cleanNode(node);
            if (node.parentNode)
                node.parentNode.removeChild(node);
        },

        "cleanExternalData" : function (node) {
            // Special support for jQuery here because it's so commonly used.
            // Many jQuery plugins (including jquery.tmpl) store data using jQuery's equivalent of domData
            // so notify it to tear down any resources associated with the node & descendants here.
            if (jQueryInstance && (typeof jQueryInstance['cleanData'] == "function"))
                jQueryInstance['cleanData']([node]);
        }
    }
})();
ko.cleanNode = ko.utils.domNodeDisposal.cleanNode; // Shorthand name for convenience
ko.removeNode = ko.utils.domNodeDisposal.removeNode; // Shorthand name for convenience
ko.exportSymbol('cleanNode', ko.cleanNode);
ko.exportSymbol('removeNode', ko.removeNode);
ko.exportSymbol('utils.domNodeDisposal', ko.utils.domNodeDisposal);
ko.exportSymbol('utils.domNodeDisposal.addDisposeCallback', ko.utils.domNodeDisposal.addDisposeCallback);
ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeDisposal.removeDisposeCallback);
(function () {
    var leadingCommentRegex = /^(\s*)<!--(.*?)-->/;

    function simpleHtmlParse(html) {
        // Based on jQuery's "clean" function, but only accounting for table-related elements.
        // If you have referenced jQuery, this won't be used anyway - KO will use jQuery's "clean" function directly

        // Note that there's still an issue in IE < 9 whereby it will discard comment nodes that are the first child of
        // a descendant node. For example: "<div><!-- mycomment -->abc</div>" will get parsed as "<div>abc</div>"
        // This won't affect anyone who has referenced jQuery, and there's always the workaround of inserting a dummy node
        // (possibly a text node) in front of the comment. So, KO does not attempt to workaround this IE issue automatically at present.

        // Trim whitespace, otherwise indexOf won't work as expected
        var tags = ko.utils.stringTrim(html).toLowerCase(), div = document.createElement("div");

        // Finds the first match from the left column, and returns the corresponding "wrap" data from the right column
        var wrap = tags.match(/^<(thead|tbody|tfoot)/)              && [1, "<table>", "</table>"] ||
                   !tags.indexOf("<tr")                             && [2, "<table><tbody>", "</tbody></table>"] ||
                   (!tags.indexOf("<td") || !tags.indexOf("<th"))   && [3, "<table><tbody><tr>", "</tr></tbody></table>"] ||
                   /* anything else */                                 [0, "", ""];

        // Go to html and back, then peel off extra wrappers
        // Note that we always prefix with some dummy text, because otherwise, IE<9 will strip out leading comment nodes in descendants. Total madness.
        var markup = "ignored<div>" + wrap[1] + html + wrap[2] + "</div>";
        if (typeof window['innerShiv'] == "function") {
            div.appendChild(window['innerShiv'](markup));
        } else {
            div.innerHTML = markup;
        }

        // Move to the right depth
        while (wrap[0]--)
            div = div.lastChild;

        return ko.utils.makeArray(div.lastChild.childNodes);
    }

    function jQueryHtmlParse(html) {
        // jQuery's "parseHTML" function was introduced in jQuery 1.8.0 and is a documented public API.
        if (jQueryInstance['parseHTML']) {
            return jQueryInstance['parseHTML'](html) || []; // Ensure we always return an array and never null
        } else {
            // For jQuery < 1.8.0, we fall back on the undocumented internal "clean" function.
            var elems = jQueryInstance['clean']([html]);

            // As of jQuery 1.7.1, jQuery parses the HTML by appending it to some dummy parent nodes held in an in-memory document fragment.
            // Unfortunately, it never clears the dummy parent nodes from the document fragment, so it leaks memory over time.
            // Fix this by finding the top-most dummy parent element, and detaching it from its owner fragment.
            if (elems && elems[0]) {
                // Find the top-most parent element that's a direct child of a document fragment
                var elem = elems[0];
                while (elem.parentNode && elem.parentNode.nodeType !== 11 /* i.e., DocumentFragment */)
                    elem = elem.parentNode;
                // ... then detach it
                if (elem.parentNode)
                    elem.parentNode.removeChild(elem);
            }

            return elems;
        }
    }

    ko.utils.parseHtmlFragment = function(html) {
        return jQueryInstance ? jQueryHtmlParse(html)   // As below, benefit from jQuery's optimisations where possible
                              : simpleHtmlParse(html);  // ... otherwise, this simple logic will do in most common cases.
    };

    ko.utils.setHtml = function(node, html) {
        ko.utils.emptyDomNode(node);

        // There's no legitimate reason to display a stringified observable without unwrapping it, so we'll unwrap it
        html = ko.utils.unwrapObservable(html);

        if ((html !== null) && (html !== undefined)) {
            if (typeof html != 'string')
                html = html.toString();

            // jQuery contains a lot of sophisticated code to parse arbitrary HTML fragments,
            // for example <tr> elements which are not normally allowed to exist on their own.
            // If you've referenced jQuery we'll use that rather than duplicating its code.
            if (jQueryInstance) {
                jQueryInstance(node)['html'](html);
            } else {
                // ... otherwise, use KO's own parsing logic.
                var parsedNodes = ko.utils.parseHtmlFragment(html);
                for (var i = 0; i < parsedNodes.length; i++)
                    node.appendChild(parsedNodes[i]);
            }
        }
    };
})();

ko.exportSymbol('utils.parseHtmlFragment', ko.utils.parseHtmlFragment);
ko.exportSymbol('utils.setHtml', ko.utils.setHtml);

ko.memoization = (function () {
    var memos = {};

    function randomMax8HexChars() {
        return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1);
    }
    function generateRandomId() {
        return randomMax8HexChars() + randomMax8HexChars();
    }
    function findMemoNodes(rootNode, appendToArray) {
        if (!rootNode)
            return;
        if (rootNode.nodeType == 8) {
            var memoId = ko.memoization.parseMemoText(rootNode.nodeValue);
            if (memoId != null)
                appendToArray.push({ domNode: rootNode, memoId: memoId });
        } else if (rootNode.nodeType == 1) {
            for (var i = 0, childNodes = rootNode.childNodes, j = childNodes.length; i < j; i++)
                findMemoNodes(childNodes[i], appendToArray);
        }
    }

    return {
        memoize: function (callback) {
            if (typeof callback != "function")
                throw new Error("You can only pass a function to ko.memoization.memoize()");
            var memoId = generateRandomId();
            memos[memoId] = callback;
            return "<!--[ko_memo:" + memoId + "]-->";
        },

        unmemoize: function (memoId, callbackParams) {
            var callback = memos[memoId];
            if (callback === undefined)
                throw new Error("Couldn't find any memo with ID " + memoId + ". Perhaps it's already been unmemoized.");
            try {
                callback.apply(null, callbackParams || []);
                return true;
            }
            finally { delete memos[memoId]; }
        },

        unmemoizeDomNodeAndDescendants: function (domNode, extraCallbackParamsArray) {
            var memos = [];
            findMemoNodes(domNode, memos);
            for (var i = 0, j = memos.length; i < j; i++) {
                var node = memos[i].domNode;
                var combinedParams = [node];
                if (extraCallbackParamsArray)
                    ko.utils.arrayPushAll(combinedParams, extraCallbackParamsArray);
                ko.memoization.unmemoize(memos[i].memoId, combinedParams);
                node.nodeValue = ""; // Neuter this node so we don't try to unmemoize it again
                if (node.parentNode)
                    node.parentNode.removeChild(node); // If possible, erase it totally (not always possible - someone else might just hold a reference to it then call unmemoizeDomNodeAndDescendants again)
            }
        },

        parseMemoText: function (memoText) {
            var match = memoText.match(/^\[ko_memo\:(.*?)\]$/);
            return match ? match[1] : null;
        }
    };
})();

ko.exportSymbol('memoization', ko.memoization);
ko.exportSymbol('memoization.memoize', ko.memoization.memoize);
ko.exportSymbol('memoization.unmemoize', ko.memoization.unmemoize);
ko.exportSymbol('memoization.parseMemoText', ko.memoization.parseMemoText);
ko.exportSymbol('memoization.unmemoizeDomNodeAndDescendants', ko.memoization.unmemoizeDomNodeAndDescendants);
ko.extenders = {
    'throttle': function(target, timeout) {
        // Throttling means two things:

        // (1) For dependent observables, we throttle *evaluations* so that, no matter how fast its dependencies
        //     notify updates, the target doesn't re-evaluate (and hence doesn't notify) faster than a certain rate
        target['throttleEvaluation'] = timeout;

        // (2) For writable targets (observables, or writable dependent observables), we throttle *writes*
        //     so the target cannot change value synchronously or faster than a certain rate
        var writeTimeoutInstance = null;
        return ko.dependentObservable({
            'read': target,
            'write': function(value) {
                clearTimeout(writeTimeoutInstance);
                writeTimeoutInstance = setTimeout(function() {
                    target(value);
                }, timeout);
            }
        });
    },

    'rateLimit': function(target, options) {
        var timeout, method, limitFunction;

        if (typeof options == 'number') {
            timeout = options;
        } else {
            timeout = options['timeout'];
            method = options['method'];
        }

        limitFunction = method == 'notifyWhenChangesStop' ?  debounce : throttle;
        target.limit(function(callback) {
            return limitFunction(callback, timeout);
        });
    },

    'notify': function(target, notifyWhen) {
        target["equalityComparer"] = notifyWhen == "always" ?
            null :  // null equalityComparer means to always notify
            valuesArePrimitiveAndEqual;
    }
};

var primitiveTypes = { 'undefined':1, 'boolean':1, 'number':1, 'string':1 };
function valuesArePrimitiveAndEqual(a, b) {
    var oldValueIsPrimitive = (a === null) || (typeof(a) in primitiveTypes);
    return oldValueIsPrimitive ? (a === b) : false;
}

function throttle(callback, timeout) {
    var timeoutInstance;
    return function () {
        if (!timeoutInstance) {
            timeoutInstance = setTimeout(function() {
                timeoutInstance = undefined;
                callback();
            }, timeout);
        }
    };
}

function debounce(callback, timeout) {
    var timeoutInstance;
    return function () {
        clearTimeout(timeoutInstance);
        timeoutInstance = setTimeout(callback, timeout);
    };
}

function applyExtenders(requestedExtenders) {
    var target = this;
    if (requestedExtenders) {
        ko.utils.objectForEach(requestedExtenders, function(key, value) {
            var extenderHandler = ko.extenders[key];
            if (typeof extenderHandler == 'function') {
                target = extenderHandler(target, value) || target;
            }
        });
    }
    return target;
}

ko.exportSymbol('extenders', ko.extenders);

ko.subscription = function (target, callback, disposeCallback) {
    this.target = target;
    this.callback = callback;
    this.disposeCallback = disposeCallback;
    this.isDisposed = false;
    ko.exportProperty(this, 'dispose', this.dispose);
};
ko.subscription.prototype.dispose = function () {
    this.isDisposed = true;
    this.disposeCallback();
};

ko.subscribable = function () {
    ko.utils.setPrototypeOfOrExtend(this, ko.subscribable['fn']);
    this._subscriptions = {};
}

var defaultEvent = "change";

var ko_subscribable_fn = {
    subscribe: function (callback, callbackTarget, event) {
        var self = this;

        event = event || defaultEvent;
        var boundCallback = callbackTarget ? callback.bind(callbackTarget) : callback;

        var subscription = new ko.subscription(self, boundCallback, function () {
            ko.utils.arrayRemoveItem(self._subscriptions[event], subscription);
            if (self.afterSubscriptionRemove)
                self.afterSubscriptionRemove(event);
        });

        if (self.beforeSubscriptionAdd)
            self.beforeSubscriptionAdd(event);

        if (!self._subscriptions[event])
            self._subscriptions[event] = [];
        self._subscriptions[event].push(subscription);

        return subscription;
    },

    "notifySubscribers": function (valueToNotify, event) {
        event = event || defaultEvent;
        if (this.hasSubscriptionsForEvent(event)) {
            try {
                ko.dependencyDetection.begin(); // Begin suppressing dependency detection (by setting the top frame to undefined)
                for (var a = this._subscriptions[event].slice(0), i = 0, subscription; subscription = a[i]; ++i) {
                    // In case a subscription was disposed during the arrayForEach cycle, check
                    // for isDisposed on each subscription before invoking its callback
                    if (!subscription.isDisposed)
                        subscription.callback(valueToNotify);
                }
            } finally {
                ko.dependencyDetection.end(); // End suppressing dependency detection
            }
        }
    },

    limit: function(limitFunction) {
        var self = this, selfIsObservable = ko.isObservable(self),
            isPending, previousValue, pendingValue, beforeChange = 'beforeChange';

        if (!self._origNotifySubscribers) {
            self._origNotifySubscribers = self["notifySubscribers"];
            self["notifySubscribers"] = function(value, event) {
                if (!event || event === defaultEvent) {
                    self._rateLimitedChange(value);
                } else if (event === beforeChange) {
                    self._rateLimitedBeforeChange(value);
                } else {
                    self._origNotifySubscribers(value, event);
                }
            };
        }

        var finish = limitFunction(function() {
            // If an observable provided a reference to itself, access it to get the latest value.
            // This allows computed observables to delay calculating their value until needed.
            if (selfIsObservable && pendingValue === self) {
                pendingValue = self();
            }
            isPending = false;
            if (self.isDifferent(previousValue, pendingValue)) {
                self._origNotifySubscribers(previousValue = pendingValue);
            }
        });

        self._rateLimitedChange = function(value) {
            isPending = true;
            pendingValue = value;
            finish();
        };
        self._rateLimitedBeforeChange = function(value) {
            if (!isPending) {
                previousValue = value;
                self._origNotifySubscribers(value, beforeChange);
            }
        };
    },

    hasSubscriptionsForEvent: function(event) {
        return this._subscriptions[event] && this._subscriptions[event].length;
    },

    getSubscriptionsCount: function () {
        var total = 0;
        ko.utils.objectForEach(this._subscriptions, function(eventName, subscriptions) {
            total += subscriptions.length;
        });
        return total;
    },

    isDifferent: function(oldValue, newValue) {
        return !this['equalityComparer'] || !this['equalityComparer'](oldValue, newValue);
    },

    extend: applyExtenders
};

ko.exportProperty(ko_subscribable_fn, 'subscribe', ko_subscribable_fn.subscribe);
ko.exportProperty(ko_subscribable_fn, 'extend', ko_subscribable_fn.extend);
ko.exportProperty(ko_subscribable_fn, 'getSubscriptionsCount', ko_subscribable_fn.getSubscriptionsCount);

// For browsers that support proto assignment, we overwrite the prototype of each
// observable instance. Since observables are functions, we need Function.prototype
// to still be in the prototype chain.
if (ko.utils.canSetPrototype) {
    ko.utils.setPrototypeOf(ko_subscribable_fn, Function.prototype);
}

ko.subscribable['fn'] = ko_subscribable_fn;


ko.isSubscribable = function (instance) {
    return instance != null && typeof instance.subscribe == "function" && typeof instance["notifySubscribers"] == "function";
};

ko.exportSymbol('subscribable', ko.subscribable);
ko.exportSymbol('isSubscribable', ko.isSubscribable);

ko.computedContext = ko.dependencyDetection = (function () {
    var outerFrames = [],
        currentFrame,
        lastId = 0;

    // Return a unique ID that can be assigned to an observable for dependency tracking.
    // Theoretically, you could eventually overflow the number storage size, resulting
    // in duplicate IDs. But in JavaScript, the largest exact integral value is 2^53
    // or 9,007,199,254,740,992. If you created 1,000,000 IDs per second, it would
    // take over 285 years to reach that number.
    // Reference http://blog.vjeux.com/2010/javascript/javascript-max_int-number-limits.html
    function getId() {
        return ++lastId;
    }

    function begin(options) {
        outerFrames.push(currentFrame);
        currentFrame = options;
    }

    function end() {
        currentFrame = outerFrames.pop();
    }

    return {
        begin: begin,

        end: end,

        registerDependency: function (subscribable) {
            if (currentFrame) {
                if (!ko.isSubscribable(subscribable))
                    throw new Error("Only subscribable things can act as dependencies");
                currentFrame.callback(subscribable, subscribable._id || (subscribable._id = getId()));
            }
        },

        ignore: function (callback, callbackTarget, callbackArgs) {
            try {
                begin();
                return callback.apply(callbackTarget, callbackArgs || []);
            } finally {
                end();
            }
        },

        getDependenciesCount: function () {
            if (currentFrame)
                return currentFrame.computed.getDependenciesCount();
        },

        isInitial: function() {
            if (currentFrame)
                return currentFrame.isInitial;
        }
    };
})();

ko.exportSymbol('computedContext', ko.computedContext);
ko.exportSymbol('computedContext.getDependenciesCount', ko.computedContext.getDependenciesCount);
ko.exportSymbol('computedContext.isInitial', ko.computedContext.isInitial);
ko.exportSymbol('computedContext.isSleeping', ko.computedContext.isSleeping);
ko.observable = function (initialValue) {
    var _latestValue = initialValue;

    function observable() {
        if (arguments.length > 0) {
            // Write

            // Ignore writes if the value hasn't changed
            if (observable.isDifferent(_latestValue, arguments[0])) {
                observable.valueWillMutate();
                _latestValue = arguments[0];
                if (DEBUG) observable._latestValue = _latestValue;
                observable.valueHasMutated();
            }
            return this; // Permits chained assignments
        }
        else {
            // Read
            ko.dependencyDetection.registerDependency(observable); // The caller only needs to be notified of changes if they did a "read" operation
            return _latestValue;
        }
    }
    ko.subscribable.call(observable);
    ko.utils.setPrototypeOfOrExtend(observable, ko.observable['fn']);

    if (DEBUG) observable._latestValue = _latestValue;
    observable.peek = function() { return _latestValue };
    observable.valueHasMutated = function () { observable["notifySubscribers"](_latestValue); }
    observable.valueWillMutate = function () { observable["notifySubscribers"](_latestValue, "beforeChange"); }

    ko.exportProperty(observable, 'peek', observable.peek);
    ko.exportProperty(observable, "valueHasMutated", observable.valueHasMutated);
    ko.exportProperty(observable, "valueWillMutate", observable.valueWillMutate);

    return observable;
}

ko.observable['fn'] = {
    "equalityComparer": valuesArePrimitiveAndEqual
};

var protoProperty = ko.observable.protoProperty = "__ko_proto__";
ko.observable['fn'][protoProperty] = ko.observable;

// Note that for browsers that don't support proto assignment, the
// inheritance chain is created manually in the ko.observable constructor
if (ko.utils.canSetPrototype) {
    ko.utils.setPrototypeOf(ko.observable['fn'], ko.subscribable['fn']);
}

ko.hasPrototype = function(instance, prototype) {
    if ((instance === null) || (instance === undefined) || (instance[protoProperty] === undefined)) return false;
    if (instance[protoProperty] === prototype) return true;
    return ko.hasPrototype(instance[protoProperty], prototype); // Walk the prototype chain
};

ko.isObservable = function (instance) {
    return ko.hasPrototype(instance, ko.observable);
}
ko.isWriteableObservable = function (instance) {
    // Observable
    if ((typeof instance == "function") && instance[protoProperty] === ko.observable)
        return true;
    // Writeable dependent observable
    if ((typeof instance == "function") && (instance[protoProperty] === ko.dependentObservable) && (instance.hasWriteFunction))
        return true;
    // Anything else
    return false;
}


ko.exportSymbol('observable', ko.observable);
ko.exportSymbol('isObservable', ko.isObservable);
ko.exportSymbol('isWriteableObservable', ko.isWriteableObservable);
ko.exportSymbol('isWritableObservable', ko.isWriteableObservable);
ko.observableArray = function (initialValues) {
    initialValues = initialValues || [];

    if (typeof initialValues != 'object' || !('length' in initialValues))
        throw new Error("The argument passed when initializing an observable array must be an array, or null, or undefined.");

    var result = ko.observable(initialValues);
    ko.utils.setPrototypeOfOrExtend(result, ko.observableArray['fn']);
    return result.extend({'trackArrayChanges':true});
};

ko.observableArray['fn'] = {
    'remove': function (valueOrPredicate) {
        var underlyingArray = this.peek();
        var removedValues = [];
        var predicate = typeof valueOrPredicate == "function" && !ko.isObservable(valueOrPredicate) ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
        for (var i = 0; i < underlyingArray.length; i++) {
            var value = underlyingArray[i];
            if (predicate(value)) {
                if (removedValues.length === 0) {
                    this.valueWillMutate();
                }
                removedValues.push(value);
                underlyingArray.splice(i, 1);
                i--;
            }
        }
        if (removedValues.length) {
            this.valueHasMutated();
        }
        return removedValues;
    },

    'removeAll': function (arrayOfValues) {
        // If you passed zero args, we remove everything
        if (arrayOfValues === undefined) {
            var underlyingArray = this.peek();
            var allValues = underlyingArray.slice(0);
            this.valueWillMutate();
            underlyingArray.splice(0, underlyingArray.length);
            this.valueHasMutated();
            return allValues;
        }
        // If you passed an arg, we interpret it as an array of entries to remove
        if (!arrayOfValues)
            return [];
        return this['remove'](function (value) {
            return ko.utils.arrayIndexOf(arrayOfValues, value) >= 0;
        });
    },

    'destroy': function (valueOrPredicate) {
        var underlyingArray = this.peek();
        var predicate = typeof valueOrPredicate == "function" && !ko.isObservable(valueOrPredicate) ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
        this.valueWillMutate();
        for (var i = underlyingArray.length - 1; i >= 0; i--) {
            var value = underlyingArray[i];
            if (predicate(value))
                underlyingArray[i]["_destroy"] = true;
        }
        this.valueHasMutated();
    },

    'destroyAll': function (arrayOfValues) {
        // If you passed zero args, we destroy everything
        if (arrayOfValues === undefined)
            return this['destroy'](function() { return true });

        // If you passed an arg, we interpret it as an array of entries to destroy
        if (!arrayOfValues)
            return [];
        return this['destroy'](function (value) {
            return ko.utils.arrayIndexOf(arrayOfValues, value) >= 0;
        });
    },

    'indexOf': function (item) {
        var underlyingArray = this();
        return ko.utils.arrayIndexOf(underlyingArray, item);
    },

    'replace': function(oldItem, newItem) {
        var index = this['indexOf'](oldItem);
        if (index >= 0) {
            this.valueWillMutate();
            this.peek()[index] = newItem;
            this.valueHasMutated();
        }
    }
};

// Populate ko.observableArray.fn with read/write functions from native arrays
// Important: Do not add any additional functions here that may reasonably be used to *read* data from the array
// because we'll eval them without causing subscriptions, so ko.computed output could end up getting stale
ko.utils.arrayForEach(["pop", "push", "reverse", "shift", "sort", "splice", "unshift"], function (methodName) {
    ko.observableArray['fn'][methodName] = function () {
        // Use "peek" to avoid creating a subscription in any computed that we're executing in the context of
        // (for consistency with mutating regular observables)
        var underlyingArray = this.peek();
        this.valueWillMutate();
        this.cacheDiffForKnownOperation(underlyingArray, methodName, arguments);
        var methodCallResult = underlyingArray[methodName].apply(underlyingArray, arguments);
        this.valueHasMutated();
        return methodCallResult;
    };
});

// Populate ko.observableArray.fn with read-only functions from native arrays
ko.utils.arrayForEach(["slice"], function (methodName) {
    ko.observableArray['fn'][methodName] = function () {
        var underlyingArray = this();
        return underlyingArray[methodName].apply(underlyingArray, arguments);
    };
});

// Note that for browsers that don't support proto assignment, the
// inheritance chain is created manually in the ko.observableArray constructor
if (ko.utils.canSetPrototype) {
    ko.utils.setPrototypeOf(ko.observableArray['fn'], ko.observable['fn']);
}

ko.exportSymbol('observableArray', ko.observableArray);
var arrayChangeEventName = 'arrayChange';
ko.extenders['trackArrayChanges'] = function(target) {
    // Only modify the target observable once
    if (target.cacheDiffForKnownOperation) {
        return;
    }
    var trackingChanges = false,
        cachedDiff = null,
        pendingNotifications = 0,
        underlyingSubscribeFunction = target.subscribe;

    // Intercept "subscribe" calls, and for array change events, ensure change tracking is enabled
    target.subscribe = target['subscribe'] = function(callback, callbackTarget, event) {
        if (event === arrayChangeEventName) {
            trackChanges();
        }
        return underlyingSubscribeFunction.apply(this, arguments);
    };

    function trackChanges() {
        // Calling 'trackChanges' multiple times is the same as calling it once
        if (trackingChanges) {
            return;
        }

        trackingChanges = true;

        // Intercept "notifySubscribers" to track how many times it was called.
        var underlyingNotifySubscribersFunction = target['notifySubscribers'];
        target['notifySubscribers'] = function(valueToNotify, event) {
            if (!event || event === defaultEvent) {
                ++pendingNotifications;
            }
            return underlyingNotifySubscribersFunction.apply(this, arguments);
        };

        // Each time the array changes value, capture a clone so that on the next
        // change it's possible to produce a diff
        var previousContents = [].concat(target.peek() || []);
        cachedDiff = null;
        target.subscribe(function(currentContents) {
            // Make a copy of the current contents and ensure it's an array
            currentContents = [].concat(currentContents || []);

            // Compute the diff and issue notifications, but only if someone is listening
            if (target.hasSubscriptionsForEvent(arrayChangeEventName)) {
                var changes = getChanges(previousContents, currentContents);
                if (changes.length) {
                    target['notifySubscribers'](changes, arrayChangeEventName);
                }
            }

            // Eliminate references to the old, removed items, so they can be GCed
            previousContents = currentContents;
            cachedDiff = null;
            pendingNotifications = 0;
        });
    }

    function getChanges(previousContents, currentContents) {
        // We try to re-use cached diffs.
        // The scenarios where pendingNotifications > 1 are when using rate-limiting or the Deferred Updates
        // plugin, which without this check would not be compatible with arrayChange notifications. Normally,
        // notifications are issued immediately so we wouldn't be queueing up more than one.
        if (!cachedDiff || pendingNotifications > 1) {
            cachedDiff = ko.utils.compareArrays(previousContents, currentContents, { 'sparse': true });
        }

        return cachedDiff;
    }

    target.cacheDiffForKnownOperation = function(rawArray, operationName, args) {
        // Only run if we're currently tracking changes for this observable array
        // and there aren't any pending deferred notifications.
        if (!trackingChanges || pendingNotifications) {
            return;
        }
        var diff = [],
            arrayLength = rawArray.length,
            argsLength = args.length,
            offset = 0;

        function pushDiff(status, value, index) {
            return diff[diff.length] = { 'status': status, 'value': value, 'index': index };
        }
        switch (operationName) {
            case 'push':
                offset = arrayLength;
            case 'unshift':
                for (var index = 0; index < argsLength; index++) {
                    pushDiff('added', args[index], offset + index);
                }
                break;

            case 'pop':
                offset = arrayLength - 1;
            case 'shift':
                if (arrayLength) {
                    pushDiff('deleted', rawArray[offset], offset);
                }
                break;

            case 'splice':
                // Negative start index means 'from end of array'. After that we clamp to [0...arrayLength].
                // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice
                var startIndex = Math.min(Math.max(0, args[0] < 0 ? arrayLength + args[0] : args[0]), arrayLength),
                    endDeleteIndex = argsLength === 1 ? arrayLength : Math.min(startIndex + (args[1] || 0), arrayLength),
                    endAddIndex = startIndex + argsLength - 2,
                    endIndex = Math.max(endDeleteIndex, endAddIndex),
                    additions = [], deletions = [];
                for (var index = startIndex, argsIndex = 2; index < endIndex; ++index, ++argsIndex) {
                    if (index < endDeleteIndex)
                        deletions.push(pushDiff('deleted', rawArray[index], index));
                    if (index < endAddIndex)
                        additions.push(pushDiff('added', args[argsIndex], index));
                }
                ko.utils.findMovesInArrayComparison(deletions, additions);
                break;

            default:
                return;
        }
        cachedDiff = diff;
    };
};
ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) {
    var _latestValue,
        _needsEvaluation = true,
        _isBeingEvaluated = false,
        _suppressDisposalUntilDisposeWhenReturnsFalse = false,
        _isDisposed = false,
        readFunction = evaluatorFunctionOrOptions,
        pure = false,
        isSleeping = false;

    if (readFunction && typeof readFunction == "object") {
        // Single-parameter syntax - everything is on this "options" param
        options = readFunction;
        readFunction = options["read"];
    } else {
        // Multi-parameter syntax - construct the options according to the params passed
        options = options || {};
        if (!readFunction)
            readFunction = options["read"];
    }
    if (typeof readFunction != "function")
        throw new Error("Pass a function that returns the value of the ko.computed");

    function addSubscriptionToDependency(subscribable, id) {
        if (!_subscriptionsToDependencies[id]) {
            _subscriptionsToDependencies[id] = subscribable.subscribe(evaluatePossiblyAsync);
            ++_dependenciesCount;
        }
    }

    function disposeAllSubscriptionsToDependencies() {
        ko.utils.objectForEach(_subscriptionsToDependencies, function (id, subscription) {
            subscription.dispose();
        });
        _subscriptionsToDependencies = {};
    }

    function disposeComputed() {
        disposeAllSubscriptionsToDependencies();
        _dependenciesCount = 0;
        _isDisposed = true;
        _needsEvaluation = false;
    }

    function evaluatePossiblyAsync() {
        var throttleEvaluationTimeout = dependentObservable['throttleEvaluation'];
        if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) {
            clearTimeout(evaluationTimeoutInstance);
            evaluationTimeoutInstance = setTimeout(evaluateImmediate, throttleEvaluationTimeout);
        } else if (dependentObservable._evalRateLimited) {
            dependentObservable._evalRateLimited();
        } else {
            evaluateImmediate();
        }
    }

    function evaluateImmediate(suppressChangeNotification) {
        if (_isBeingEvaluated) {
            if (pure) {
                throw Error("A 'pure' computed must not be called recursively");
            }
            // If the evaluation of a ko.computed causes side effects, it's possible that it will trigger its own re-evaluation.
            // This is not desirable (it's hard for a developer to realise a chain of dependencies might cause this, and they almost
            // certainly didn't intend infinite re-evaluations). So, for predictability, we simply prevent ko.computeds from causing
            // their own re-evaluation. Further discussion at https://github.com/SteveSanderson/knockout/pull/387
            return;
        }

        // Do not evaluate (and possibly capture new dependencies) if disposed
        if (_isDisposed) {
            return;
        }

        if (disposeWhen && disposeWhen()) {
            // See comment below about _suppressDisposalUntilDisposeWhenReturnsFalse
            if (!_suppressDisposalUntilDisposeWhenReturnsFalse) {
                dispose();
                return;
            }
        } else {
            // It just did return false, so we can stop suppressing now
            _suppressDisposalUntilDisposeWhenReturnsFalse = false;
        }

        _isBeingEvaluated = true;

        // When sleeping, recalculate the value and return.
        if (isSleeping) {
            try {
                var dependencyTracking = {};
                ko.dependencyDetection.begin({
                    callback: function (subscribable, id) {
                        if (!dependencyTracking[id]) {
                            dependencyTracking[id] = 1;
                            ++_dependenciesCount;
                        }
                    },
                    computed: dependentObservable,
                    isInitial: undefined
                });
                _dependenciesCount = 0;
                _latestValue = readFunction.call(evaluatorFunctionTarget);
            } finally {
                ko.dependencyDetection.end();
                _isBeingEvaluated = false;
            }
        } else {
            try {
                // Initially, we assume that none of the subscriptions are still being used (i.e., all are candidates for disposal).
                // Then, during evaluation, we cross off any that are in fact still being used.
                var disposalCandidates = _subscriptionsToDependencies, disposalCount = _dependenciesCount;
                ko.dependencyDetection.begin({
                    callback: function(subscribable, id) {
                        if (!_isDisposed) {
                            if (disposalCount && disposalCandidates[id]) {
                                // Don't want to dispose this subscription, as it's still being used
                                _subscriptionsToDependencies[id] = disposalCandidates[id];
                                ++_dependenciesCount;
                                delete disposalCandidates[id];
                                --disposalCount;
                            } else {
                                // Brand new subscription - add it
                                addSubscriptionToDependency(subscribable, id);
                            }
                        }
                    },
                    computed: dependentObservable,
                    isInitial: pure ? undefined : !_dependenciesCount        // If we're evaluating when there are no previous dependencies, it must be the first time
                });

                _subscriptionsToDependencies = {};
                _dependenciesCount = 0;

                try {
                    var newValue = evaluatorFunctionTarget ? readFunction.call(evaluatorFunctionTarget) : readFunction();

                } finally {
                    ko.dependencyDetection.end();

                    // For each subscription no longer being used, remove it from the active subscriptions list and dispose it
                    if (disposalCount) {
                        ko.utils.objectForEach(disposalCandidates, function(id, toDispose) {
                            toDispose.dispose();
                        });
                    }

                    _needsEvaluation = false;
                }

                if (dependentObservable.isDifferent(_latestValue, newValue)) {
                    dependentObservable["notifySubscribers"](_latestValue, "beforeChange");

                    _latestValue = newValue;
                    if (DEBUG) dependentObservable._latestValue = _latestValue;

                    if (suppressChangeNotification !== true) {  // Check for strict true value since setTimeout in Firefox passes a numeric value to the function
                        dependentObservable["notifySubscribers"](_latestValue);
                    }
                }
            } finally {
                _isBeingEvaluated = false;
            }
        }

        if (!_dependenciesCount)
            dispose();
    }

    function dependentObservable() {
        if (arguments.length > 0) {
            if (typeof writeFunction === "function") {
                // Writing a value
                writeFunction.apply(evaluatorFunctionTarget, arguments);
            } else {
                throw new Error("Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.");
            }
            return this; // Permits chained assignments
        } else {
            // Reading the value
            ko.dependencyDetection.registerDependency(dependentObservable);
            if (_needsEvaluation)
                evaluateImmediate(true /* suppressChangeNotification */);
            return _latestValue;
        }
    }

    function peek() {
        // Peek won't re-evaluate, except to get the initial value when "deferEvaluation" is set, or while the computed is sleeping.
        // Those are the only times that both of these conditions will be satisfied.
        if (_needsEvaluation && !_dependenciesCount)
            evaluateImmediate(true /* suppressChangeNotification */);
        return _latestValue;
    }

    function isActive() {
        return _needsEvaluation || _dependenciesCount > 0;
    }

    // By here, "options" is always non-null
    var writeFunction = options["write"],
        disposeWhenNodeIsRemoved = options["disposeWhenNodeIsRemoved"] || options.disposeWhenNodeIsRemoved || null,
        disposeWhenOption = options["disposeWhen"] || options.disposeWhen,
        disposeWhen = disposeWhenOption,
        dispose = disposeComputed,
        _subscriptionsToDependencies = {},
        _dependenciesCount = 0,
        evaluationTimeoutInstance = null;

    if (!evaluatorFunctionTarget)
        evaluatorFunctionTarget = options["owner"];

    ko.subscribable.call(dependentObservable);
    ko.utils.setPrototypeOfOrExtend(dependentObservable, ko.dependentObservable['fn']);

    dependentObservable.peek = peek;
    dependentObservable.getDependenciesCount = function () { return _dependenciesCount; };
    dependentObservable.hasWriteFunction = typeof options["write"] === "function";
    dependentObservable.dispose = function () { dispose(); };
    dependentObservable.isActive = isActive;

    // Replace the limit function with one that delays evaluation as well.
    var originalLimit = dependentObservable.limit;
    dependentObservable.limit = function(limitFunction) {
        originalLimit.call(dependentObservable, limitFunction);
        dependentObservable._evalRateLimited = function() {
            dependentObservable._rateLimitedBeforeChange(_latestValue);

            _needsEvaluation = true;    // Mark as dirty

            // Pass the observable to the rate-limit code, which will access it when
            // it's time to do the notification.
            dependentObservable._rateLimitedChange(dependentObservable);
        }
    };

    if (options['pure']) {
        pure = true;
        isSleeping = true;     // Starts off sleeping; will awake on the first subscription
        dependentObservable.beforeSubscriptionAdd = function () {
            // If asleep, wake up the computed and evaluate to register any dependencies.
            if (isSleeping) {
                isSleeping = false;
                evaluateImmediate(true /* suppressChangeNotification */);
            }
        }
        dependentObservable.afterSubscriptionRemove = function () {
            if (!dependentObservable.getSubscriptionsCount()) {
                disposeAllSubscriptionsToDependencies();
                isSleeping = _needsEvaluation = true;
            }
        }
    } else if (options['deferEvaluation']) {
        // This will force a computed with deferEvaluation to evaluate when the first subscriptions is registered.
        dependentObservable.beforeSubscriptionAdd = function () {
            peek();
            delete dependentObservable.beforeSubscriptionAdd;
        }
    }

    ko.exportProperty(dependentObservable, 'peek', dependentObservable.peek);
    ko.exportProperty(dependentObservable, 'dispose', dependentObservable.dispose);
    ko.exportProperty(dependentObservable, 'isActive', dependentObservable.isActive);
    ko.exportProperty(dependentObservable, 'getDependenciesCount', dependentObservable.getDependenciesCount);

    // Add a "disposeWhen" callback that, on each evaluation, disposes if the node was removed without using ko.removeNode.
    if (disposeWhenNodeIsRemoved) {
        // Since this computed is associated with a DOM node, and we don't want to dispose the computed
        // until the DOM node is *removed* from the document (as opposed to never having been in the document),
        // we'll prevent disposal until "disposeWhen" first returns false.
        _suppressDisposalUntilDisposeWhenReturnsFalse = true;

        // Only watch for the node's disposal if the value really is a node. It might not be,
        // e.g., { disposeWhenNodeIsRemoved: true } can be used to opt into the "only dispose
        // after first false result" behaviour even if there's no specific node to watch. This
        // technique is intended for KO's internal use only and shouldn't be documented or used
        // by application code, as it's likely to change in a future version of KO.
        if (disposeWhenNodeIsRemoved.nodeType) {
            disposeWhen = function () {
                return !ko.utils.domNodeIsAttachedToDocument(disposeWhenNodeIsRemoved) || (disposeWhenOption && disposeWhenOption());
            };
        }
    }

    // Evaluate, unless sleeping or deferEvaluation is true
    if (!isSleeping && !options['deferEvaluation'])
        evaluateImmediate();

    // Attach a DOM node disposal callback so that the computed will be proactively disposed as soon as the node is
    // removed using ko.removeNode. But skip if isActive is false (there will never be any dependencies to dispose).
    if (disposeWhenNodeIsRemoved && isActive() && disposeWhenNodeIsRemoved.nodeType) {
        dispose = function() {
            ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, dispose);
            disposeComputed();
        };
        ko.utils.domNodeDisposal.addDisposeCallback(disposeWhenNodeIsRemoved, dispose);
    }

    return dependentObservable;
};

ko.isComputed = function(instance) {
    return ko.hasPrototype(instance, ko.dependentObservable);
};

var protoProp = ko.observable.protoProperty; // == "__ko_proto__"
ko.dependentObservable[protoProp] = ko.observable;

ko.dependentObservable['fn'] = {
    "equalityComparer": valuesArePrimitiveAndEqual
};
ko.dependentObservable['fn'][protoProp] = ko.dependentObservable;

// Note that for browsers that don't support proto assignment, the
// inheritance chain is created manually in the ko.dependentObservable constructor
if (ko.utils.canSetPrototype) {
    ko.utils.setPrototypeOf(ko.dependentObservable['fn'], ko.subscribable['fn']);
}

ko.exportSymbol('dependentObservable', ko.dependentObservable);
ko.exportSymbol('computed', ko.dependentObservable); // Make "ko.computed" an alias for "ko.dependentObservable"
ko.exportSymbol('isComputed', ko.isComputed);

ko.pureComputed = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget) {
    if (typeof evaluatorFunctionOrOptions === 'function') {
        return ko.computed(evaluatorFunctionOrOptions, evaluatorFunctionTarget, {'pure':true});
    } else {
        evaluatorFunctionOrOptions = ko.utils.extend({}, evaluatorFunctionOrOptions);   // make a copy of the parameter object
        evaluatorFunctionOrOptions['pure'] = true;
        return ko.computed(evaluatorFunctionOrOptions, evaluatorFunctionTarget);
    }
}
ko.exportSymbol('pureComputed', ko.pureComputed);

(function() {
    var maxNestedObservableDepth = 10; // Escape the (unlikely) pathalogical case where an observable's current value is itself (or similar reference cycle)

    ko.toJS = function(rootObject) {
        if (arguments.length == 0)
            throw new Error("When calling ko.toJS, pass the object you want to convert.");

        // We just unwrap everything at every level in the object graph
        return mapJsObjectGraph(rootObject, function(valueToMap) {
            // Loop because an observable's value might in turn be another observable wrapper
            for (var i = 0; ko.isObservable(valueToMap) && (i < maxNestedObservableDepth); i++)
                valueToMap = valueToMap();
            return valueToMap;
        });
    };

    ko.toJSON = function(rootObject, replacer, space) {     // replacer and space are optional
        var plainJavaScriptObject = ko.toJS(rootObject);
        return ko.utils.stringifyJson(plainJavaScriptObject, replacer, space);
    };

    function mapJsObjectGraph(rootObject, mapInputCallback, visitedObjects) {
        visitedObjects = visitedObjects || new objectLookup();

        rootObject = mapInputCallback(rootObject);
        var canHaveProperties = (typeof rootObject == "object") && (rootObject !== null) && (rootObject !== undefined) && (!(rootObject instanceof Date)) && (!(rootObject instanceof String)) && (!(rootObject instanceof Number)) && (!(rootObject instanceof Boolean));
        if (!canHaveProperties)
            return rootObject;

        var outputProperties = rootObject instanceof Array ? [] : {};
        visitedObjects.save(rootObject, outputProperties);

        visitPropertiesOrArrayEntries(rootObject, function(indexer) {
            var propertyValue = mapInputCallback(rootObject[indexer]);

            switch (typeof propertyValue) {
                case "boolean":
                case "number":
                case "string":
                case "function":
                    outputProperties[indexer] = propertyValue;
                    break;
                case "object":
                case "undefined":
                    var previouslyMappedValue = visitedObjects.get(propertyValue);
                    outputProperties[indexer] = (previouslyMappedValue !== undefined)
                        ? previouslyMappedValue
                        : mapJsObjectGraph(propertyValue, mapInputCallback, visitedObjects);
                    break;
            }
        });

        return outputProperties;
    }

    function visitPropertiesOrArrayEntries(rootObject, visitorCallback) {
        if (rootObject instanceof Array) {
            for (var i = 0; i < rootObject.length; i++)
                visitorCallback(i);

            // For arrays, also respect toJSON property for custom mappings (fixes #278)
            if (typeof rootObject['toJSON'] == 'function')
                visitorCallback('toJSON');
        } else {
            for (var propertyName in rootObject) {
                visitorCallback(propertyName);
            }
        }
    };

    function objectLookup() {
        this.keys = [];
        this.values = [];
    };

    objectLookup.prototype = {
        constructor: objectLookup,
        save: function(key, value) {
            var existingIndex = ko.utils.arrayIndexOf(this.keys, key);
            if (existingIndex >= 0)
                this.values[existingIndex] = value;
            else {
                this.keys.push(key);
                this.values.push(value);
            }
        },
        get: function(key) {
            var existingIndex = ko.utils.arrayIndexOf(this.keys, key);
            return (existingIndex >= 0) ? this.values[existingIndex] : undefined;
        }
    };
})();

ko.exportSymbol('toJS', ko.toJS);
ko.exportSymbol('toJSON', ko.toJSON);
(function () {
    var hasDomDataExpandoProperty = '__ko__hasDomDataOptionValue__';

    // Normally, SELECT elements and their OPTIONs can only take value of type 'string' (because the values
    // are stored on DOM attributes). ko.selectExtensions provides a way for SELECTs/OPTIONs to have values
    // that are arbitrary objects. This is very convenient when implementing things like cascading dropdowns.
    ko.selectExtensions = {
        readValue : function(element) {
            switch (ko.utils.tagNameLower(element)) {
                case 'option':
                    if (element[hasDomDataExpandoProperty] === true)
                        return ko.utils.domData.get(element, ko.bindingHandlers.options.optionValueDomDataKey);
                    return ko.utils.ieVersion <= 7
                        ? (element.getAttributeNode('value') && element.getAttributeNode('value').specified ? element.value : element.text)
                        : element.value;
                case 'select':
                    return element.selectedIndex >= 0 ? ko.selectExtensions.readValue(element.options[element.selectedIndex]) : undefined;
                default:
                    return element.value;
            }
        },

        writeValue: function(element, value, allowUnset) {
            switch (ko.utils.tagNameLower(element)) {
                case 'option':
                    switch(typeof value) {
                        case "string":
                            ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, undefined);
                            if (hasDomDataExpandoProperty in element) { // IE <= 8 throws errors if you delete non-existent properties from a DOM node
                                delete element[hasDomDataExpandoProperty];
                            }
                            element.value = value;
                            break;
                        default:
                            // Store arbitrary object using DomData
                            ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, value);
                            element[hasDomDataExpandoProperty] = true;

                            // Special treatment of numbers is just for backward compatibility. KO 1.2.1 wrote numerical values to element.value.
                            element.value = typeof value === "number" ? value : "";
                            break;
                    }
                    break;
                case 'select':
                    if (value === "" || value === null)       // A blank string or null value will select the caption
                        value = undefined;
                    var selection = -1;
                    for (var i = 0, n = element.options.length, optionValue; i < n; ++i) {
                        optionValue = ko.selectExtensions.readValue(element.options[i]);
                        // Include special check to handle selecting a caption with a blank string value
                        if (optionValue == value || (optionValue == "" && value === undefined)) {
                            selection = i;
                            break;
                        }
                    }
                    if (allowUnset || selection >= 0 || (value === undefined && element.size > 1)) {
                        element.selectedIndex = selection;
                    }
                    break;
                default:
                    if ((value === null) || (value === undefined))
                        value = "";
                    element.value = value;
                    break;
            }
        }
    };
})();

ko.exportSymbol('selectExtensions', ko.selectExtensions);
ko.exportSymbol('selectExtensions.readValue', ko.selectExtensions.readValue);
ko.exportSymbol('selectExtensions.writeValue', ko.selectExtensions.writeValue);
ko.expressionRewriting = (function () {
    var javaScriptReservedWords = ["true", "false", "null", "undefined"];

    // Matches something that can be assigned to--either an isolated identifier or something ending with a property accessor
    // This is designed to be simple and avoid false negatives, but could produce false positives (e.g., a+b.c).
    // This also will not properly handle nested brackets (e.g., obj1[obj2['prop']]; see #911).
    var javaScriptAssignmentTarget = /^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i;

    function getWriteableValue(expression) {
        if (ko.utils.arrayIndexOf(javaScriptReservedWords, expression) >= 0)
            return false;
        var match = expression.match(javaScriptAssignmentTarget);
        return match === null ? false : match[1] ? ('Object(' + match[1] + ')' + match[2]) : expression;
    }

    // The following regular expressions will be used to split an object-literal string into tokens

        // These two match strings, either with double quotes or single quotes
    var stringDouble = '"(?:[^"\\\\]|\\\\.)*"',
        stringSingle = "'(?:[^'\\\\]|\\\\.)*'",
        // Matches a regular expression (text enclosed by slashes), but will also match sets of divisions
        // as a regular expression (this is handled by the parsing loop below).
        stringRegexp = '/(?:[^/\\\\]|\\\\.)*/\w*',
        // These characters have special meaning to the parser and must not appear in the middle of a
        // token, except as part of a string.
        specials = ',"\'{}()/:[\\]',
        // Match text (at least two characters) that does not contain any of the above special characters,
        // although some of the special characters are allowed to start it (all but the colon and comma).
        // The text can contain spaces, but leading or trailing spaces are skipped.
        everyThingElse = '[^\\s:,/][^' + specials + ']*[^\\s' + specials + ']',
        // Match any non-space character not matched already. This will match colons and commas, since they're
        // not matched by "everyThingElse", but will also match any other single character that wasn't already
        // matched (for example: in "a: 1, b: 2", each of the non-space characters will be matched by oneNotSpace).
        oneNotSpace = '[^\\s]',

        // Create the actual regular expression by or-ing the above strings. The order is important.
        bindingToken = RegExp(stringDouble + '|' + stringSingle + '|' + stringRegexp + '|' + everyThingElse + '|' + oneNotSpace, 'g'),

        // Match end of previous token to determine whether a slash is a division or regex.
        divisionLookBehind = /[\])"'A-Za-z0-9_$]+$/,
        keywordRegexLookBehind = {'in':1,'return':1,'typeof':1};

    function parseObjectLiteral(objectLiteralString) {
        // Trim leading and trailing spaces from the string
        var str = ko.utils.stringTrim(objectLiteralString);

        // Trim braces '{' surrounding the whole object literal
        if (str.charCodeAt(0) === 123) str = str.slice(1, -1);

        // Split into tokens
        var result = [], toks = str.match(bindingToken), key, values, depth = 0;

        if (toks) {
            // Append a comma so that we don't need a separate code block to deal with the last item
            toks.push(',');

            for (var i = 0, tok; tok = toks[i]; ++i) {
                var c = tok.charCodeAt(0);
                // A comma signals the end of a key/value pair if depth is zero
                if (c === 44) { // ","
                    if (depth <= 0) {
                        if (key)
                            result.push(values ? {key: key, value: values.join('')} : {'unknown': key});
                        key = values = depth = 0;
                        continue;
                    }
                // Simply skip the colon that separates the name and value
                } else if (c === 58) { // ":"
                    if (!values)
                        continue;
                // A set of slashes is initially matched as a regular expression, but could be division
                } else if (c === 47 && i && tok.length > 1) {  // "/"
                    // Look at the end of the previous token to determine if the slash is actually division
                    var match = toks[i-1].match(divisionLookBehind);
                    if (match && !keywordRegexLookBehind[match[0]]) {
                        // The slash is actually a division punctuator; re-parse the remainder of the string (not including the slash)
                        str = str.substr(str.indexOf(tok) + 1);
                        toks = str.match(bindingToken);
                        toks.push(',');
                        i = -1;
                        // Continue with just the slash
                        tok = '/';
                    }
                // Increment depth for parentheses, braces, and brackets so that interior commas are ignored
                } else if (c === 40 || c === 123 || c === 91) { // '(', '{', '['
                    ++depth;
                } else if (c === 41 || c === 125 || c === 93) { // ')', '}', ']'
                    --depth;
                // The key must be a single token; if it's a string, trim the quotes
                } else if (!key && !values) {
                    key = (c === 34 || c === 39) /* '"', "'" */ ? tok.slice(1, -1) : tok;
                    continue;
                }
                if (values)
                    values.push(tok);
                else
                    values = [tok];
            }
        }
        return result;
    }

    // Two-way bindings include a write function that allow the handler to update the value even if it's not an observable.
    var twoWayBindings = {};

    function preProcessBindings(bindingsStringOrKeyValueArray, bindingOptions) {
        bindingOptions = bindingOptions || {};

        function processKeyValue(key, val) {
            var writableVal;
            function callPreprocessHook(obj) {
                return (obj && obj['preprocess']) ? (val = obj['preprocess'](val, key, processKeyValue)) : true;
            }
            if (!bindingParams) {
                if (!callPreprocessHook(ko['getBindingHandler'](key)))
                    return;

                if (twoWayBindings[key] && (writableVal = getWriteableValue(val))) {
                    // For two-way bindings, provide a write method in case the value
                    // isn't a writable observable.
                    propertyAccessorResultStrings.push("'" + key + "':function(_z){" + writableVal + "=_z}");
                }
            }
            // Values are wrapped in a function so that each value can be accessed independently
            if (makeValueAccessors) {
                val = 'function(){return ' + val + ' }';
            }
            resultStrings.push("'" + key + "':" + val);
        }

        var resultStrings = [],
            propertyAccessorResultStrings = [],
            makeValueAccessors = bindingOptions['valueAccessors'],
            bindingParams = bindingOptions['bindingParams'],
            keyValueArray = typeof bindingsStringOrKeyValueArray === "string" ?
                parseObjectLiteral(bindingsStringOrKeyValueArray) : bindingsStringOrKeyValueArray;

        ko.utils.arrayForEach(keyValueArray, function(keyValue) {
            processKeyValue(keyValue.key || keyValue['unknown'], keyValue.value);
        });

        if (propertyAccessorResultStrings.length)
            processKeyValue('_ko_property_writers', "{" + propertyAccessorResultStrings.join(",") + " }");

        return resultStrings.join(",");
    }

    return {
        bindingRewriteValidators: [],

        twoWayBindings: twoWayBindings,

        parseObjectLiteral: parseObjectLiteral,

        preProcessBindings: preProcessBindings,

        keyValueArrayContainsKey: function(keyValueArray, key) {
            for (var i = 0; i < keyValueArray.length; i++)
                if (keyValueArray[i]['key'] == key)
                    return true;
            return false;
        },

        // Internal, private KO utility for updating model properties from within bindings
        // property:            If the property being updated is (or might be) an observable, pass it here
        //                      If it turns out to be a writable observable, it will be written to directly
        // allBindings:         An object with a get method to retrieve bindings in the current execution context.
        //                      This will be searched for a '_ko_property_writers' property in case you're writing to a non-observable
        // key:                 The key identifying the property to be written. Example: for { hasFocus: myValue }, write to 'myValue' by specifying the key 'hasFocus'
        // value:               The value to be written
        // checkIfDifferent:    If true, and if the property being written is a writable observable, the value will only be written if
        //                      it is !== existing value on that writable observable
        writeValueToProperty: function(property, allBindings, key, value, checkIfDifferent) {
            if (!property || !ko.isObservable(property)) {
                var propWriters = allBindings.get('_ko_property_writers');
                if (propWriters && propWriters[key])
                    propWriters[key](value);
            } else if (ko.isWriteableObservable(property) && (!checkIfDifferent || property.peek() !== value)) {
                property(value);
            }
        }
    };
})();

ko.exportSymbol('expressionRewriting', ko.expressionRewriting);
ko.exportSymbol('expressionRewriting.bindingRewriteValidators', ko.expressionRewriting.bindingRewriteValidators);
ko.exportSymbol('expressionRewriting.parseObjectLiteral', ko.expressionRewriting.parseObjectLiteral);
ko.exportSymbol('expressionRewriting.preProcessBindings', ko.expressionRewriting.preProcessBindings);

// Making bindings explicitly declare themselves as "two way" isn't ideal in the long term (it would be better if
// all bindings could use an official 'property writer' API without needing to declare that they might). However,
// since this is not, and has never been, a public API (_ko_property_writers was never documented), it's acceptable
// as an internal implementation detail in the short term.
// For those developers who rely on _ko_property_writers in their custom bindings, we expose _twoWayBindings as an
// undocumented feature that makes it relatively easy to upgrade to KO 3.0. However, this is still not an official
// public API, and we reserve the right to remove it at any time if we create a real public property writers API.
ko.exportSymbol('expressionRewriting._twoWayBindings', ko.expressionRewriting.twoWayBindings);

// For backward compatibility, define the following aliases. (Previously, these function names were misleading because
// they referred to JSON specifically, even though they actually work with arbitrary JavaScript object literal expressions.)
ko.exportSymbol('jsonExpressionRewriting', ko.expressionRewriting);
ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.expressionRewriting.preProcessBindings);
(function() {
    // "Virtual elements" is an abstraction on top of the usual DOM API which understands the notion that comment nodes
    // may be used to represent hierarchy (in addition to the DOM's natural hierarchy).
    // If you call the DOM-manipulating functions on ko.virtualElements, you will be able to read and write the state
    // of that virtual hierarchy
    //
    // The point of all this is to support containerless templates (e.g., <!-- ko foreach:someCollection -->blah<!-- /ko -->)
    // without having to scatter special cases all over the binding and templating code.

    // IE 9 cannot reliably read the "nodeValue" property of a comment node (see https://github.com/SteveSanderson/knockout/issues/186)
    // but it does give them a nonstandard alternative property called "text" that it can read reliably. Other browsers don't have that property.
    // So, use node.text where available, and node.nodeValue elsewhere
    var commentNodesHaveTextProperty = document && document.createComment("test").text === "<!--test-->";

    var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko(?:\s+([\s\S]+))?\s*-->$/ : /^\s*ko(?:\s+([\s\S]+))?\s*$/;
    var endCommentRegex =   commentNodesHaveTextProperty ? /^<!--\s*\/ko\s*-->$/ : /^\s*\/ko\s*$/;
    var htmlTagsWithOptionallyClosingChildren = { 'ul': true, 'ol': true };

    function isStartComment(node) {
        return (node.nodeType == 8) && startCommentRegex.test(commentNodesHaveTextProperty ? node.text : node.nodeValue);
    }

    function isEndComment(node) {
        return (node.nodeType == 8) && endCommentRegex.test(commentNodesHaveTextProperty ? node.text : node.nodeValue);
    }

    function getVirtualChildren(startComment, allowUnbalanced) {
        var currentNode = startComment;
        var depth = 1;
        var children = [];
        while (currentNode = currentNode.nextSibling) {
            if (isEndComment(currentNode)) {
                depth--;
                if (depth === 0)
                    return children;
            }

            children.push(currentNode);

            if (isStartComment(currentNode))
                depth++;
        }
        if (!allowUnbalanced)
            throw new Error("Cannot find closing comment tag to match: " + startComment.nodeValue);
        return null;
    }

    function getMatchingEndComment(startComment, allowUnbalanced) {
        var allVirtualChildren = getVirtualChildren(startComment, allowUnbalanced);
        if (allVirtualChildren) {
            if (allVirtualChildren.length > 0)
                return allVirtualChildren[allVirtualChildren.length - 1].nextSibling;
            return startComment.nextSibling;
        } else
            return null; // Must have no matching end comment, and allowUnbalanced is true
    }

    function getUnbalancedChildTags(node) {
        // e.g., from <div>OK</div><!-- ko blah --><span>Another</span>, returns: <!-- ko blah --><span>Another</span>
        //       from <div>OK</div><!-- /ko --><!-- /ko -->,             returns: <!-- /ko --><!-- /ko -->
        var childNode = node.firstChild, captureRemaining = null;
        if (childNode) {
            do {
                if (captureRemaining)                   // We already hit an unbalanced node and are now just scooping up all subsequent nodes
                    captureRemaining.push(childNode);
                else if (isStartComment(childNode)) {
                    var matchingEndComment = getMatchingEndComment(childNode, /* allowUnbalanced: */ true);
                    if (matchingEndComment)             // It's a balanced tag, so skip immediately to the end of this virtual set
                        childNode = matchingEndComment;
                    else
                        captureRemaining = [childNode]; // It's unbalanced, so start capturing from this point
                } else if (isEndComment(childNode)) {
                    captureRemaining = [childNode];     // It's unbalanced (if it wasn't, we'd have skipped over it already), so start capturing
                }
            } while (childNode = childNode.nextSibling);
        }
        return captureRemaining;
    }

    ko.virtualElements = {
        allowedBindings: {},

        childNodes: function(node) {
            return isStartComment(node) ? getVirtualChildren(node) : node.childNodes;
        },

        emptyNode: function(node) {
            if (!isStartComment(node))
                ko.utils.emptyDomNode(node);
            else {
                var virtualChildren = ko.virtualElements.childNodes(node);
                for (var i = 0, j = virtualChildren.length; i < j; i++)
                    ko.removeNode(virtualChildren[i]);
            }
        },

        setDomNodeChildren: function(node, childNodes) {
            if (!isStartComment(node))
                ko.utils.setDomNodeChildren(node, childNodes);
            else {
                ko.virtualElements.emptyNode(node);
                var endCommentNode = node.nextSibling; // Must be the next sibling, as we just emptied the children
                for (var i = 0, j = childNodes.length; i < j; i++)
                    endCommentNode.parentNode.insertBefore(childNodes[i], endCommentNode);
            }
        },

        prepend: function(containerNode, nodeToPrepend) {
            if (!isStartComment(containerNode)) {
                if (containerNode.firstChild)
                    containerNode.insertBefore(nodeToPrepend, containerNode.firstChild);
                else
                    containerNode.appendChild(nodeToPrepend);
            } else {
                // Start comments must always have a parent and at least one following sibling (the end comment)
                containerNode.parentNode.insertBefore(nodeToPrepend, containerNode.nextSibling);
            }
        },

        insertAfter: function(containerNode, nodeToInsert, insertAfterNode) {
            if (!insertAfterNode) {
                ko.virtualElements.prepend(containerNode, nodeToInsert);
            } else if (!isStartComment(containerNode)) {
                // Insert after insertion point
                if (insertAfterNode.nextSibling)
                    containerNode.insertBefore(nodeToInsert, insertAfterNode.nextSibling);
                else
                    containerNode.appendChild(nodeToInsert);
            } else {
                // Children of start comments must always have a parent and at least one following sibling (the end comment)
                containerNode.parentNode.insertBefore(nodeToInsert, insertAfterNode.nextSibling);
            }
        },

        firstChild: function(node) {
            if (!isStartComment(node))
                return node.firstChild;
            if (!node.nextSibling || isEndComment(node.nextSibling))
                return null;
            return node.nextSibling;
        },

        nextSibling: function(node) {
            if (isStartComment(node))
                node = getMatchingEndComment(node);
            if (node.nextSibling && isEndComment(node.nextSibling))
                return null;
            return node.nextSibling;
        },

        hasBindingValue: isStartComment,

        virtualNodeBindingValue: function(node) {
            var regexMatch = (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(startCommentRegex);
            return regexMatch ? regexMatch[1] : null;
        },

        normaliseVirtualElementDomStructure: function(elementVerified) {
            // Workaround for https://github.com/SteveSanderson/knockout/issues/155
            // (IE <= 8 or IE 9 quirks mode parses your HTML weirdly, treating closing </li> tags as if they don't exist, thereby moving comment nodes
            // that are direct descendants of <ul> into the preceding <li>)
            if (!htmlTagsWithOptionallyClosingChildren[ko.utils.tagNameLower(elementVerified)])
                return;

            // Scan immediate children to see if they contain unbalanced comment tags. If they do, those comment tags
            // must be intended to appear *after* that child, so move them there.
            var childNode = elementVerified.firstChild;
            if (childNode) {
                do {
                    if (childNode.nodeType === 1) {
                        var unbalancedTags = getUnbalancedChildTags(childNode);
                        if (unbalancedTags) {
                            // Fix up the DOM by moving the unbalanced tags to where they most likely were intended to be placed - *after* the child
                            var nodeToInsertBefore = childNode.nextSibling;
                            for (var i = 0; i < unbalancedTags.length; i++) {
                                if (nodeToInsertBefore)
                                    elementVerified.insertBefore(unbalancedTags[i], nodeToInsertBefore);
                                else
                                    elementVerified.appendChild(unbalancedTags[i]);
                            }
                        }
                    }
                } while (childNode = childNode.nextSibling);
            }
        }
    };
})();
ko.exportSymbol('virtualElements', ko.virtualElements);
ko.exportSymbol('virtualElements.allowedBindings', ko.virtualElements.allowedBindings);
ko.exportSymbol('virtualElements.emptyNode', ko.virtualElements.emptyNode);
//ko.exportSymbol('virtualElements.firstChild', ko.virtualElements.firstChild);     // firstChild is not minified
ko.exportSymbol('virtualElements.insertAfter', ko.virtualElements.insertAfter);
//ko.exportSymbol('virtualElements.nextSibling', ko.virtualElements.nextSibling);   // nextSibling is not minified
ko.exportSymbol('virtualElements.prepend', ko.virtualElements.prepend);
ko.exportSymbol('virtualElements.setDomNodeChildren', ko.virtualElements.setDomNodeChildren);
(function() {
    var defaultBindingAttributeName = "data-bind";

    ko.bindingProvider = function() {
        this.bindingCache = {};
    };

    ko.utils.extend(ko.bindingProvider.prototype, {
        'nodeHasBindings': function(node) {
            switch (node.nodeType) {
                case 1: // Element
                    return node.getAttribute(defaultBindingAttributeName) != null
                        || ko.components['getComponentNameForNode'](node);
                case 8: // Comment node
                    return ko.virtualElements.hasBindingValue(node);
                default: return false;
            }
        },

        'getBindings': function(node, bindingContext) {
            var bindingsString = this['getBindingsString'](node, bindingContext),
                parsedBindings = bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node) : null;
            return ko.components.addBindingsForCustomElement(parsedBindings, node, bindingContext, /* valueAccessors */ false);
        },

        'getBindingAccessors': function(node, bindingContext) {
            var bindingsString = this['getBindingsString'](node, bindingContext),
                parsedBindings = bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node, { 'valueAccessors': true }) : null;
            return ko.components.addBindingsForCustomElement(parsedBindings, node, bindingContext, /* valueAccessors */ true);
        },

        // The following function is only used internally by this default provider.
        // It's not part of the interface definition for a general binding provider.
        'getBindingsString': function(node, bindingContext) {
            switch (node.nodeType) {
                case 1: return node.getAttribute(defaultBindingAttributeName);   // Element
                case 8: return ko.virtualElements.virtualNodeBindingValue(node); // Comment node
                default: return null;
            }
        },

        // The following function is only used internally by this default provider.
        // It's not part of the interface definition for a general binding provider.
        'parseBindingsString': function(bindingsString, bindingContext, node, options) {
            try {
                var bindingFunction = createBindingsStringEvaluatorViaCache(bindingsString, this.bindingCache, options);
                return bindingFunction(bindingContext, node);
            } catch (ex) {
                ex.message = "Unable to parse bindings.\nBindings value: " + bindingsString + "\nMessage: " + ex.message;
                throw ex;
            }
        }
    });

    ko.bindingProvider['instance'] = new ko.bindingProvider();

    function createBindingsStringEvaluatorViaCache(bindingsString, cache, options) {
        var cacheKey = bindingsString + (options && options['valueAccessors'] || '');
        return cache[cacheKey]
            || (cache[cacheKey] = createBindingsStringEvaluator(bindingsString, options));
    }

    function createBindingsStringEvaluator(bindingsString, options) {
        // Build the source for a function that evaluates "expression"
        // For each scope variable, add an extra level of "with" nesting
        // Example result: with(sc1) { with(sc0) { return (expression) } }
        var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString, options),
            functionBody = "with($context){with($data||{}){return{" + rewrittenBindings + "}}}";
        return new Function("$context", "$element", functionBody);
    }
})();

ko.exportSymbol('bindingProvider', ko.bindingProvider);
(function () {
    ko.bindingHandlers = {};

    // The following element types will not be recursed into during binding. In the future, we
    // may consider adding <template> to this list, because such elements' contents are always
    // intended to be bound in a different context from where they appear in the document.
    var bindingDoesNotRecurseIntoElementTypes = {
        // Don't want bindings that operate on text nodes to mutate <script> contents,
        // because it's unexpected and a potential XSS issue
        'script': true
    };

    // Use an overridable method for retrieving binding handlers so that a plugins may support dynamically created handlers
    ko['getBindingHandler'] = function(bindingKey) {
        return ko.bindingHandlers[bindingKey];
    };

    // The ko.bindingContext constructor is only called directly to create the root context. For child
    // contexts, use bindingContext.createChildContext or bindingContext.extend.
    ko.bindingContext = function(dataItemOrAccessor, parentContext, dataItemAlias, extendCallback) {

        // The binding context object includes static properties for the current, parent, and root view models.
        // If a view model is actually stored in an observable, the corresponding binding context object, and
        // any child contexts, must be updated when the view model is changed.
        function updateContext() {
            // Most of the time, the context will directly get a view model object, but if a function is given,
            // we call the function to retrieve the view model. If the function accesses any obsevables or returns
            // an observable, the dependency is tracked, and those observables can later cause the binding
            // context to be updated.
            var dataItemOrObservable = isFunc ? dataItemOrAccessor() : dataItemOrAccessor,
                dataItem = ko.utils.unwrapObservable(dataItemOrObservable);

            if (parentContext) {
                // When a "parent" context is given, register a dependency on the parent context. Thus whenever the
                // parent context is updated, this context will also be updated.
                if (parentContext._subscribable)
                    parentContext._subscribable();

                // Copy $root and any custom properties from the parent context
                ko.utils.extend(self, parentContext);

                // Because the above copy overwrites our own properties, we need to reset them.
                // During the first execution, "subscribable" isn't set, so don't bother doing the update then.
                if (subscribable) {
                    self._subscribable = subscribable;
                }
            } else {
                self['$parents'] = [];
                self['$root'] = dataItem;

                // Export 'ko' in the binding context so it will be available in bindings and templates
                // even if 'ko' isn't exported as a global, such as when using an AMD loader.
                // See https://github.com/SteveSanderson/knockout/issues/490
                self['ko'] = ko;
            }
            self['$rawData'] = dataItemOrObservable;
            self['$data'] = dataItem;
            if (dataItemAlias)
                self[dataItemAlias] = dataItem;

            // The extendCallback function is provided when creating a child context or extending a context.
            // It handles the specific actions needed to finish setting up the binding context. Actions in this
            // function could also add dependencies to this binding context.
            if (extendCallback)
                extendCallback(self, parentContext, dataItem);

            return self['$data'];
        }
        function disposeWhen() {
            return nodes && !ko.utils.anyDomNodeIsAttachedToDocument(nodes);
        }

        var self = this,
            isFunc = typeof(dataItemOrAccessor) == "function" && !ko.isObservable(dataItemOrAccessor),
            nodes,
            subscribable = ko.dependentObservable(updateContext, null, { disposeWhen: disposeWhen, disposeWhenNodeIsRemoved: true });

        // At this point, the binding context has been initialized, and the "subscribable" computed observable is
        // subscribed to any observables that were accessed in the process. If there is nothing to track, the
        // computed will be inactive, and we can safely throw it away. If it's active, the computed is stored in
        // the context object.
        if (subscribable.isActive()) {
            self._subscribable = subscribable;

            // Always notify because even if the model ($data) hasn't changed, other context properties might have changed
            subscribable['equalityComparer'] = null;

            // We need to be able to dispose of this computed observable when it's no longer needed. This would be
            // easy if we had a single node to watch, but binding contexts can be used by many different nodes, and
            // we cannot assume that those nodes have any relation to each other. So instead we track any node that
            // the context is attached to, and dispose the computed when all of those nodes have been cleaned.

            // Add properties to *subscribable* instead of *self* because any properties added to *self* may be overwritten on updates
            nodes = [];
            subscribable._addNode = function(node) {
                nodes.push(node);
                ko.utils.domNodeDisposal.addDisposeCallback(node, function(node) {
                    ko.utils.arrayRemoveItem(nodes, node);
                    if (!nodes.length) {
                        subscribable.dispose();
                        self._subscribable = subscribable = undefined;
                    }
                });
            };
        }
    }

    // Extend the binding context hierarchy with a new view model object. If the parent context is watching
    // any obsevables, the new child context will automatically get a dependency on the parent context.
    // But this does not mean that the $data value of the child context will also get updated. If the child
    // view model also depends on the parent view model, you must provide a function that returns the correct
    // view model on each update.
    ko.bindingContext.prototype['createChildContext'] = function (dataItemOrAccessor, dataItemAlias, extendCallback) {
        return new ko.bindingContext(dataItemOrAccessor, this, dataItemAlias, function(self, parentContext) {
            // Extend the context hierarchy by setting the appropriate pointers
            self['$parentContext'] = parentContext;
            self['$parent'] = parentContext['$data'];
            self['$parents'] = (parentContext['$parents'] || []).slice(0);
            self['$parents'].unshift(self['$parent']);
            if (extendCallback)
                extendCallback(self);
        });
    };

    // Extend the binding context with new custom properties. This doesn't change the context hierarchy.
    // Similarly to "child" contexts, provide a function here to make sure that the correct values are set
    // when an observable view model is updated.
    ko.bindingContext.prototype['extend'] = function(properties) {
        // If the parent context references an observable view model, "_subscribable" will always be the
        // latest view model object. If not, "_subscribable" isn't set, and we can use the static "$data" value.
        return new ko.bindingContext(this._subscribable || this['$data'], this, null, function(self, parentContext) {
            // This "child" context doesn't directly track a parent observable view model,
            // so we need to manually set the $rawData value to match the parent.
            self['$rawData'] = parentContext['$rawData'];
            ko.utils.extend(self, typeof(properties) == "function" ? properties() : properties);
        });
    };

    // Returns the valueAccesor function for a binding value
    function makeValueAccessor(value) {
        return function() {
            return value;
        };
    }

    // Returns the value of a valueAccessor function
    function evaluateValueAccessor(valueAccessor) {
        return valueAccessor();
    }

    // Given a function that returns bindings, create and return a new object that contains
    // binding value-accessors functions. Each accessor function calls the original function
    // so that it always gets the latest value and all dependencies are captured. This is used
    // by ko.applyBindingsToNode and getBindingsAndMakeAccessors.
    function makeAccessorsFromFunction(callback) {
        return ko.utils.objectMap(ko.dependencyDetection.ignore(callback), function(value, key) {
            return function() {
                return callback()[key];
            };
        });
    }

    // Given a bindings function or object, create and return a new object that contains
    // binding value-accessors functions. This is used by ko.applyBindingsToNode.
    function makeBindingAccessors(bindings, context, node) {
        if (typeof bindings === 'function') {
            return makeAccessorsFromFunction(bindings.bind(null, context, node));
        } else {
            return ko.utils.objectMap(bindings, makeValueAccessor);
        }
    }

    // This function is used if the binding provider doesn't include a getBindingAccessors function.
    // It must be called with 'this' set to the provider instance.
    function getBindingsAndMakeAccessors(node, context) {
        return makeAccessorsFromFunction(this['getBindings'].bind(this, node, context));
    }

    function validateThatBindingIsAllowedForVirtualElements(bindingName) {
        var validator = ko.virtualElements.allowedBindings[bindingName];
        if (!validator)
            throw new Error("The binding '" + bindingName + "' cannot be used with virtual elements")
    }

    function applyBindingsToDescendantsInternal (bindingContext, elementOrVirtualElement, bindingContextsMayDifferFromDomParentElement) {
        var currentChild,
            nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement),
            provider = ko.bindingProvider['instance'],
            preprocessNode = provider['preprocessNode'];

        // Preprocessing allows a binding provider to mutate a node before bindings are applied to it. For example it's
        // possible to insert new siblings after it, and/or replace the node with a different one. This can be used to
        // implement custom binding syntaxes, such as {{ value }} for string interpolation, or custom element types that
        // trigger insertion of <template> contents at that point in the document.
        if (preprocessNode) {
            while (currentChild = nextInQueue) {
                nextInQueue = ko.virtualElements.nextSibling(currentChild);
                preprocessNode.call(provider, currentChild);
            }
            // Reset nextInQueue for the next loop
            nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement);
        }

        while (currentChild = nextInQueue) {
            // Keep a record of the next child *before* applying bindings, in case the binding removes the current child from its position
            nextInQueue = ko.virtualElements.nextSibling(currentChild);
            applyBindingsToNodeAndDescendantsInternal(bindingContext, currentChild, bindingContextsMayDifferFromDomParentElement);
        }
    }

    function applyBindingsToNodeAndDescendantsInternal (bindingContext, nodeVerified, bindingContextMayDifferFromDomParentElement) {
        var shouldBindDescendants = true;

        // Perf optimisation: Apply bindings only if...
        // (1) We need to store the binding context on this node (because it may differ from the DOM parent node's binding context)
        //     Note that we can't store binding contexts on non-elements (e.g., text nodes), as IE doesn't allow expando properties for those
        // (2) It might have bindings (e.g., it has a data-bind attribute, or it's a marker for a containerless template)
        var isElement = (nodeVerified.nodeType === 1);
        if (isElement) // Workaround IE <= 8 HTML parsing weirdness
            ko.virtualElements.normaliseVirtualElementDomStructure(nodeVerified);

        var shouldApplyBindings = (isElement && bindingContextMayDifferFromDomParentElement)             // Case (1)
                               || ko.bindingProvider['instance']['nodeHasBindings'](nodeVerified);       // Case (2)
        if (shouldApplyBindings)
            shouldBindDescendants = applyBindingsToNodeInternal(nodeVerified, null, bindingContext, bindingContextMayDifferFromDomParentElement)['shouldBindDescendants'];

        if (shouldBindDescendants && !bindingDoesNotRecurseIntoElementTypes[ko.utils.tagNameLower(nodeVerified)]) {
            // We're recursing automatically into (real or virtual) child nodes without changing binding contexts. So,
            //  * For children of a *real* element, the binding context is certainly the same as on their DOM .parentNode,
            //    hence bindingContextsMayDifferFromDomParentElement is false
            //  * For children of a *virtual* element, we can't be sure. Evaluating .parentNode on those children may
            //    skip over any number of intermediate virtual elements, any of which might define a custom binding context,
            //    hence bindingContextsMayDifferFromDomParentElement is true
            applyBindingsToDescendantsInternal(bindingContext, nodeVerified, /* bindingContextsMayDifferFromDomParentElement: */ !isElement);
        }
    }

    var boundElementDomDataKey = ko.utils.domData.nextKey();


    function topologicalSortBindings(bindings) {
        // Depth-first sort
        var result = [],                // The list of key/handler pairs that we will return
            bindingsConsidered = {},    // A temporary record of which bindings are already in 'result'
            cyclicDependencyStack = []; // Keeps track of a depth-search so that, if there's a cycle, we know which bindings caused it
        ko.utils.objectForEach(bindings, function pushBinding(bindingKey) {
            if (!bindingsConsidered[bindingKey]) {
                var binding = ko['getBindingHandler'](bindingKey);
                if (binding) {
                    // First add dependencies (if any) of the current binding
                    if (binding['after']) {
                        cyclicDependencyStack.push(bindingKey);
                        ko.utils.arrayForEach(binding['after'], function(bindingDependencyKey) {
                            if (bindings[bindingDependencyKey]) {
                                if (ko.utils.arrayIndexOf(cyclicDependencyStack, bindingDependencyKey) !== -1) {
                                    throw Error("Cannot combine the following bindings, because they have a cyclic dependency: " + cyclicDependencyStack.join(", "));
                                } else {
                                    pushBinding(bindingDependencyKey);
                                }
                            }
                        });
                        cyclicDependencyStack.length--;
                    }
                    // Next add the current binding
                    result.push({ key: bindingKey, handler: binding });
                }
                bindingsConsidered[bindingKey] = true;
            }
        });

        return result;
    }

    function applyBindingsToNodeInternal(node, sourceBindings, bindingContext, bindingContextMayDifferFromDomParentElement) {
        // Prevent multiple applyBindings calls for the same node, except when a binding value is specified
        var alreadyBound = ko.utils.domData.get(node, boundElementDomDataKey);
        if (!sourceBindings) {
            if (alreadyBound) {
                throw Error("You cannot apply bindings multiple times to the same element.");
            }
            ko.utils.domData.set(node, boundElementDomDataKey, true);
        }

        // Optimization: Don't store the binding context on this node if it's definitely the same as on node.parentNode, because
        // we can easily recover it just by scanning up the node's ancestors in the DOM
        // (note: here, parent node means "real DOM parent" not "virtual parent", as there's no O(1) way to find the virtual parent)
        if (!alreadyBound && bindingContextMayDifferFromDomParentElement)
            ko.storedBindingContextForNode(node, bindingContext);

        // Use bindings if given, otherwise fall back on asking the bindings provider to give us some bindings
        var bindings;
        if (sourceBindings && typeof sourceBindings !== 'function') {
            bindings = sourceBindings;
        } else {
            var provider = ko.bindingProvider['instance'],
                getBindings = provider['getBindingAccessors'] || getBindingsAndMakeAccessors;

            // Get the binding from the provider within a computed observable so that we can update the bindings whenever
            // the binding context is updated or if the binding provider accesses observables.
            var bindingsUpdater = ko.dependentObservable(
                function() {
                    bindings = sourceBindings ? sourceBindings(bindingContext, node) : getBindings.call(provider, node, bindingContext);
                    // Register a dependency on the binding context to support obsevable view models.
                    if (bindings && bindingContext._subscribable)
                        bindingContext._subscribable();
                    return bindings;
                },
                null, { disposeWhenNodeIsRemoved: node }
            );

            if (!bindings || !bindingsUpdater.isActive())
                bindingsUpdater = null;
        }

        var bindingHandlerThatControlsDescendantBindings;
        if (bindings) {
            // Return the value accessor for a given binding. When bindings are static (won't be updated because of a binding
            // context update), just return the value accessor from the binding. Otherwise, return a function that always gets
            // the latest binding value and registers a dependency on the binding updater.
            var getValueAccessor = bindingsUpdater
                ? function(bindingKey) {
                    return function() {
                        return evaluateValueAccessor(bindingsUpdater()[bindingKey]);
                    };
                } : function(bindingKey) {
                    return bindings[bindingKey];
                };

            // Use of allBindings as a function is maintained for backwards compatibility, but its use is deprecated
            function allBindings() {
                return ko.utils.objectMap(bindingsUpdater ? bindingsUpdater() : bindings, evaluateValueAccessor);
            }
            // The following is the 3.x allBindings API
            allBindings['get'] = function(key) {
                return bindings[key] && evaluateValueAccessor(getValueAccessor(key));
            };
            allBindings['has'] = function(key) {
                return key in bindings;
            };

            // First put the bindings into the right order
            var orderedBindings = topologicalSortBindings(bindings);

            // Go through the sorted bindings, calling init and update for each
            ko.utils.arrayForEach(orderedBindings, function(bindingKeyAndHandler) {
                // Note that topologicalSortBindings has already filtered out any nonexistent binding handlers,
                // so bindingKeyAndHandler.handler will always be nonnull.
                var handlerInitFn = bindingKeyAndHandler.handler["init"],
                    handlerUpdateFn = bindingKeyAndHandler.handler["update"],
                    bindingKey = bindingKeyAndHandler.key;

                if (node.nodeType === 8) {
                    validateThatBindingIsAllowedForVirtualElements(bindingKey);
                }

                try {
                    // Run init, ignoring any dependencies
                    if (typeof handlerInitFn == "function") {
                        ko.dependencyDetection.ignore(function() {
                            var initResult = handlerInitFn(node, getValueAccessor(bindingKey), allBindings, bindingContext['$data'], bindingContext);

                            // If this binding handler claims to control descendant bindings, make a note of this
                            if (initResult && initResult['controlsDescendantBindings']) {
                                if (bindingHandlerThatControlsDescendantBindings !== undefined)
                                    throw new Error("Multiple bindings (" + bindingHandlerThatControlsDescendantBindings + " and " + bindingKey + ") are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.");
                                bindingHandlerThatControlsDescendantBindings = bindingKey;
                            }
                        });
                    }

                    // Run update in its own computed wrapper
                    if (typeof handlerUpdateFn == "function") {
                        ko.dependentObservable(
                            function() {
                                handlerUpdateFn(node, getValueAccessor(bindingKey), allBindings, bindingContext['$data'], bindingContext);
                            },
                            null,
                            { disposeWhenNodeIsRemoved: node }
                        );
                    }
                } catch (ex) {
                    ex.message = "Unable to process binding \"" + bindingKey + ": " + bindings[bindingKey] + "\"\nMessage: " + ex.message;
                    throw ex;
                }
            });
        }

        return {
            'shouldBindDescendants': bindingHandlerThatControlsDescendantBindings === undefined
        };
    };

    var storedBindingContextDomDataKey = ko.utils.domData.nextKey();
    ko.storedBindingContextForNode = function (node, bindingContext) {
        if (arguments.length == 2) {
            ko.utils.domData.set(node, storedBindingContextDomDataKey, bindingContext);
            if (bindingContext._subscribable)
                bindingContext._subscribable._addNode(node);
        } else {
            return ko.utils.domData.get(node, storedBindingContextDomDataKey);
        }
    }

    function getBindingContext(viewModelOrBindingContext) {
        return viewModelOrBindingContext && (viewModelOrBindingContext instanceof ko.bindingContext)
            ? viewModelOrBindingContext
            : new ko.bindingContext(viewModelOrBindingContext);
    }

    ko.applyBindingAccessorsToNode = function (node, bindings, viewModelOrBindingContext) {
        if (node.nodeType === 1) // If it's an element, workaround IE <= 8 HTML parsing weirdness
            ko.virtualElements.normaliseVirtualElementDomStructure(node);
        return applyBindingsToNodeInternal(node, bindings, getBindingContext(viewModelOrBindingContext), true);
    };

    ko.applyBindingsToNode = function (node, bindings, viewModelOrBindingContext) {
        var context = getBindingContext(viewModelOrBindingContext);
        return ko.applyBindingAccessorsToNode(node, makeBindingAccessors(bindings, context, node), context);
    };

    ko.applyBindingsToDescendants = function(viewModelOrBindingContext, rootNode) {
        if (rootNode.nodeType === 1 || rootNode.nodeType === 8)
            applyBindingsToDescendantsInternal(getBindingContext(viewModelOrBindingContext), rootNode, true);
    };

    ko.applyBindings = function (viewModelOrBindingContext, rootNode) {
        // If jQuery is loaded after Knockout, we won't initially have access to it. So save it here.
        if (!jQueryInstance && window['jQuery']) {
            jQueryInstance = window['jQuery'];
        }

        if (rootNode && (rootNode.nodeType !== 1) && (rootNode.nodeType !== 8))
            throw new Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node");
        rootNode = rootNode || window.document.body; // Make "rootNode" parameter optional

        applyBindingsToNodeAndDescendantsInternal(getBindingContext(viewModelOrBindingContext), rootNode, true);
    };

    // Retrieving binding context from arbitrary nodes
    ko.contextFor = function(node) {
        // We can only do something meaningful for elements and comment nodes (in particular, not text nodes, as IE can't store domdata for them)
        switch (node.nodeType) {
            case 1:
            case 8:
                var context = ko.storedBindingContextForNode(node);
                if (context) return context;
                if (node.parentNode) return ko.contextFor(node.parentNode);
                break;
        }
        return undefined;
    };
    ko.dataFor = function(node) {
        var context = ko.contextFor(node);
        return context ? context['$data'] : undefined;
    };

    ko.exportSymbol('bindingHandlers', ko.bindingHandlers);
    ko.exportSymbol('applyBindings', ko.applyBindings);
    ko.exportSymbol('applyBindingsToDescendants', ko.applyBindingsToDescendants);
    ko.exportSymbol('applyBindingAccessorsToNode', ko.applyBindingAccessorsToNode);
    ko.exportSymbol('applyBindingsToNode', ko.applyBindingsToNode);
    ko.exportSymbol('contextFor', ko.contextFor);
    ko.exportSymbol('dataFor', ko.dataFor);
})();
(function(undefined) {
    var loadingSubscribablesCache = {}, // Tracks component loads that are currently in flight
        loadedDefinitionsCache = {};    // Tracks component loads that have already completed

    ko.components = {
        get: function(componentName, callback) {
            var cachedDefinition = getObjectOwnProperty(loadedDefinitionsCache, componentName);
            if (cachedDefinition) {
                // It's already loaded and cached. Reuse the same definition object.
                // Note that for API consistency, even cache hits complete asynchronously.
                setTimeout(function() { callback(cachedDefinition) }, 0);
            } else {
                // Join the loading process that is already underway, or start a new one.
                loadComponentAndNotify(componentName, callback);
            }
        },

        clearCachedDefinition: function(componentName) {
            delete loadedDefinitionsCache[componentName];
        },

        _getFirstResultFromLoaders: getFirstResultFromLoaders
    };

    function getObjectOwnProperty(obj, propName) {
        return obj.hasOwnProperty(propName) ? obj[propName] : undefined;
    }

    function loadComponentAndNotify(componentName, callback) {
        var subscribable = getObjectOwnProperty(loadingSubscribablesCache, componentName),
            completedAsync;
        if (!subscribable) {
            // It's not started loading yet. Start loading, and when it's done, move it to loadedDefinitionsCache.
            subscribable = loadingSubscribablesCache[componentName] = new ko.subscribable();
            beginLoadingComponent(componentName, function(definition) {
                loadedDefinitionsCache[componentName] = definition;
                delete loadingSubscribablesCache[componentName];

                // For API consistency, all loads complete asynchronously. However we want to avoid
                // adding an extra setTimeout if it's unnecessary (i.e., the completion is already
                // async) since setTimeout(..., 0) still takes about 16ms or more on most browsers.
                if (completedAsync) {
                    subscribable['notifySubscribers'](definition);
                } else {
                    setTimeout(function() {
                        subscribable['notifySubscribers'](definition);
                    }, 0);
                }
            });
            completedAsync = true;
        }
        subscribable.subscribe(callback);
    }

    function beginLoadingComponent(componentName, callback) {
        getFirstResultFromLoaders('getConfig', [componentName], function(config) {
            if (config) {
                // We have a config, so now load its definition
                getFirstResultFromLoaders('loadComponent', [componentName, config], function(definition) {
                    callback(definition);
                });
            } else {
                // The component has no config - it's unknown to all the loaders.
                // Note that this is not an error (e.g., a module loading error) - that would abort the
                // process and this callback would not run. For this callback to run, all loaders must
                // have confirmed they don't know about this component.
                callback(null);
            }
        });
    }

    function getFirstResultFromLoaders(methodName, argsExceptCallback, callback, candidateLoaders) {
        // On the first call in the stack, start with the full set of loaders
        if (!candidateLoaders) {
            candidateLoaders = ko.components['loaders'].slice(0); // Use a copy, because we'll be mutating this array
        }

        // Try the next candidate
        var currentCandidateLoader = candidateLoaders.shift();
        if (currentCandidateLoader) {
            var methodInstance = currentCandidateLoader[methodName];
            if (methodInstance) {
                var wasAborted = false,
                    synchronousReturnValue = methodInstance.apply(currentCandidateLoader, argsExceptCallback.concat(function(result) {
                        if (wasAborted) {
                            callback(null);
                        } else if (result !== null) {
                            // This candidate returned a value. Use it.
                            callback(result);
                        } else {
                            // Try the next candidate
                            getFirstResultFromLoaders(methodName, argsExceptCallback, callback, candidateLoaders);
                        }
                    }));

                // Currently, loaders may not return anything synchronously. This leaves open the possibility
                // that we'll extend the API to support synchronous return values in the future. It won't be
                // a breaking change, because currently no loader is allowed to return anything except undefined.
                if (synchronousReturnValue !== undefined) {
                    wasAborted = true;

                    // Method to suppress exceptions will remain undocumented. This is only to keep
                    // KO's specs running tidily, since we can observe the loading got aborted without
                    // having exceptions cluttering up the console too.
                    if (!currentCandidateLoader['suppressLoaderExceptions']) {
                        throw new Error('Component loaders must supply values by invoking the callback, not by returning values synchronously.');
                    }
                }
            } else {
                // This candidate doesn't have the relevant handler. Synchronously move on to the next one.
                getFirstResultFromLoaders(methodName, argsExceptCallback, callback, candidateLoaders);
            }
        } else {
            // No candidates returned a value
            callback(null);
        }
    }

    // Reference the loaders via string name so it's possible for developers
    // to replace the whole array by assigning to ko.components.loaders
    ko.components['loaders'] = [];

    ko.exportSymbol('components', ko.components);
    ko.exportSymbol('components.get', ko.components.get);
    ko.exportSymbol('components.clearCachedDefinition', ko.components.clearCachedDefinition);
})();
(function(undefined) {

    // The default loader is responsible for two things:
    // 1. Maintaining the default in-memory registry of component configuration objects
    //    (i.e., the thing you're writing to when you call ko.components.register(someName, ...))
    // 2. Answering requests for components by fetching configuration objects
    //    from that default in-memory registry and resolving them into standard
    //    component definition objects (of the form { createViewModel: ..., template: ... })
    // Custom loaders may override either of these facilities, i.e.,
    // 1. To supply configuration objects from some other source (e.g., conventions)
    // 2. Or, to resolve configuration objects by loading viewmodels/templates via arbitrary logic.

    var defaultConfigRegistry = {};

    ko.components.register = function(componentName, config) {
        if (!config) {
            throw new Error('Invalid configuration for ' + componentName);
        }

        if (ko.components.isRegistered(componentName)) {
            throw new Error('Component ' + componentName + ' is already registered');
        }

        defaultConfigRegistry[componentName] = config;
    }

    ko.components.isRegistered = function(componentName) {
        return componentName in defaultConfigRegistry;
    }

    ko.components.unregister = function(componentName) {
        delete defaultConfigRegistry[componentName];
        ko.components.clearCachedDefinition(componentName);
    }

    ko.components.defaultLoader = {
        'getConfig': function(componentName, callback) {
            var result = defaultConfigRegistry.hasOwnProperty(componentName)
                ? defaultConfigRegistry[componentName]
                : null;
            callback(result);
        },

        'loadComponent': function(componentName, config, callback) {
            var errorCallback = makeErrorCallback(componentName);
            possiblyGetConfigFromAmd(errorCallback, config, function(loadedConfig) {
                resolveConfig(componentName, errorCallback, loadedConfig, callback);
            });
        },

        'loadTemplate': function(componentName, templateConfig, callback) {
            resolveTemplate(makeErrorCallback(componentName), templateConfig, callback);
        },

        'loadViewModel': function(componentName, viewModelConfig, callback) {
            resolveViewModel(makeErrorCallback(componentName), viewModelConfig, callback);
        }
    };

    var createViewModelKey = 'createViewModel';

    // Takes a config object of the form { template: ..., viewModel: ... }, and asynchronously convert it
    // into the standard component definition format:
    //    { template: <ArrayOfDomNodes>, createViewModel: function(params, componentInfo) { ... } }.
    // Since both template and viewModel may need to be resolved asynchronously, both tasks are performed
    // in parallel, and the results joined when both are ready. We don't depend on any promises infrastructure,
    // so this is implemented manually below.
    function resolveConfig(componentName, errorCallback, config, callback) {
        var result = {},
            makeCallBackWhenZero = 2,
            tryIssueCallback = function() {
                if (--makeCallBackWhenZero === 0) {
                    callback(result);
                }
            },
            templateConfig = config['template'],
            viewModelConfig = config['viewModel'];

        if (templateConfig) {
            possiblyGetConfigFromAmd(errorCallback, templateConfig, function(loadedConfig) {
                ko.components._getFirstResultFromLoaders('loadTemplate', [componentName, loadedConfig], function(resolvedTemplate) {
                    result['template'] = resolvedTemplate;
                    tryIssueCallback();
                });
            });
        } else {
            tryIssueCallback();
        }

        if (viewModelConfig) {
            possiblyGetConfigFromAmd(errorCallback, viewModelConfig, function(loadedConfig) {
                ko.components._getFirstResultFromLoaders('loadViewModel', [componentName, loadedConfig], function(resolvedViewModel) {
                    result[createViewModelKey] = resolvedViewModel;
                    tryIssueCallback();
                });
            });
        } else {
            tryIssueCallback();
        }
    }

    function resolveTemplate(errorCallback, templateConfig, callback) {
        if (typeof templateConfig === 'string') {
            // Markup - parse it
            callback(ko.utils.parseHtmlFragment(templateConfig));
        } else if (templateConfig instanceof Array) {
            // Assume already an array of DOM nodes - pass through unchanged
            callback(templateConfig);
        } else if (isDocumentFragment(templateConfig)) {
            // Document fragment - use its child nodes
            callback(ko.utils.makeArray(templateConfig.childNodes));
        } else if (templateConfig['element']) {
            var element = templateConfig['element'];
            if (isDomElement(element)) {
                // Element instance - copy its child nodes
                callback(cloneNodesFromTemplateSourceElement(element));
            } else if (typeof element === 'string') {
                // Element ID - find it, then copy its child nodes
                var elemInstance = document.getElementById(element);
                if (elemInstance) {
                    callback(cloneNodesFromTemplateSourceElement(elemInstance));
                } else {
                    errorCallback('Cannot find element with ID ' + element);
                }
            } else {
                errorCallback('Unknown element type: ' + element);
            }
        } else {
            errorCallback('Unknown template value: ' + templateConfig);
        }
    }

    function resolveViewModel(errorCallback, viewModelConfig, callback) {
        if (typeof viewModelConfig === 'function') {
            // Constructor - convert to standard factory function format
            // By design, this does *not* supply componentInfo to the constructor, as the intent is that
            // componentInfo contains non-viewmodel data (e.g., the component's element) that should only
            // be used in factory functions, not viewmodel constructors.
            callback(function (params /*, componentInfo */) {
                return new viewModelConfig(params);
            });
        } else if (typeof viewModelConfig[createViewModelKey] === 'function') {
            // Already a factory function - use it as-is
            callback(viewModelConfig[createViewModelKey]);
        } else if ('instance' in viewModelConfig) {
            // Fixed object instance - promote to createViewModel format for API consistency
            var fixedInstance = viewModelConfig['instance'];
            callback(function (params, componentInfo) {
                return fixedInstance;
            });
        } else if ('viewModel' in viewModelConfig) {
            // Resolved AMD module whose value is of the form { viewModel: ... }
            resolveViewModel(errorCallback, viewModelConfig['viewModel'], callback);
        } else {
            errorCallback('Unknown viewModel value: ' + viewModelConfig);
        }
    }

    function cloneNodesFromTemplateSourceElement(elemInstance) {
        switch (ko.utils.tagNameLower(elemInstance)) {
            case 'script':
                return ko.utils.parseHtmlFragment(elemInstance.text);
            case 'textarea':
                return ko.utils.parseHtmlFragment(elemInstance.value);
            case 'template':
                // For browsers with proper <template> element support (i.e., where the .content property
                // gives a document fragment), use that document fragment.
                if (isDocumentFragment(elemInstance.content)) {
                    return ko.utils.cloneNodes(elemInstance.content.childNodes);
                }
        }

        // Regular elements such as <div>, and <template> elements on old browsers that don't really
        // understand <template> and just treat it as a regular container
        return ko.utils.cloneNodes(elemInstance.childNodes);
    }

    function isDomElement(obj) {
        if (window['HTMLElement']) {
            return obj instanceof HTMLElement;
        } else {
            return obj && obj.tagName && obj.nodeType === 1;
        }
    }

    function isDocumentFragment(obj) {
        if (window['DocumentFragment']) {
            return obj instanceof DocumentFragment;
        } else {
            return obj && obj.nodeType === 11;
        }
    }

    function possiblyGetConfigFromAmd(errorCallback, config, callback) {
        if (typeof config['require'] === 'string') {
            // The config is the value of an AMD module
            if (require || window['require']) {
                (require || window['require'])([config['require']], callback);
            } else {
                errorCallback('Uses require, but no AMD loader is present');
            }
        } else {
            callback(config);
        }
    }

    function makeErrorCallback(componentName) {
        return function (message) {
            throw new Error('Component \'' + componentName + '\': ' + message);
        };
    }

    ko.exportSymbol('components.register', ko.components.register);
    ko.exportSymbol('components.isRegistered', ko.components.isRegistered);
    ko.exportSymbol('components.unregister', ko.components.unregister);

    // Expose the default loader so that developers can directly ask it for configuration
    // or to resolve configuration
    ko.exportSymbol('components.defaultLoader', ko.components.defaultLoader);

    // By default, the default loader is the only registered component loader
    ko.components['loaders'].push(ko.components.defaultLoader);

    // Privately expose the underlying config registry for use in old-IE shim
    ko.components._allRegisteredComponents = defaultConfigRegistry;
})();
(function (undefined) {
    // Overridable API for determining which component name applies to a given node. By overriding this,
    // you can for example map specific tagNames to components that are not preregistered.
    ko.components['getComponentNameForNode'] = function(node) {
        var tagNameLower = ko.utils.tagNameLower(node);
        return ko.components.isRegistered(tagNameLower) && tagNameLower;
    };

    ko.components.addBindingsForCustomElement = function(allBindings, node, bindingContext, valueAccessors) {
        // Determine if it's really a custom element matching a component
        if (node.nodeType === 1) {
            var componentName = ko.components['getComponentNameForNode'](node);
            if (componentName) {
                // It does represent a component, so add a component binding for it
                allBindings = allBindings || {};

                if (allBindings['component']) {
                    // Avoid silently overwriting some other 'component' binding that may already be on the element
                    throw new Error('Cannot use the "component" binding on a custom element matching a component');
                }

                var componentBindingValue = { 'name': componentName, 'params': getComponentParamsFromCustomElement(node, bindingContext) };

                allBindings['component'] = valueAccessors
                    ? function() { return componentBindingValue; }
                    : componentBindingValue;
            }
        }

        return allBindings;
    }

    var nativeBindingProviderInstance = new ko.bindingProvider();

    function getComponentParamsFromCustomElement(elem, bindingContext) {
        var paramsAttribute = elem.getAttribute('params');

        if (paramsAttribute) {
            var params = nativeBindingProviderInstance['parseBindingsString'](paramsAttribute, bindingContext, elem, { 'valueAccessors': true, 'bindingParams': true }),
                rawParamComputedValues = ko.utils.objectMap(params, function(paramValue, paramName) {
                    return ko.computed(paramValue, null, { disposeWhenNodeIsRemoved: elem });
                }),
                result = ko.utils.objectMap(rawParamComputedValues, function(paramValueComputed, paramName) {
                    // Does the evaluation of the parameter value unwrap any observables?
                    if (!paramValueComputed.isActive()) {
                        // No it doesn't, so there's no need for any computed wrapper. Just pass through the supplied value directly.
                        // Example: "someVal: firstName, age: 123" (whether or not firstName is an observable/computed)
                        return paramValueComputed.peek();
                    } else {
                        // Yes it does. Supply a computed property that unwraps both the outer (binding expression)
                        // level of observability, and any inner (resulting model value) level of observability.
                        // This means the component doesn't have to worry about multiple unwrapping.
                        return ko.computed(function() {
                            return ko.utils.unwrapObservable(paramValueComputed());
                        }, null, { disposeWhenNodeIsRemoved: elem });
                    }
                });

            // Give access to the raw computeds, as long as that wouldn't overwrite any custom param also called '$raw'
            // This is in case the developer wants to react to outer (binding) observability separately from inner
            // (model value) observability, or in case the model value observable has subobservables.
            if (!result.hasOwnProperty('$raw')) {
                result['$raw'] = rawParamComputedValues;
            }

            return result;
        } else {
            // For consistency, absence of a "params" attribute is treated the same as the presence of
            // any empty one. Otherwise component viewmodels need special code to check whether or not
            // 'params' or 'params.$raw' is null/undefined before reading subproperties, which is annoying.
            return { '$raw': {} };
        }
    }

    // --------------------------------------------------------------------------------
    // Compatibility code for older (pre-HTML5) IE browsers

    if (ko.utils.ieVersion < 9) {
        // Whenever you preregister a component, enable it as a custom element in the current document
        ko.components['register'] = (function(originalFunction) {
            return function(componentName) {
                document.createElement(componentName); // Allows IE<9 to parse markup containing the custom element
                return originalFunction.apply(this, arguments);
            }
        })(ko.components['register']);

        // Whenever you create a document fragment, enable all preregistered component names as custom elements
        // This is needed to make innerShiv/jQuery HTML parsing correctly handle the custom elements
        document.createDocumentFragment = (function(originalFunction) {
            return function() {
                var newDocFrag = originalFunction(),
                    allComponents = ko.components._allRegisteredComponents;
                for (var componentName in allComponents) {
                    if (allComponents.hasOwnProperty(componentName)) {
                        newDocFrag.createElement(componentName);
                    }
                }
                return newDocFrag;
            };
        })(document.createDocumentFragment);
    }
})();(function(undefined) {

    var componentLoadingOperationUniqueId = 0;

    ko.bindingHandlers['component'] = {
        'init': function(element, valueAccessor, ignored1, ignored2, bindingContext) {
            var currentViewModel,
                currentLoadingOperationId,
                disposeAssociatedComponentViewModel = function () {
                    var currentViewModelDispose = currentViewModel && currentViewModel['dispose'];
                    if (typeof currentViewModelDispose === 'function') {
                        currentViewModelDispose.call(currentViewModel);
                    }

                    // Any in-flight loading operation is no longer relevant, so make sure we ignore its completion
                    currentLoadingOperationId = null;
                };

            ko.utils.domNodeDisposal.addDisposeCallback(element, disposeAssociatedComponentViewModel);

            ko.computed(function () {
                var value = ko.utils.unwrapObservable(valueAccessor()),
                    componentName, componentParams;

                if (typeof value === 'string') {
                    componentName = value;
                } else {
                    componentName = ko.utils.unwrapObservable(value['name']);
                    componentParams = ko.utils.unwrapObservable(value['params']);
                }

                if (!componentName) {
                    throw new Error('No component name specified');
                }

                var loadingOperationId = currentLoadingOperationId = ++componentLoadingOperationUniqueId;
                ko.components.get(componentName, function(componentDefinition) {
                    // If this is not the current load operation for this element, ignore it.
                    if (currentLoadingOperationId !== loadingOperationId) {
                        return;
                    }

                    // Clean up previous state
                    disposeAssociatedComponentViewModel();

                    // Instantiate and bind new component. Implicitly this cleans any old DOM nodes.
                    if (!componentDefinition) {
                        throw new Error('Unknown component \'' + componentName + '\'');
                    }
                    cloneTemplateIntoElement(componentName, componentDefinition, element);
                    var componentViewModel = createViewModel(componentDefinition, element, componentParams),
                        childBindingContext = bindingContext['createChildContext'](componentViewModel);
                    currentViewModel = componentViewModel;
                    ko.applyBindingsToDescendants(childBindingContext, element);
                });
            }, null, { disposeWhenNodeIsRemoved: element });

            return { 'controlsDescendantBindings': true };
        }
    };

    ko.virtualElements.allowedBindings['component'] = true;

    function cloneTemplateIntoElement(componentName, componentDefinition, element) {
        var template = componentDefinition['template'];
        if (!template) {
            throw new Error('Component \'' + componentName + '\' has no template');
        }

        var clonedNodesArray = ko.utils.cloneNodes(template);
        ko.virtualElements.setDomNodeChildren(element, clonedNodesArray);
    }

    function createViewModel(componentDefinition, element, componentParams) {
        var componentViewModelFactory = componentDefinition['createViewModel'];
        return componentViewModelFactory
            ? componentViewModelFactory.call(componentDefinition, componentParams, { element: element })
            : componentParams; // Template-only component
    }

})();
var attrHtmlToJavascriptMap = { 'class': 'className', 'for': 'htmlFor' };
ko.bindingHandlers['attr'] = {
    'update': function(element, valueAccessor, allBindings) {
        var value = ko.utils.unwrapObservable(valueAccessor()) || {};
        ko.utils.objectForEach(value, function(attrName, attrValue) {
            attrValue = ko.utils.unwrapObservable(attrValue);

            // To cover cases like "attr: { checked:someProp }", we want to remove the attribute entirely
            // when someProp is a "no value"-like value (strictly null, false, or undefined)
            // (because the absence of the "checked" attr is how to mark an element as not checked, etc.)
            var toRemove = (attrValue === false) || (attrValue === null) || (attrValue === undefined);
            if (toRemove)
                element.removeAttribute(attrName);

            // In IE <= 7 and IE8 Quirks Mode, you have to use the Javascript property name instead of the
            // HTML attribute name for certain attributes. IE8 Standards Mode supports the correct behavior,
            // but instead of figuring out the mode, we'll just set the attribute through the Javascript
            // property for IE <= 8.
            if (ko.utils.ieVersion <= 8 && attrName in attrHtmlToJavascriptMap) {
                attrName = attrHtmlToJavascriptMap[attrName];
                if (toRemove)
                    element.removeAttribute(attrName);
                else
                    element[attrName] = attrValue;
            } else if (!toRemove) {
                element.setAttribute(attrName, attrValue.toString());
            }

            // Treat "name" specially - although you can think of it as an attribute, it also needs
            // special handling on older versions of IE (https://github.com/SteveSanderson/knockout/pull/333)
            // Deliberately being case-sensitive here because XHTML would regard "Name" as a different thing
            // entirely, and there's no strong reason to allow for such casing in HTML.
            if (attrName === "name") {
                ko.utils.setElementName(element, toRemove ? "" : attrValue.toString());
            }
        });
    }
};
(function() {

ko.bindingHandlers['checked'] = {
    'after': ['value', 'attr'],
    'init': function (element, valueAccessor, allBindings) {
        var checkedValue = ko.pureComputed(function() {
            // Treat "value" like "checkedValue" when it is included with "checked" binding
            if (allBindings['has']('checkedValue')) {
                return ko.utils.unwrapObservable(allBindings.get('checkedValue'));
            } else if (allBindings['has']('value')) {
                return ko.utils.unwrapObservable(allBindings.get('value'));
            }

            return element.value;
        });

        function updateModel() {
            // This updates the model value from the view value.
            // It runs in response to DOM events (click) and changes in checkedValue.
            var isChecked = element.checked,
                elemValue = useCheckedValue ? checkedValue() : isChecked;

            // When we're first setting up this computed, don't change any model state.
            if (ko.computedContext.isInitial()) {
                return;
            }

            // We can ignore unchecked radio buttons, because some other radio
            // button will be getting checked, and that one can take care of updating state.
            if (isRadio && !isChecked) {
                return;
            }

            var modelValue = ko.dependencyDetection.ignore(valueAccessor);
            if (isValueArray) {
                if (oldElemValue !== elemValue) {
                    // When we're responding to the checkedValue changing, and the element is
                    // currently checked, replace the old elem value with the new elem value
                    // in the model array.
                    if (isChecked) {
                        ko.utils.addOrRemoveItem(modelValue, elemValue, true);
                        ko.utils.addOrRemoveItem(modelValue, oldElemValue, false);
                    }

                    oldElemValue = elemValue;
                } else {
                    // When we're responding to the user having checked/unchecked a checkbox,
                    // add/remove the element value to the model array.
                    ko.utils.addOrRemoveItem(modelValue, elemValue, isChecked);
                }
            } else {
                ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'checked', elemValue, true);
            }
        };

        function updateView() {
            // This updates the view value from the model value.
            // It runs in response to changes in the bound (checked) value.
            var modelValue = ko.utils.unwrapObservable(valueAccessor());

            if (isValueArray) {
                // When a checkbox is bound to an array, being checked represents its value being present in that array
                element.checked = ko.utils.arrayIndexOf(modelValue, checkedValue()) >= 0;
            } else if (isCheckbox) {
                // When a checkbox is bound to any other value (not an array), being checked represents the value being trueish
                element.checked = modelValue;
            } else {
                // For radio buttons, being checked means that the radio button's value corresponds to the model value
                element.checked = (checkedValue() === modelValue);
            }
        };

        var isCheckbox = element.type == "checkbox",
            isRadio = element.type == "radio";

        // Only bind to check boxes and radio buttons
        if (!isCheckbox && !isRadio) {
            return;
        }

        var isValueArray = isCheckbox && (ko.utils.unwrapObservable(valueAccessor()) instanceof Array),
            oldElemValue = isValueArray ? checkedValue() : undefined,
            useCheckedValue = isRadio || isValueArray;

        // IE 6 won't allow radio buttons to be selected unless they have a name
        if (isRadio && !element.name)
            ko.bindingHandlers['uniqueName']['init'](element, function() { return true });

        // Set up two computeds to update the binding:

        // The first responds to changes in the checkedValue value and to element clicks
        ko.computed(updateModel, null, { disposeWhenNodeIsRemoved: element });
        ko.utils.registerEventHandler(element, "click", updateModel);

        // The second responds to changes in the model value (the one associated with the checked binding)
        ko.computed(updateView, null, { disposeWhenNodeIsRemoved: element });
    }
};
ko.expressionRewriting.twoWayBindings['checked'] = true;

ko.bindingHandlers['checkedValue'] = {
    'update': function (element, valueAccessor) {
        element.value = ko.utils.unwrapObservable(valueAccessor());
    }
};

})();var classesWrittenByBindingKey = '__ko__cssValue';
ko.bindingHandlers['css'] = {
    'update': function (element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());
        if (typeof value == "object") {
            ko.utils.objectForEach(value, function(className, shouldHaveClass) {
                shouldHaveClass = ko.utils.unwrapObservable(shouldHaveClass);
                ko.utils.toggleDomNodeCssClass(element, className, shouldHaveClass);
            });
        } else {
            value = String(value || ''); // Make sure we don't try to store or set a non-string value
            ko.utils.toggleDomNodeCssClass(element, element[classesWrittenByBindingKey], false);
            element[classesWrittenByBindingKey] = value;
            ko.utils.toggleDomNodeCssClass(element, value, true);
        }
    }
};
ko.bindingHandlers['enable'] = {
    'update': function (element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());
        if (value && element.disabled)
            element.removeAttribute("disabled");
        else if ((!value) && (!element.disabled))
            element.disabled = true;
    }
};

ko.bindingHandlers['disable'] = {
    'update': function (element, valueAccessor) {
        ko.bindingHandlers['enable']['update'](element, function() { return !ko.utils.unwrapObservable(valueAccessor()) });
    }
};
// For certain common events (currently just 'click'), allow a simplified data-binding syntax
// e.g. click:handler instead of the usual full-length event:{click:handler}
function makeEventHandlerShortcut(eventName) {
    ko.bindingHandlers[eventName] = {
        'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            var newValueAccessor = function () {
                var result = {};
                result[eventName] = valueAccessor();
                return result;
            };
            return ko.bindingHandlers['event']['init'].call(this, element, newValueAccessor, allBindings, viewModel, bindingContext);
        }
    }
}

ko.bindingHandlers['event'] = {
    'init' : function (element, valueAccessor, allBindings, viewModel, bindingContext) {
        var eventsToHandle = valueAccessor() || {};
        ko.utils.objectForEach(eventsToHandle, function(eventName) {
            if (typeof eventName == "string") {
                ko.utils.registerEventHandler(element, eventName, function (event) {
                    var handlerReturnValue;
                    var handlerFunction = valueAccessor()[eventName];
                    if (!handlerFunction)
                        return;

                    try {
                        // Take all the event args, and prefix with the viewmodel
                        var argsForHandler = ko.utils.makeArray(arguments);
                        viewModel = bindingContext['$data'];
                        argsForHandler.unshift(viewModel);
                        handlerReturnValue = handlerFunction.apply(viewModel, argsForHandler);
                    } finally {
                        if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true.
                            if (event.preventDefault)
                                event.preventDefault();
                            else
                                event.returnValue = false;
                        }
                    }

                    var bubble = allBindings.get(eventName + 'Bubble') !== false;
                    if (!bubble) {
                        event.cancelBubble = true;
                        if (event.stopPropagation)
                            event.stopPropagation();
                    }
                });
            }
        });
    }
};
// "foreach: someExpression" is equivalent to "template: { foreach: someExpression }"
// "foreach: { data: someExpression, afterAdd: myfn }" is equivalent to "template: { foreach: someExpression, afterAdd: myfn }"
ko.bindingHandlers['foreach'] = {
    makeTemplateValueAccessor: function(valueAccessor) {
        return function() {
            var modelValue = valueAccessor(),
                unwrappedValue = ko.utils.peekObservable(modelValue);    // Unwrap without setting a dependency here

            // If unwrappedValue is the array, pass in the wrapped value on its own
            // The value will be unwrapped and tracked within the template binding
            // (See https://github.com/SteveSanderson/knockout/issues/523)
            if ((!unwrappedValue) || typeof unwrappedValue.length == "number")
                return { 'foreach': modelValue, 'templateEngine': ko.nativeTemplateEngine.instance };

            // If unwrappedValue.data is the array, preserve all relevant options and unwrap again value so we get updates
            ko.utils.unwrapObservable(modelValue);
            return {
                'foreach': unwrappedValue['data'],
                'as': unwrappedValue['as'],
                'includeDestroyed': unwrappedValue['includeDestroyed'],
                'afterAdd': unwrappedValue['afterAdd'],
                'beforeRemove': unwrappedValue['beforeRemove'],
                'afterRender': unwrappedValue['afterRender'],
                'beforeMove': unwrappedValue['beforeMove'],
                'afterMove': unwrappedValue['afterMove'],
                'templateEngine': ko.nativeTemplateEngine.instance
            };
        };
    },
    'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor));
    },
    'update': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor), allBindings, viewModel, bindingContext);
    }
};
ko.expressionRewriting.bindingRewriteValidators['foreach'] = false; // Can't rewrite control flow bindings
ko.virtualElements.allowedBindings['foreach'] = true;
var hasfocusUpdatingProperty = '__ko_hasfocusUpdating';
var hasfocusLastValue = '__ko_hasfocusLastValue';
ko.bindingHandlers['hasfocus'] = {
    'init': function(element, valueAccessor, allBindings) {
        var handleElementFocusChange = function(isFocused) {
            // Where possible, ignore which event was raised and determine focus state using activeElement,
            // as this avoids phantom focus/blur events raised when changing tabs in modern browsers.
            // However, not all KO-targeted browsers (Firefox 2) support activeElement. For those browsers,
            // prevent a loss of focus when changing tabs/windows by setting a flag that prevents hasfocus
            // from calling 'blur()' on the element when it loses focus.
            // Discussion at https://github.com/SteveSanderson/knockout/pull/352
            element[hasfocusUpdatingProperty] = true;
            var ownerDoc = element.ownerDocument;
            if ("activeElement" in ownerDoc) {
                var active;
                try {
                    active = ownerDoc.activeElement;
                } catch(e) {
                    // IE9 throws if you access activeElement during page load (see issue #703)
                    active = ownerDoc.body;
                }
                isFocused = (active === element);
            }
            var modelValue = valueAccessor();
            ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'hasfocus', isFocused, true);

            //cache the latest value, so we can avoid unnecessarily calling focus/blur in the update function
            element[hasfocusLastValue] = isFocused;
            element[hasfocusUpdatingProperty] = false;
        };
        var handleElementFocusIn = handleElementFocusChange.bind(null, true);
        var handleElementFocusOut = handleElementFocusChange.bind(null, false);

        ko.utils.registerEventHandler(element, "focus", handleElementFocusIn);
        ko.utils.registerEventHandler(element, "focusin", handleElementFocusIn); // For IE
        ko.utils.registerEventHandler(element, "blur",  handleElementFocusOut);
        ko.utils.registerEventHandler(element, "focusout",  handleElementFocusOut); // For IE
    },
    'update': function(element, valueAccessor) {
        var value = !!ko.utils.unwrapObservable(valueAccessor()); //force boolean to compare with last value
        if (!element[hasfocusUpdatingProperty] && element[hasfocusLastValue] !== value) {
            value ? element.focus() : element.blur();
            ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, value ? "focusin" : "focusout"]); // For IE, which doesn't reliably fire "focus" or "blur" events synchronously
        }
    }
};
ko.expressionRewriting.twoWayBindings['hasfocus'] = true;

ko.bindingHandlers['hasFocus'] = ko.bindingHandlers['hasfocus']; // Make "hasFocus" an alias
ko.expressionRewriting.twoWayBindings['hasFocus'] = true;
ko.bindingHandlers['html'] = {
    'init': function() {
        // Prevent binding on the dynamically-injected HTML (as developers are unlikely to expect that, and it has security implications)
        return { 'controlsDescendantBindings': true };
    },
    'update': function (element, valueAccessor) {
        // setHtml will unwrap the value if needed
        ko.utils.setHtml(element, valueAccessor());
    }
};
// Makes a binding like with or if
function makeWithIfBinding(bindingKey, isWith, isNot, makeContextCallback) {
    ko.bindingHandlers[bindingKey] = {
        'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            var didDisplayOnLastUpdate,
                savedNodes;
            ko.computed(function() {
                var dataValue = ko.utils.unwrapObservable(valueAccessor()),
                    shouldDisplay = !isNot !== !dataValue, // equivalent to isNot ? !dataValue : !!dataValue
                    isFirstRender = !savedNodes,
                    needsRefresh = isFirstRender || isWith || (shouldDisplay !== didDisplayOnLastUpdate);

                if (needsRefresh) {
                    // Save a copy of the inner nodes on the initial update, but only if we have dependencies.
                    if (isFirstRender && ko.computedContext.getDependenciesCount()) {
                        savedNodes = ko.utils.cloneNodes(ko.virtualElements.childNodes(element), true /* shouldCleanNodes */);
                    }

                    if (shouldDisplay) {
                        if (!isFirstRender) {
                            ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(savedNodes));
                        }
                        ko.applyBindingsToDescendants(makeContextCallback ? makeContextCallback(bindingContext, dataValue) : bindingContext, element);
                    } else {
                        ko.virtualElements.emptyNode(element);
                    }

                    didDisplayOnLastUpdate = shouldDisplay;
                }
            }, null, { disposeWhenNodeIsRemoved: element });
            return { 'controlsDescendantBindings': true };
        }
    };
    ko.expressionRewriting.bindingRewriteValidators[bindingKey] = false; // Can't rewrite control flow bindings
    ko.virtualElements.allowedBindings[bindingKey] = true;
}

// Construct the actual binding handlers
makeWithIfBinding('if');
makeWithIfBinding('ifnot', false /* isWith */, true /* isNot */);
makeWithIfBinding('with', true /* isWith */, false /* isNot */,
    function(bindingContext, dataValue) {
        return bindingContext['createChildContext'](dataValue);
    }
);
var captionPlaceholder = {};
ko.bindingHandlers['options'] = {
    'init': function(element) {
        if (ko.utils.tagNameLower(element) !== "select")
            throw new Error("options binding applies only to SELECT elements");

        // Remove all existing <option>s.
        while (element.length > 0) {
            element.remove(0);
        }

        // Ensures that the binding processor doesn't try to bind the options
        return { 'controlsDescendantBindings': true };
    },
    'update': function (element, valueAccessor, allBindings) {
        function selectedOptions() {
            return ko.utils.arrayFilter(element.options, function (node) { return node.selected; });
        }

        var selectWasPreviouslyEmpty = element.length == 0;
        var previousScrollTop = (!selectWasPreviouslyEmpty && element.multiple) ? element.scrollTop : null;
        var unwrappedArray = ko.utils.unwrapObservable(valueAccessor());
        var includeDestroyed = allBindings.get('optionsIncludeDestroyed');
        var arrayToDomNodeChildrenOptions = {};
        var captionValue;
        var filteredArray;
        var previousSelectedValues;

        if (element.multiple) {
            previousSelectedValues = ko.utils.arrayMap(selectedOptions(), ko.selectExtensions.readValue);
        } else {
            previousSelectedValues = element.selectedIndex >= 0 ? [ ko.selectExtensions.readValue(element.options[element.selectedIndex]) ] : [];
        }

        if (unwrappedArray) {
            if (typeof unwrappedArray.length == "undefined") // Coerce single value into array
                unwrappedArray = [unwrappedArray];

            // Filter out any entries marked as destroyed
            filteredArray = ko.utils.arrayFilter(unwrappedArray, function(item) {
                return includeDestroyed || item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']);
            });

            // If caption is included, add it to the array
            if (allBindings['has']('optionsCaption')) {
                captionValue = ko.utils.unwrapObservable(allBindings.get('optionsCaption'));
                // If caption value is null or undefined, don't show a caption
                if (captionValue !== null && captionValue !== undefined) {
                    filteredArray.unshift(captionPlaceholder);
                }
            }
        } else {
            // If a falsy value is provided (e.g. null), we'll simply empty the select element
        }

        function applyToObject(object, predicate, defaultValue) {
            var predicateType = typeof predicate;
            if (predicateType == "function")    // Given a function; run it against the data value
                return predicate(object);
            else if (predicateType == "string") // Given a string; treat it as a property name on the data value
                return object[predicate];
            else                                // Given no optionsText arg; use the data value itself
                return defaultValue;
        }

        // The following functions can run at two different times:
        // The first is when the whole array is being updated directly from this binding handler.
        // The second is when an observable value for a specific array entry is updated.
        // oldOptions will be empty in the first case, but will be filled with the previously generated option in the second.
        var itemUpdate = false;
        function optionForArrayItem(arrayEntry, index, oldOptions) {
            if (oldOptions.length) {
                previousSelectedValues = oldOptions[0].selected ? [ ko.selectExtensions.readValue(oldOptions[0]) ] : [];
                itemUpdate = true;
            }
            var option = element.ownerDocument.createElement("option");
            if (arrayEntry === captionPlaceholder) {
                ko.utils.setTextContent(option, allBindings.get('optionsCaption'));
                ko.selectExtensions.writeValue(option, undefined);
            } else {
                // Apply a value to the option element
                var optionValue = applyToObject(arrayEntry, allBindings.get('optionsValue'), arrayEntry);
                ko.selectExtensions.writeValue(option, ko.utils.unwrapObservable(optionValue));

                // Apply some text to the option element
                var optionText = applyToObject(arrayEntry, allBindings.get('optionsText'), optionValue);
                ko.utils.setTextContent(option, optionText);
            }
            return [option];
        }

        // By using a beforeRemove callback, we delay the removal until after new items are added. This fixes a selection
        // problem in IE<=8 and Firefox. See https://github.com/knockout/knockout/issues/1208
        arrayToDomNodeChildrenOptions['beforeRemove'] =
            function (option) {
                element.removeChild(option);
            };

        function setSelectionCallback(arrayEntry, newOptions) {
            // IE6 doesn't like us to assign selection to OPTION nodes before they're added to the document.
            // That's why we first added them without selection. Now it's time to set the selection.
            if (previousSelectedValues.length) {
                var isSelected = ko.utils.arrayIndexOf(previousSelectedValues, ko.selectExtensions.readValue(newOptions[0])) >= 0;
                ko.utils.setOptionNodeSelectionState(newOptions[0], isSelected);

                // If this option was changed from being selected during a single-item update, notify the change
                if (itemUpdate && !isSelected)
                    ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]);
            }
        }

        var callback = setSelectionCallback;
        if (allBindings['has']('optionsAfterRender')) {
            callback = function(arrayEntry, newOptions) {
                setSelectionCallback(arrayEntry, newOptions);
                ko.dependencyDetection.ignore(allBindings.get('optionsAfterRender'), null, [newOptions[0], arrayEntry !== captionPlaceholder ? arrayEntry : undefined]);
            }
        }

        ko.utils.setDomNodeChildrenFromArrayMapping(element, filteredArray, optionForArrayItem, arrayToDomNodeChildrenOptions, callback);

        ko.dependencyDetection.ignore(function () {
            if (allBindings.get('valueAllowUnset') && allBindings['has']('value')) {
                // The model value is authoritative, so make sure its value is the one selected
                ko.selectExtensions.writeValue(element, ko.utils.unwrapObservable(allBindings.get('value')), true /* allowUnset */);
            } else {
                // Determine if the selection has changed as a result of updating the options list
                var selectionChanged;
                if (element.multiple) {
                    // For a multiple-select box, compare the new selection count to the previous one
                    // But if nothing was selected before, the selection can't have changed
                    selectionChanged = previousSelectedValues.length && selectedOptions().length < previousSelectedValues.length;
                } else {
                    // For a single-select box, compare the current value to the previous value
                    // But if nothing was selected before or nothing is selected now, just look for a change in selection
                    selectionChanged = (previousSelectedValues.length && element.selectedIndex >= 0)
                        ? (ko.selectExtensions.readValue(element.options[element.selectedIndex]) !== previousSelectedValues[0])
                        : (previousSelectedValues.length || element.selectedIndex >= 0);
                }

                // Ensure consistency between model value and selected option.
                // If the dropdown was changed so that selection is no longer the same,
                // notify the value or selectedOptions binding.
                if (selectionChanged) {
                    ko.utils.triggerEvent(element, "change");
                }
            }
        });

        // Workaround for IE bug
        ko.utils.ensureSelectElementIsRenderedCorrectly(element);

        if (previousScrollTop && Math.abs(previousScrollTop - element.scrollTop) > 20)
            element.scrollTop = previousScrollTop;
    }
};
ko.bindingHandlers['options'].optionValueDomDataKey = ko.utils.domData.nextKey();
ko.bindingHandlers['selectedOptions'] = {
    'after': ['options', 'foreach'],
    'init': function (element, valueAccessor, allBindings) {
        ko.utils.registerEventHandler(element, "change", function () {
            var value = valueAccessor(), valueToWrite = [];
            ko.utils.arrayForEach(element.getElementsByTagName("option"), function(node) {
                if (node.selected)
                    valueToWrite.push(ko.selectExtensions.readValue(node));
            });
            ko.expressionRewriting.writeValueToProperty(value, allBindings, 'selectedOptions', valueToWrite);
        });
    },
    'update': function (element, valueAccessor) {
        if (ko.utils.tagNameLower(element) != "select")
            throw new Error("values binding applies only to SELECT elements");

        var newValue = ko.utils.unwrapObservable(valueAccessor());
        if (newValue && typeof newValue.length == "number") {
            ko.utils.arrayForEach(element.getElementsByTagName("option"), function(node) {
                var isSelected = ko.utils.arrayIndexOf(newValue, ko.selectExtensions.readValue(node)) >= 0;
                ko.utils.setOptionNodeSelectionState(node, isSelected);
            });
        }
    }
};
ko.expressionRewriting.twoWayBindings['selectedOptions'] = true;
ko.bindingHandlers['style'] = {
    'update': function (element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor() || {});
        ko.utils.objectForEach(value, function(styleName, styleValue) {
            styleValue = ko.utils.unwrapObservable(styleValue);

            if (styleValue === null || styleValue === undefined || styleValue === false) {
                // Empty string removes the value, whereas null/undefined have no effect
                styleValue = "";
            }

            element.style[styleName] = styleValue;
        });
    }
};
ko.bindingHandlers['submit'] = {
    'init': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
        if (typeof valueAccessor() != "function")
            throw new Error("The value for a submit binding must be a function");
        ko.utils.registerEventHandler(element, "submit", function (event) {
            var handlerReturnValue;
            var value = valueAccessor();
            try { handlerReturnValue = value.call(bindingContext['$data'], element); }
            finally {
                if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true.
                    if (event.preventDefault)
                        event.preventDefault();
                    else
                        event.returnValue = false;
                }
            }
        });
    }
};
ko.bindingHandlers['text'] = {
    'init': function() {
        // Prevent binding on the dynamically-injected text node (as developers are unlikely to expect that, and it has security implications).
        // It should also make things faster, as we no longer have to consider whether the text node might be bindable.
        return { 'controlsDescendantBindings': true };
    },
    'update': function (element, valueAccessor) {
        ko.utils.setTextContent(element, valueAccessor());
    }
};
ko.virtualElements.allowedBindings['text'] = true;
(function () {

if (window && window.navigator) {
    var parseVersion = function (matches) {
        if (matches) {
            return parseFloat(matches[1]);
        }
    };

    // Detect various browser versions because some old versions don't fully support the 'input' event
    var operaVersion = window.opera && window.opera.version && parseInt(window.opera.version()),
        userAgent = window.navigator.userAgent,
        safariVersion = parseVersion(userAgent.match(/^(?:(?!chrome).)*version\/([^ ]*) safari/i)),
        firefoxVersion = parseVersion(userAgent.match(/Firefox\/([^ ]*)/));
}

// IE 8 and 9 have bugs that prevent the normal events from firing when the value changes.
// But it does fire the 'selectionchange' event on many of those, presumably because the
// cursor is moving and that counts as the selection changing. The 'selectionchange' event is
// fired at the document level only and doesn't directly indicate which element changed. We
// set up just one event handler for the document and use 'activeElement' to determine which
// element was changed.
if (ko.utils.ieVersion < 10) {
    var selectionChangeRegisteredName = ko.utils.domData.nextKey(),
        selectionChangeHandlerName = ko.utils.domData.nextKey();
    var selectionChangeHandler = function(event) {
        var target = this.activeElement,
            handler = target && ko.utils.domData.get(target, selectionChangeHandlerName);
        if (handler) {
            handler(event);
        }
    };
    var registerForSelectionChangeEvent = function (element, handler) {
        var ownerDoc = element.ownerDocument;
        if (!ko.utils.domData.get(ownerDoc, selectionChangeRegisteredName)) {
            ko.utils.domData.set(ownerDoc, selectionChangeRegisteredName, true);
            ko.utils.registerEventHandler(ownerDoc, 'selectionchange', selectionChangeHandler);
        }
        ko.utils.domData.set(element, selectionChangeHandlerName, handler);
    };
}

ko.bindingHandlers['textInput'] = {
    'init': function (element, valueAccessor, allBindings) {

        var previousElementValue = element.value,
            timeoutHandle,
            elementValueBeforeEvent;

        var updateModel = function (event) {
            clearTimeout(timeoutHandle);
            elementValueBeforeEvent = timeoutHandle = undefined;

            var elementValue = element.value;
            if (previousElementValue !== elementValue) {
                // Provide a way for tests to know exactly which event was processed
                if (DEBUG && event) element['_ko_textInputProcessedEvent'] = event.type;
                previousElementValue = elementValue;
                ko.expressionRewriting.writeValueToProperty(valueAccessor(), allBindings, 'textInput', elementValue);
            }
        };

        var deferUpdateModel = function (event) {
            if (!timeoutHandle) {
                // The elementValueBeforeEvent variable is set *only* during the brief gap between an
                // event firing and the updateModel function running. This allows us to ignore model
                // updates that are from the previous state of the element, usually due to techniques
                // such as rateLimit. Such updates, if not ignored, can cause keystrokes to be lost.
                elementValueBeforeEvent = element.value;
                var handler = DEBUG ? updateModel.bind(element, {type: event.type}) : updateModel;
                timeoutHandle = setTimeout(handler, 4);
            }
        };

        var updateView = function () {
            var modelValue = ko.utils.unwrapObservable(valueAccessor());

            if (modelValue === null || modelValue === undefined) {
                modelValue = '';
            }

            if (elementValueBeforeEvent !== undefined && modelValue === elementValueBeforeEvent) {
                setTimeout(updateView, 4);
                return;
            }

            // Update the element only if the element and model are different. On some browsers, updating the value
            // will move the cursor to the end of the input, which would be bad while the user is typing.
            if (element.value !== modelValue) {
                previousElementValue = modelValue;  // Make sure we ignore events (propertychange) that result from updating the value
                element.value = modelValue;
            }
        };

        var onEvent = function (event, handler) {
            ko.utils.registerEventHandler(element, event, handler);
        };

        if (DEBUG && ko.bindingHandlers['textInput']['_forceUpdateOn']) {
            // Provide a way for tests to specify exactly which events are bound
            ko.utils.arrayForEach(ko.bindingHandlers['textInput']['_forceUpdateOn'], function(eventName) {
                if (eventName.slice(0,5) == 'after') {
                    onEvent(eventName.slice(5), deferUpdateModel);
                } else {
                    onEvent(eventName, updateModel);
                }
            });
        } else {
            if (ko.utils.ieVersion < 10) {
                // Internet Explorer <= 8 doesn't support the 'input' event, but does include 'propertychange' that fires whenever
                // any property of an element changes. Unlike 'input', it also fires if a property is changed from JavaScript code,
                // but that's an acceptable compromise for this binding. IE 9 does support 'input', but since it doesn't fire it
                // when using autocomplete, we'll use 'propertychange' for it also.
                onEvent('propertychange', function(event) {
                    if (event.propertyName === 'value') {
                        updateModel(event);
                    }
                });

                if (ko.utils.ieVersion == 8) {
                    // IE 8 has a bug where it fails to fire 'propertychange' on the first update following a value change from
                    // JavaScript code. It also doesn't fire if you clear the entire value. To fix this, we bind to the following
                    // events too.
                    onEvent('keyup', updateModel);      // A single keystoke
                    onEvent('keydown', updateModel);    // The first character when a key is held down
                }
                if (ko.utils.ieVersion >= 8) {
                    // Internet Explorer 9 doesn't fire the 'input' event when deleting text, including using
                    // the backspace, delete, or ctrl-x keys, clicking the 'x' to clear the input, dragging text
                    // out of the field, and cutting or deleting text using the context menu. 'selectionchange'
                    // can detect all of those except dragging text out of the field, for which we use 'dragend'.
                    // These are also needed in IE8 because of the bug described above.
                    registerForSelectionChangeEvent(element, updateModel);  // 'selectionchange' covers cut, paste, drop, delete, etc.
                    onEvent('dragend', deferUpdateModel);
                }
            } else {
                // All other supported browsers support the 'input' event, which fires whenever the content of the element is changed
                // through the user interface.
                onEvent('input', updateModel);

                if (safariVersion < 5 && ko.utils.tagNameLower(element) === "textarea") {
                    // Safari <5 doesn't fire the 'input' event for <textarea> elements (it does fire 'textInput'
                    // but only when typing). So we'll just catch as much as we can with keydown, cut, and paste.
                    onEvent('keydown', deferUpdateModel);
                    onEvent('paste', deferUpdateModel);
                    onEvent('cut', deferUpdateModel);
                } else if (operaVersion < 11) {
                    // Opera 10 doesn't always fire the 'input' event for cut, paste, undo & drop operations.
                    // We can try to catch some of those using 'keydown'.
                    onEvent('keydown', deferUpdateModel);
                } else if (firefoxVersion < 4.0) {
                    // Firefox <= 3.6 doesn't fire the 'input' event when text is filled in through autocomplete
                    onEvent('DOMAutoComplete', updateModel);

                    // Firefox <=3.5 doesn't fire the 'input' event when text is dropped into the input.
                    onEvent('dragdrop', updateModel);       // <3.5
                    onEvent('drop', updateModel);           // 3.5
                }
            }
        }

        // Bind to the change event so that we can catch programmatic updates of the value that fire this event.
        onEvent('change', updateModel);

        ko.computed(updateView, null, { disposeWhenNodeIsRemoved: element });
    }
};
ko.expressionRewriting.twoWayBindings['textInput'] = true;

// textinput is an alias for textInput
ko.bindingHandlers['textinput'] = {
    // preprocess is the only way to set up a full alias
    'preprocess': function (value, name, addBinding) {
        addBinding('textInput', value);
    }
};

})();ko.bindingHandlers['uniqueName'] = {
    'init': function (element, valueAccessor) {
        if (valueAccessor()) {
            var name = "ko_unique_" + (++ko.bindingHandlers['uniqueName'].currentIndex);
            ko.utils.setElementName(element, name);
        }
    }
};
ko.bindingHandlers['uniqueName'].currentIndex = 0;
ko.bindingHandlers['value'] = {
    'after': ['options', 'foreach'],
    'init': function (element, valueAccessor, allBindings) {
        // If the value binding is placed on a radio/checkbox, then just pass through to checkedValue and quit
        if (element.tagName.toLowerCase() == "input" && (element.type == "checkbox" || element.type == "radio")) {
            ko.applyBindingAccessorsToNode(element, { 'checkedValue': valueAccessor });
            return;
        }

        // Always catch "change" event; possibly other events too if asked
        var eventsToCatch = ["change"];
        var requestedEventsToCatch = allBindings.get("valueUpdate");
        var propertyChangedFired = false;
        var elementValueBeforeEvent = null;

        if (requestedEventsToCatch) {
            if (typeof requestedEventsToCatch == "string") // Allow both individual event names, and arrays of event names
                requestedEventsToCatch = [requestedEventsToCatch];
            ko.utils.arrayPushAll(eventsToCatch, requestedEventsToCatch);
            eventsToCatch = ko.utils.arrayGetDistinctValues(eventsToCatch);
        }

        var valueUpdateHandler = function() {
            elementValueBeforeEvent = null;
            propertyChangedFired = false;
            var modelValue = valueAccessor();
            var elementValue = ko.selectExtensions.readValue(element);
            ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'value', elementValue);
        }

        // Workaround for https://github.com/SteveSanderson/knockout/issues/122
        // IE doesn't fire "change" events on textboxes if the user selects a value from its autocomplete list
        var ieAutoCompleteHackNeeded = ko.utils.ieVersion && element.tagName.toLowerCase() == "input" && element.type == "text"
                                       && element.autocomplete != "off" && (!element.form || element.form.autocomplete != "off");
        if (ieAutoCompleteHackNeeded && ko.utils.arrayIndexOf(eventsToCatch, "propertychange") == -1) {
            ko.utils.registerEventHandler(element, "propertychange", function () { propertyChangedFired = true });
            ko.utils.registerEventHandler(element, "focus", function () { propertyChangedFired = false });
            ko.utils.registerEventHandler(element, "blur", function() {
                if (propertyChangedFired) {
                    valueUpdateHandler();
                }
            });
        }

        ko.utils.arrayForEach(eventsToCatch, function(eventName) {
            // The syntax "after<eventname>" means "run the handler asynchronously after the event"
            // This is useful, for example, to catch "keydown" events after the browser has updated the control
            // (otherwise, ko.selectExtensions.readValue(this) will receive the control's value *before* the key event)
            var handler = valueUpdateHandler;
            if (ko.utils.stringStartsWith(eventName, "after")) {
                handler = function() {
                    // The elementValueBeforeEvent variable is non-null *only* during the brief gap between
                    // a keyX event firing and the valueUpdateHandler running, which is scheduled to happen
                    // at the earliest asynchronous opportunity. We store this temporary information so that
                    // if, between keyX and valueUpdateHandler, the underlying model value changes separately,
                    // we can overwrite that model value change with the value the user just typed. Otherwise,
                    // techniques like rateLimit can trigger model changes at critical moments that will
                    // override the user's inputs, causing keystrokes to be lost.
                    elementValueBeforeEvent = ko.selectExtensions.readValue(element);
                    setTimeout(valueUpdateHandler, 0);
                };
                eventName = eventName.substring("after".length);
            }
            ko.utils.registerEventHandler(element, eventName, handler);
        });

        var updateFromModel = function () {
            var newValue = ko.utils.unwrapObservable(valueAccessor());
            var elementValue = ko.selectExtensions.readValue(element);

            if (elementValueBeforeEvent !== null && newValue === elementValueBeforeEvent) {
                setTimeout(updateFromModel, 0);
                return;
            }

            var valueHasChanged = (newValue !== elementValue);

            if (valueHasChanged) {
                if (ko.utils.tagNameLower(element) === "select") {
                    var allowUnset = allBindings.get('valueAllowUnset');
                    var applyValueAction = function () {
                        ko.selectExtensions.writeValue(element, newValue, allowUnset);
                    };
                    applyValueAction();

                    if (!allowUnset && newValue !== ko.selectExtensions.readValue(element)) {
                        // If you try to set a model value that can't be represented in an already-populated dropdown, reject that change,
                        // because you're not allowed to have a model value that disagrees with a visible UI selection.
                        ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]);
                    } else {
                        // Workaround for IE6 bug: It won't reliably apply values to SELECT nodes during the same execution thread
                        // right after you've changed the set of OPTION nodes on it. So for that node type, we'll schedule a second thread
                        // to apply the value as well.
                        setTimeout(applyValueAction, 0);
                    }
                } else {
                    ko.selectExtensions.writeValue(element, newValue);
                }
            }
        };

        ko.computed(updateFromModel, null, { disposeWhenNodeIsRemoved: element });
    },
    'update': function() {} // Keep for backwards compatibility with code that may have wrapped value binding
};
ko.expressionRewriting.twoWayBindings['value'] = true;
ko.bindingHandlers['visible'] = {
    'update': function (element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());
        var isCurrentlyVisible = !(element.style.display == "none");
        if (value && !isCurrentlyVisible)
            element.style.display = "";
        else if ((!value) && isCurrentlyVisible)
            element.style.display = "none";
    }
};
// 'click' is just a shorthand for the usual full-length event:{click:handler}
makeEventHandlerShortcut('click');
// If you want to make a custom template engine,
//
// [1] Inherit from this class (like ko.nativeTemplateEngine does)
// [2] Override 'renderTemplateSource', supplying a function with this signature:
//
//        function (templateSource, bindingContext, options) {
//            // - templateSource.text() is the text of the template you should render
//            // - bindingContext.$data is the data you should pass into the template
//            //   - you might also want to make bindingContext.$parent, bindingContext.$parents,
//            //     and bindingContext.$root available in the template too
//            // - options gives you access to any other properties set on "data-bind: { template: options }"
//            //
//            // Return value: an array of DOM nodes
//        }
//
// [3] Override 'createJavaScriptEvaluatorBlock', supplying a function with this signature:
//
//        function (script) {
//            // Return value: Whatever syntax means "Evaluate the JavaScript statement 'script' and output the result"
//            //               For example, the jquery.tmpl template engine converts 'someScript' to '${ someScript }'
//        }
//
//     This is only necessary if you want to allow data-bind attributes to reference arbitrary template variables.
//     If you don't want to allow that, you can set the property 'allowTemplateRewriting' to false (like ko.nativeTemplateEngine does)
//     and then you don't need to override 'createJavaScriptEvaluatorBlock'.

ko.templateEngine = function () { };

ko.templateEngine.prototype['renderTemplateSource'] = function (templateSource, bindingContext, options) {
    throw new Error("Override renderTemplateSource");
};

ko.templateEngine.prototype['createJavaScriptEvaluatorBlock'] = function (script) {
    throw new Error("Override createJavaScriptEvaluatorBlock");
};

ko.templateEngine.prototype['makeTemplateSource'] = function(template, templateDocument) {
    // Named template
    if (typeof template == "string") {
        templateDocument = templateDocument || document;
        var elem = templateDocument.getElementById(template);
        if (!elem)
            throw new Error("Cannot find template with ID " + template);
        return new ko.templateSources.domElement(elem);
    } else if ((template.nodeType == 1) || (template.nodeType == 8)) {
        // Anonymous template
        return new ko.templateSources.anonymousTemplate(template);
    } else
        throw new Error("Unknown template type: " + template);
};

ko.templateEngine.prototype['renderTemplate'] = function (template, bindingContext, options, templateDocument) {
    var templateSource = this['makeTemplateSource'](template, templateDocument);
    return this['renderTemplateSource'](templateSource, bindingContext, options);
};

ko.templateEngine.prototype['isTemplateRewritten'] = function (template, templateDocument) {
    // Skip rewriting if requested
    if (this['allowTemplateRewriting'] === false)
        return true;
    return this['makeTemplateSource'](template, templateDocument)['data']("isRewritten");
};

ko.templateEngine.prototype['rewriteTemplate'] = function (template, rewriterCallback, templateDocument) {
    var templateSource = this['makeTemplateSource'](template, templateDocument);
    var rewritten = rewriterCallback(templateSource['text']());
    templateSource['text'](rewritten);
    templateSource['data']("isRewritten", true);
};

ko.exportSymbol('templateEngine', ko.templateEngine);

ko.templateRewriting = (function () {
    var memoizeDataBindingAttributeSyntaxRegex = /(<([a-z]+\d*)(?:\s+(?!data-bind\s*=\s*)[a-z0-9\-]+(?:=(?:\"[^\"]*\"|\'[^\']*\'))?)*\s+)data-bind\s*=\s*(["'])([\s\S]*?)\3/gi;
    var memoizeVirtualContainerBindingSyntaxRegex = /<!--\s*ko\b\s*([\s\S]*?)\s*-->/g;

    function validateDataBindValuesForRewriting(keyValueArray) {
        var allValidators = ko.expressionRewriting.bindingRewriteValidators;
        for (var i = 0; i < keyValueArray.length; i++) {
            var key = keyValueArray[i]['key'];
            if (allValidators.hasOwnProperty(key)) {
                var validator = allValidators[key];

                if (typeof validator === "function") {
                    var possibleErrorMessage = validator(keyValueArray[i]['value']);
                    if (possibleErrorMessage)
                        throw new Error(possibleErrorMessage);
                } else if (!validator) {
                    throw new Error("This template engine does not support the '" + key + "' binding within its templates");
                }
            }
        }
    }

    function constructMemoizedTagReplacement(dataBindAttributeValue, tagToRetain, nodeName, templateEngine) {
        var dataBindKeyValueArray = ko.expressionRewriting.parseObjectLiteral(dataBindAttributeValue);
        validateDataBindValuesForRewriting(dataBindKeyValueArray);
        var rewrittenDataBindAttributeValue = ko.expressionRewriting.preProcessBindings(dataBindKeyValueArray, {'valueAccessors':true});

        // For no obvious reason, Opera fails to evaluate rewrittenDataBindAttributeValue unless it's wrapped in an additional
        // anonymous function, even though Opera's built-in debugger can evaluate it anyway. No other browser requires this
        // extra indirection.
        var applyBindingsToNextSiblingScript =
            "ko.__tr_ambtns(function($context,$element){return(function(){return{ " + rewrittenDataBindAttributeValue + " } })()},'" + nodeName.toLowerCase() + "')";
        return templateEngine['createJavaScriptEvaluatorBlock'](applyBindingsToNextSiblingScript) + tagToRetain;
    }

    return {
        ensureTemplateIsRewritten: function (template, templateEngine, templateDocument) {
            if (!templateEngine['isTemplateRewritten'](template, templateDocument))
                templateEngine['rewriteTemplate'](template, function (htmlString) {
                    return ko.templateRewriting.memoizeBindingAttributeSyntax(htmlString, templateEngine);
                }, templateDocument);
        },

        memoizeBindingAttributeSyntax: function (htmlString, templateEngine) {
            return htmlString.replace(memoizeDataBindingAttributeSyntaxRegex, function () {
                return constructMemoizedTagReplacement(/* dataBindAttributeValue: */ arguments[4], /* tagToRetain: */ arguments[1], /* nodeName: */ arguments[2], templateEngine);
            }).replace(memoizeVirtualContainerBindingSyntaxRegex, function() {
                return constructMemoizedTagReplacement(/* dataBindAttributeValue: */ arguments[1], /* tagToRetain: */ "<!-- ko -->", /* nodeName: */ "#comment", templateEngine);
            });
        },

        applyMemoizedBindingsToNextSibling: function (bindings, nodeName) {
            return ko.memoization.memoize(function (domNode, bindingContext) {
                var nodeToBind = domNode.nextSibling;
                if (nodeToBind && nodeToBind.nodeName.toLowerCase() === nodeName) {
                    ko.applyBindingAccessorsToNode(nodeToBind, bindings, bindingContext);
                }
            });
        }
    }
})();


// Exported only because it has to be referenced by string lookup from within rewritten template
ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextSibling);
(function() {
    // A template source represents a read/write way of accessing a template. This is to eliminate the need for template loading/saving
    // logic to be duplicated in every template engine (and means they can all work with anonymous templates, etc.)
    //
    // Two are provided by default:
    //  1. ko.templateSources.domElement       - reads/writes the text content of an arbitrary DOM element
    //  2. ko.templateSources.anonymousElement - uses ko.utils.domData to read/write text *associated* with the DOM element, but
    //                                           without reading/writing the actual element text content, since it will be overwritten
    //                                           with the rendered template output.
    // You can implement your own template source if you want to fetch/store templates somewhere other than in DOM elements.
    // Template sources need to have the following functions:
    //   text() 			- returns the template text from your storage location
    //   text(value)		- writes the supplied template text to your storage location
    //   data(key)			- reads values stored using data(key, value) - see below
    //   data(key, value)	- associates "value" with this template and the key "key". Is used to store information like "isRewritten".
    //
    // Optionally, template sources can also have the following functions:
    //   nodes()            - returns a DOM element containing the nodes of this template, where available
    //   nodes(value)       - writes the given DOM element to your storage location
    // If a DOM element is available for a given template source, template engines are encouraged to use it in preference over text()
    // for improved speed. However, all templateSources must supply text() even if they don't supply nodes().
    //
    // Once you've implemented a templateSource, make your template engine use it by subclassing whatever template engine you were
    // using and overriding "makeTemplateSource" to return an instance of your custom template source.

    ko.templateSources = {};

    // ---- ko.templateSources.domElement -----

    ko.templateSources.domElement = function(element) {
        this.domElement = element;
    }

    ko.templateSources.domElement.prototype['text'] = function(/* valueToWrite */) {
        var tagNameLower = ko.utils.tagNameLower(this.domElement),
            elemContentsProperty = tagNameLower === "script" ? "text"
                                 : tagNameLower === "textarea" ? "value"
                                 : "innerHTML";

        if (arguments.length == 0) {
            return this.domElement[elemContentsProperty];
        } else {
            var valueToWrite = arguments[0];
            if (elemContentsProperty === "innerHTML")
                ko.utils.setHtml(this.domElement, valueToWrite);
            else
                this.domElement[elemContentsProperty] = valueToWrite;
        }
    };

    var dataDomDataPrefix = ko.utils.domData.nextKey() + "_";
    ko.templateSources.domElement.prototype['data'] = function(key /*, valueToWrite */) {
        if (arguments.length === 1) {
            return ko.utils.domData.get(this.domElement, dataDomDataPrefix + key);
        } else {
            ko.utils.domData.set(this.domElement, dataDomDataPrefix + key, arguments[1]);
        }
    };

    // ---- ko.templateSources.anonymousTemplate -----
    // Anonymous templates are normally saved/retrieved as DOM nodes through "nodes".
    // For compatibility, you can also read "text"; it will be serialized from the nodes on demand.
    // Writing to "text" is still supported, but then the template data will not be available as DOM nodes.

    var anonymousTemplatesDomDataKey = ko.utils.domData.nextKey();
    ko.templateSources.anonymousTemplate = function(element) {
        this.domElement = element;
    }
    ko.templateSources.anonymousTemplate.prototype = new ko.templateSources.domElement();
    ko.templateSources.anonymousTemplate.prototype.constructor = ko.templateSources.anonymousTemplate;
    ko.templateSources.anonymousTemplate.prototype['text'] = function(/* valueToWrite */) {
        if (arguments.length == 0) {
            var templateData = ko.utils.domData.get(this.domElement, anonymousTemplatesDomDataKey) || {};
            if (templateData.textData === undefined && templateData.containerData)
                templateData.textData = templateData.containerData.innerHTML;
            return templateData.textData;
        } else {
            var valueToWrite = arguments[0];
            ko.utils.domData.set(this.domElement, anonymousTemplatesDomDataKey, {textData: valueToWrite});
        }
    };
    ko.templateSources.domElement.prototype['nodes'] = function(/* valueToWrite */) {
        if (arguments.length == 0) {
            var templateData = ko.utils.domData.get(this.domElement, anonymousTemplatesDomDataKey) || {};
            return templateData.containerData;
        } else {
            var valueToWrite = arguments[0];
            ko.utils.domData.set(this.domElement, anonymousTemplatesDomDataKey, {containerData: valueToWrite});
        }
    };

    ko.exportSymbol('templateSources', ko.templateSources);
    ko.exportSymbol('templateSources.domElement', ko.templateSources.domElement);
    ko.exportSymbol('templateSources.anonymousTemplate', ko.templateSources.anonymousTemplate);
})();
(function () {
    var _templateEngine;
    ko.setTemplateEngine = function (templateEngine) {
        if ((templateEngine != undefined) && !(templateEngine instanceof ko.templateEngine))
            throw new Error("templateEngine must inherit from ko.templateEngine");
        _templateEngine = templateEngine;
    }

    function invokeForEachNodeInContinuousRange(firstNode, lastNode, action) {
        var node, nextInQueue = firstNode, firstOutOfRangeNode = ko.virtualElements.nextSibling(lastNode);
        while (nextInQueue && ((node = nextInQueue) !== firstOutOfRangeNode)) {
            nextInQueue = ko.virtualElements.nextSibling(node);
            action(node, nextInQueue);
        }
    }

    function activateBindingsOnContinuousNodeArray(continuousNodeArray, bindingContext) {
        // To be used on any nodes that have been rendered by a template and have been inserted into some parent element
        // Walks through continuousNodeArray (which *must* be continuous, i.e., an uninterrupted sequence of sibling nodes, because
        // the algorithm for walking them relies on this), and for each top-level item in the virtual-element sense,
        // (1) Does a regular "applyBindings" to associate bindingContext with this node and to activate any non-memoized bindings
        // (2) Unmemoizes any memos in the DOM subtree (e.g., to activate bindings that had been memoized during template rewriting)

        if (continuousNodeArray.length) {
            var firstNode = continuousNodeArray[0],
                lastNode = continuousNodeArray[continuousNodeArray.length - 1],
                parentNode = firstNode.parentNode,
                provider = ko.bindingProvider['instance'],
                preprocessNode = provider['preprocessNode'];

            if (preprocessNode) {
                invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node, nextNodeInRange) {
                    var nodePreviousSibling = node.previousSibling;
                    var newNodes = preprocessNode.call(provider, node);
                    if (newNodes) {
                        if (node === firstNode)
                            firstNode = newNodes[0] || nextNodeInRange;
                        if (node === lastNode)
                            lastNode = newNodes[newNodes.length - 1] || nodePreviousSibling;
                    }
                });

                // Because preprocessNode can change the nodes, including the first and last nodes, update continuousNodeArray to match.
                // We need the full set, including inner nodes, because the unmemoize step might remove the first node (and so the real
                // first node needs to be in the array).
                continuousNodeArray.length = 0;
                if (!firstNode) { // preprocessNode might have removed all the nodes, in which case there's nothing left to do
                    return;
                }
                if (firstNode === lastNode) {
                    continuousNodeArray.push(firstNode);
                } else {
                    continuousNodeArray.push(firstNode, lastNode);
                    ko.utils.fixUpContinuousNodeArray(continuousNodeArray, parentNode);
                }
            }

            // Need to applyBindings *before* unmemoziation, because unmemoization might introduce extra nodes (that we don't want to re-bind)
            // whereas a regular applyBindings won't introduce new memoized nodes
            invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node) {
                if (node.nodeType === 1 || node.nodeType === 8)
                    ko.applyBindings(bindingContext, node);
            });
            invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node) {
                if (node.nodeType === 1 || node.nodeType === 8)
                    ko.memoization.unmemoizeDomNodeAndDescendants(node, [bindingContext]);
            });

            // Make sure any changes done by applyBindings or unmemoize are reflected in the array
            ko.utils.fixUpContinuousNodeArray(continuousNodeArray, parentNode);
        }
    }

    function getFirstNodeFromPossibleArray(nodeOrNodeArray) {
        return nodeOrNodeArray.nodeType ? nodeOrNodeArray
                                        : nodeOrNodeArray.length > 0 ? nodeOrNodeArray[0]
                                        : null;
    }

    function executeTemplate(targetNodeOrNodeArray, renderMode, template, bindingContext, options) {
        options = options || {};
        var firstTargetNode = targetNodeOrNodeArray && getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
        var templateDocument = firstTargetNode && firstTargetNode.ownerDocument;
        var templateEngineToUse = (options['templateEngine'] || _templateEngine);
        ko.templateRewriting.ensureTemplateIsRewritten(template, templateEngineToUse, templateDocument);
        var renderedNodesArray = templateEngineToUse['renderTemplate'](template, bindingContext, options, templateDocument);

        // Loosely check result is an array of DOM nodes
        if ((typeof renderedNodesArray.length != "number") || (renderedNodesArray.length > 0 && typeof renderedNodesArray[0].nodeType != "number"))
            throw new Error("Template engine must return an array of DOM nodes");

        var haveAddedNodesToParent = false;
        switch (renderMode) {
            case "replaceChildren":
                ko.virtualElements.setDomNodeChildren(targetNodeOrNodeArray, renderedNodesArray);
                haveAddedNodesToParent = true;
                break;
            case "replaceNode":
                ko.utils.replaceDomNodes(targetNodeOrNodeArray, renderedNodesArray);
                haveAddedNodesToParent = true;
                break;
            case "ignoreTargetNode": break;
            default:
                throw new Error("Unknown renderMode: " + renderMode);
        }

        if (haveAddedNodesToParent) {
            activateBindingsOnContinuousNodeArray(renderedNodesArray, bindingContext);
            if (options['afterRender'])
                ko.dependencyDetection.ignore(options['afterRender'], null, [renderedNodesArray, bindingContext['$data']]);
        }

        return renderedNodesArray;
    }

    function resolveTemplateName(template, data, context) {
        // The template can be specified as:
        if (ko.isObservable(template)) {
            // 1. An observable, with string value
            return template();
        } else if (typeof template === 'function') {
            // 2. A function of (data, context) returning a string
            return template(data, context);
        } else {
            // 3. A string
            return template;
        }
    }

    ko.renderTemplate = function (template, dataOrBindingContext, options, targetNodeOrNodeArray, renderMode) {
        options = options || {};
        if ((options['templateEngine'] || _templateEngine) == undefined)
            throw new Error("Set a template engine before calling renderTemplate");
        renderMode = renderMode || "replaceChildren";

        if (targetNodeOrNodeArray) {
            var firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray);

            var whenToDispose = function () { return (!firstTargetNode) || !ko.utils.domNodeIsAttachedToDocument(firstTargetNode); }; // Passive disposal (on next evaluation)
            var activelyDisposeWhenNodeIsRemoved = (firstTargetNode && renderMode == "replaceNode") ? firstTargetNode.parentNode : firstTargetNode;

            return ko.dependentObservable( // So the DOM is automatically updated when any dependency changes
                function () {
                    // Ensure we've got a proper binding context to work with
                    var bindingContext = (dataOrBindingContext && (dataOrBindingContext instanceof ko.bindingContext))
                        ? dataOrBindingContext
                        : new ko.bindingContext(ko.utils.unwrapObservable(dataOrBindingContext));

                    var templateName = resolveTemplateName(template, bindingContext['$data'], bindingContext),
                        renderedNodesArray = executeTemplate(targetNodeOrNodeArray, renderMode, templateName, bindingContext, options);

                    if (renderMode == "replaceNode") {
                        targetNodeOrNodeArray = renderedNodesArray;
                        firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
                    }
                },
                null,
                { disposeWhen: whenToDispose, disposeWhenNodeIsRemoved: activelyDisposeWhenNodeIsRemoved }
            );
        } else {
            // We don't yet have a DOM node to evaluate, so use a memo and render the template later when there is a DOM node
            return ko.memoization.memoize(function (domNode) {
                ko.renderTemplate(template, dataOrBindingContext, options, domNode, "replaceNode");
            });
        }
    };

    ko.renderTemplateForEach = function (template, arrayOrObservableArray, options, targetNode, parentBindingContext) {
        // Since setDomNodeChildrenFromArrayMapping always calls executeTemplateForArrayItem and then
        // activateBindingsCallback for added items, we can store the binding context in the former to use in the latter.
        var arrayItemContext;

        // This will be called by setDomNodeChildrenFromArrayMapping to get the nodes to add to targetNode
        var executeTemplateForArrayItem = function (arrayValue, index) {
            // Support selecting template as a function of the data being rendered
            arrayItemContext = parentBindingContext['createChildContext'](arrayValue, options['as'], function(context) {
                context['$index'] = index;
            });

            var templateName = resolveTemplateName(template, arrayValue, arrayItemContext);
            return executeTemplate(null, "ignoreTargetNode", templateName, arrayItemContext, options);
        }

        // This will be called whenever setDomNodeChildrenFromArrayMapping has added nodes to targetNode
        var activateBindingsCallback = function(arrayValue, addedNodesArray, index) {
            activateBindingsOnContinuousNodeArray(addedNodesArray, arrayItemContext);
            if (options['afterRender'])
                options['afterRender'](addedNodesArray, arrayValue);
        };

        return ko.dependentObservable(function () {
            var unwrappedArray = ko.utils.unwrapObservable(arrayOrObservableArray) || [];
            if (typeof unwrappedArray.length == "undefined") // Coerce single value into array
                unwrappedArray = [unwrappedArray];

            // Filter out any entries marked as destroyed
            var filteredArray = ko.utils.arrayFilter(unwrappedArray, function(item) {
                return options['includeDestroyed'] || item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']);
            });

            // Call setDomNodeChildrenFromArrayMapping, ignoring any observables unwrapped within (most likely from a callback function).
            // If the array items are observables, though, they will be unwrapped in executeTemplateForArrayItem and managed within setDomNodeChildrenFromArrayMapping.
            ko.dependencyDetection.ignore(ko.utils.setDomNodeChildrenFromArrayMapping, null, [targetNode, filteredArray, executeTemplateForArrayItem, options, activateBindingsCallback]);

        }, null, { disposeWhenNodeIsRemoved: targetNode });
    };

    var templateComputedDomDataKey = ko.utils.domData.nextKey();
    function disposeOldComputedAndStoreNewOne(element, newComputed) {
        var oldComputed = ko.utils.domData.get(element, templateComputedDomDataKey);
        if (oldComputed && (typeof(oldComputed.dispose) == 'function'))
            oldComputed.dispose();
        ko.utils.domData.set(element, templateComputedDomDataKey, (newComputed && newComputed.isActive()) ? newComputed : undefined);
    }

    ko.bindingHandlers['template'] = {
        'init': function(element, valueAccessor) {
            // Support anonymous templates
            var bindingValue = ko.utils.unwrapObservable(valueAccessor());
            if (typeof bindingValue == "string" || bindingValue['name']) {
                // It's a named template - clear the element
                ko.virtualElements.emptyNode(element);
            } else {
                // It's an anonymous template - store the element contents, then clear the element
                var templateNodes = ko.virtualElements.childNodes(element),
                    container = ko.utils.moveCleanedNodesToContainerElement(templateNodes); // This also removes the nodes from their current parent
                new ko.templateSources.anonymousTemplate(element)['nodes'](container);
            }
            return { 'controlsDescendantBindings': true };
        },
        'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
            var value = valueAccessor(),
                dataValue,
                options = ko.utils.unwrapObservable(value),
                shouldDisplay = true,
                templateComputed = null,
                templateName;

            if (typeof options == "string") {
                templateName = value;
                options = {};
            } else {
                templateName = options['name'];

                // Support "if"/"ifnot" conditions
                if ('if' in options)
                    shouldDisplay = ko.utils.unwrapObservable(options['if']);
                if (shouldDisplay && 'ifnot' in options)
                    shouldDisplay = !ko.utils.unwrapObservable(options['ifnot']);

                dataValue = ko.utils.unwrapObservable(options['data']);
            }

            if ('foreach' in options) {
                // Render once for each data point (treating data set as empty if shouldDisplay==false)
                var dataArray = (shouldDisplay && options['foreach']) || [];
                templateComputed = ko.renderTemplateForEach(templateName || element, dataArray, options, element, bindingContext);
            } else if (!shouldDisplay) {
                ko.virtualElements.emptyNode(element);
            } else {
                // Render once for this single data point (or use the viewModel if no data was provided)
                var innerBindingContext = ('data' in options) ?
                    bindingContext['createChildContext'](dataValue, options['as']) :  // Given an explitit 'data' value, we create a child binding context for it
                    bindingContext;                                                        // Given no explicit 'data' value, we retain the same binding context
                templateComputed = ko.renderTemplate(templateName || element, innerBindingContext, options, element);
            }

            // It only makes sense to have a single template computed per element (otherwise which one should have its output displayed?)
            disposeOldComputedAndStoreNewOne(element, templateComputed);
        }
    };

    // Anonymous templates can't be rewritten. Give a nice error message if you try to do it.
    ko.expressionRewriting.bindingRewriteValidators['template'] = function(bindingValue) {
        var parsedBindingValue = ko.expressionRewriting.parseObjectLiteral(bindingValue);

        if ((parsedBindingValue.length == 1) && parsedBindingValue[0]['unknown'])
            return null; // It looks like a string literal, not an object literal, so treat it as a named template (which is allowed for rewriting)

        if (ko.expressionRewriting.keyValueArrayContainsKey(parsedBindingValue, "name"))
            return null; // Named templates can be rewritten, so return "no error"
        return "This template engine does not support anonymous templates nested within its templates";
    };

    ko.virtualElements.allowedBindings['template'] = true;
})();

ko.exportSymbol('setTemplateEngine', ko.setTemplateEngine);
ko.exportSymbol('renderTemplate', ko.renderTemplate);
// Go through the items that have been added and deleted and try to find matches between them.
ko.utils.findMovesInArrayComparison = function (left, right, limitFailedCompares) {
    if (left.length && right.length) {
        var failedCompares, l, r, leftItem, rightItem;
        for (failedCompares = l = 0; (!limitFailedCompares || failedCompares < limitFailedCompares) && (leftItem = left[l]); ++l) {
            for (r = 0; rightItem = right[r]; ++r) {
                if (leftItem['value'] === rightItem['value']) {
                    leftItem['moved'] = rightItem['index'];
                    rightItem['moved'] = leftItem['index'];
                    right.splice(r, 1);         // This item is marked as moved; so remove it from right list
                    failedCompares = r = 0;     // Reset failed compares count because we're checking for consecutive failures
                    break;
                }
            }
            failedCompares += r;
        }
    }
};

ko.utils.compareArrays = (function () {
    var statusNotInOld = 'added', statusNotInNew = 'deleted';

    // Simple calculation based on Levenshtein distance.
    function compareArrays(oldArray, newArray, options) {
        // For backward compatibility, if the third arg is actually a bool, interpret
        // it as the old parameter 'dontLimitMoves'. Newer code should use { dontLimitMoves: true }.
        options = (typeof options === 'boolean') ? { 'dontLimitMoves': options } : (options || {});
        oldArray = oldArray || [];
        newArray = newArray || [];

        if (oldArray.length <= newArray.length)
            return compareSmallArrayToBigArray(oldArray, newArray, statusNotInOld, statusNotInNew, options);
        else
            return compareSmallArrayToBigArray(newArray, oldArray, statusNotInNew, statusNotInOld, options);
    }

    function compareSmallArrayToBigArray(smlArray, bigArray, statusNotInSml, statusNotInBig, options) {
        var myMin = Math.min,
            myMax = Math.max,
            editDistanceMatrix = [],
            smlIndex, smlIndexMax = smlArray.length,
            bigIndex, bigIndexMax = bigArray.length,
            compareRange = (bigIndexMax - smlIndexMax) || 1,
            maxDistance = smlIndexMax + bigIndexMax + 1,
            thisRow, lastRow,
            bigIndexMaxForRow, bigIndexMinForRow;

        for (smlIndex = 0; smlIndex <= smlIndexMax; smlIndex++) {
            lastRow = thisRow;
            editDistanceMatrix.push(thisRow = []);
            bigIndexMaxForRow = myMin(bigIndexMax, smlIndex + compareRange);
            bigIndexMinForRow = myMax(0, smlIndex - 1);
            for (bigIndex = bigIndexMinForRow; bigIndex <= bigIndexMaxForRow; bigIndex++) {
                if (!bigIndex)
                    thisRow[bigIndex] = smlIndex + 1;
                else if (!smlIndex)  // Top row - transform empty array into new array via additions
                    thisRow[bigIndex] = bigIndex + 1;
                else if (smlArray[smlIndex - 1] === bigArray[bigIndex - 1])
                    thisRow[bigIndex] = lastRow[bigIndex - 1];                  // copy value (no edit)
                else {
                    var northDistance = lastRow[bigIndex] || maxDistance;       // not in big (deletion)
                    var westDistance = thisRow[bigIndex - 1] || maxDistance;    // not in small (addition)
                    thisRow[bigIndex] = myMin(northDistance, westDistance) + 1;
                }
            }
        }

        var editScript = [], meMinusOne, notInSml = [], notInBig = [];
        for (smlIndex = smlIndexMax, bigIndex = bigIndexMax; smlIndex || bigIndex;) {
            meMinusOne = editDistanceMatrix[smlIndex][bigIndex] - 1;
            if (bigIndex && meMinusOne === editDistanceMatrix[smlIndex][bigIndex-1]) {
                notInSml.push(editScript[editScript.length] = {     // added
                    'status': statusNotInSml,
                    'value': bigArray[--bigIndex],
                    'index': bigIndex });
            } else if (smlIndex && meMinusOne === editDistanceMatrix[smlIndex - 1][bigIndex]) {
                notInBig.push(editScript[editScript.length] = {     // deleted
                    'status': statusNotInBig,
                    'value': smlArray[--smlIndex],
                    'index': smlIndex });
            } else {
                --bigIndex;
                --smlIndex;
                if (!options['sparse']) {
                    editScript.push({
                        'status': "retained",
                        'value': bigArray[bigIndex] });
                }
            }
        }

        // Set a limit on the number of consecutive non-matching comparisons; having it a multiple of
        // smlIndexMax keeps the time complexity of this algorithm linear.
        ko.utils.findMovesInArrayComparison(notInSml, notInBig, smlIndexMax * 10);

        return editScript.reverse();
    }

    return compareArrays;
})();

ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
(function () {
    // Objective:
    // * Given an input array, a container DOM node, and a function from array elements to arrays of DOM nodes,
    //   map the array elements to arrays of DOM nodes, concatenate together all these arrays, and use them to populate the container DOM node
    // * Next time we're given the same combination of things (with the array possibly having mutated), update the container DOM node
    //   so that its children is again the concatenation of the mappings of the array elements, but don't re-map any array elements that we
    //   previously mapped - retain those nodes, and just insert/delete other ones

    // "callbackAfterAddingNodes" will be invoked after any "mapping"-generated nodes are inserted into the container node
    // You can use this, for example, to activate bindings on those nodes.

    function mapNodeAndRefreshWhenChanged(containerNode, mapping, valueToMap, callbackAfterAddingNodes, index) {
        // Map this array value inside a dependentObservable so we re-map when any dependency changes
        var mappedNodes = [];
        var dependentObservable = ko.dependentObservable(function() {
            var newMappedNodes = mapping(valueToMap, index, ko.utils.fixUpContinuousNodeArray(mappedNodes, containerNode)) || [];

            // On subsequent evaluations, just replace the previously-inserted DOM nodes
            if (mappedNodes.length > 0) {
                ko.utils.replaceDomNodes(mappedNodes, newMappedNodes);
                if (callbackAfterAddingNodes)
                    ko.dependencyDetection.ignore(callbackAfterAddingNodes, null, [valueToMap, newMappedNodes, index]);
            }

            // Replace the contents of the mappedNodes array, thereby updating the record
            // of which nodes would be deleted if valueToMap was itself later removed
            mappedNodes.length = 0;
            ko.utils.arrayPushAll(mappedNodes, newMappedNodes);
        }, null, { disposeWhenNodeIsRemoved: containerNode, disposeWhen: function() { return !ko.utils.anyDomNodeIsAttachedToDocument(mappedNodes); } });
        return { mappedNodes : mappedNodes, dependentObservable : (dependentObservable.isActive() ? dependentObservable : undefined) };
    }

    var lastMappingResultDomDataKey = ko.utils.domData.nextKey();

    ko.utils.setDomNodeChildrenFromArrayMapping = function (domNode, array, mapping, options, callbackAfterAddingNodes) {
        // Compare the provided array against the previous one
        array = array || [];
        options = options || {};
        var isFirstExecution = ko.utils.domData.get(domNode, lastMappingResultDomDataKey) === undefined;
        var lastMappingResult = ko.utils.domData.get(domNode, lastMappingResultDomDataKey) || [];
        var lastArray = ko.utils.arrayMap(lastMappingResult, function (x) { return x.arrayEntry; });
        var editScript = ko.utils.compareArrays(lastArray, array, options['dontLimitMoves']);

        // Build the new mapping result
        var newMappingResult = [];
        var lastMappingResultIndex = 0;
        var newMappingResultIndex = 0;

        var nodesToDelete = [];
        var itemsToProcess = [];
        var itemsForBeforeRemoveCallbacks = [];
        var itemsForMoveCallbacks = [];
        var itemsForAfterAddCallbacks = [];
        var mapData;

        function itemMovedOrRetained(editScriptIndex, oldPosition) {
            mapData = lastMappingResult[oldPosition];
            if (newMappingResultIndex !== oldPosition)
                itemsForMoveCallbacks[editScriptIndex] = mapData;
            // Since updating the index might change the nodes, do so before calling fixUpContinuousNodeArray
            mapData.indexObservable(newMappingResultIndex++);
            ko.utils.fixUpContinuousNodeArray(mapData.mappedNodes, domNode);
            newMappingResult.push(mapData);
            itemsToProcess.push(mapData);
        }

        function callCallback(callback, items) {
            if (callback) {
                for (var i = 0, n = items.length; i < n; i++) {
                    if (items[i]) {
                        ko.utils.arrayForEach(items[i].mappedNodes, function(node) {
                            callback(node, i, items[i].arrayEntry);
                        });
                    }
                }
            }
        }

        for (var i = 0, editScriptItem, movedIndex; editScriptItem = editScript[i]; i++) {
            movedIndex = editScriptItem['moved'];
            switch (editScriptItem['status']) {
                case "deleted":
                    if (movedIndex === undefined) {
                        mapData = lastMappingResult[lastMappingResultIndex];

                        // Stop tracking changes to the mapping for these nodes
                        if (mapData.dependentObservable)
                            mapData.dependentObservable.dispose();

                        // Queue these nodes for later removal
                        nodesToDelete.push.apply(nodesToDelete, ko.utils.fixUpContinuousNodeArray(mapData.mappedNodes, domNode));
                        if (options['beforeRemove']) {
                            itemsForBeforeRemoveCallbacks[i] = mapData;
                            itemsToProcess.push(mapData);
                        }
                    }
                    lastMappingResultIndex++;
                    break;

                case "retained":
                    itemMovedOrRetained(i, lastMappingResultIndex++);
                    break;

                case "added":
                    if (movedIndex !== undefined) {
                        itemMovedOrRetained(i, movedIndex);
                    } else {
                        mapData = { arrayEntry: editScriptItem['value'], indexObservable: ko.observable(newMappingResultIndex++) };
                        newMappingResult.push(mapData);
                        itemsToProcess.push(mapData);
                        if (!isFirstExecution)
                            itemsForAfterAddCallbacks[i] = mapData;
                    }
                    break;
            }
        }

        // Call beforeMove first before any changes have been made to the DOM
        callCallback(options['beforeMove'], itemsForMoveCallbacks);

        // Next remove nodes for deleted items (or just clean if there's a beforeRemove callback)
        ko.utils.arrayForEach(nodesToDelete, options['beforeRemove'] ? ko.cleanNode : ko.removeNode);

        // Next add/reorder the remaining items (will include deleted items if there's a beforeRemove callback)
        for (var i = 0, nextNode = ko.virtualElements.firstChild(domNode), lastNode, node; mapData = itemsToProcess[i]; i++) {
            // Get nodes for newly added items
            if (!mapData.mappedNodes)
                ko.utils.extend(mapData, mapNodeAndRefreshWhenChanged(domNode, mapping, mapData.arrayEntry, callbackAfterAddingNodes, mapData.indexObservable));

            // Put nodes in the right place if they aren't there already
            for (var j = 0; node = mapData.mappedNodes[j]; nextNode = node.nextSibling, lastNode = node, j++) {
                if (node !== nextNode)
                    ko.virtualElements.insertAfter(domNode, node, lastNode);
            }

            // Run the callbacks for newly added nodes (for example, to apply bindings, etc.)
            if (!mapData.initialized && callbackAfterAddingNodes) {
                callbackAfterAddingNodes(mapData.arrayEntry, mapData.mappedNodes, mapData.indexObservable);
                mapData.initialized = true;
            }
        }

        // If there's a beforeRemove callback, call it after reordering.
        // Note that we assume that the beforeRemove callback will usually be used to remove the nodes using
        // some sort of animation, which is why we first reorder the nodes that will be removed. If the
        // callback instead removes the nodes right away, it would be more efficient to skip reordering them.
        // Perhaps we'll make that change in the future if this scenario becomes more common.
        callCallback(options['beforeRemove'], itemsForBeforeRemoveCallbacks);

        // Finally call afterMove and afterAdd callbacks
        callCallback(options['afterMove'], itemsForMoveCallbacks);
        callCallback(options['afterAdd'], itemsForAfterAddCallbacks);

        // Store a copy of the array items we just considered so we can difference it next time
        ko.utils.domData.set(domNode, lastMappingResultDomDataKey, newMappingResult);
    }
})();

ko.exportSymbol('utils.setDomNodeChildrenFromArrayMapping', ko.utils.setDomNodeChildrenFromArrayMapping);
ko.nativeTemplateEngine = function () {
    this['allowTemplateRewriting'] = false;
}

ko.nativeTemplateEngine.prototype = new ko.templateEngine();
ko.nativeTemplateEngine.prototype.constructor = ko.nativeTemplateEngine;
ko.nativeTemplateEngine.prototype['renderTemplateSource'] = function (templateSource, bindingContext, options) {
    var useNodesIfAvailable = !(ko.utils.ieVersion < 9), // IE<9 cloneNode doesn't work properly
        templateNodesFunc = useNodesIfAvailable ? templateSource['nodes'] : null,
        templateNodes = templateNodesFunc ? templateSource['nodes']() : null;

    if (templateNodes) {
        return ko.utils.makeArray(templateNodes.cloneNode(true).childNodes);
    } else {
        var templateText = templateSource['text']();
        return ko.utils.parseHtmlFragment(templateText);
    }
};

ko.nativeTemplateEngine.instance = new ko.nativeTemplateEngine();
ko.setTemplateEngine(ko.nativeTemplateEngine.instance);

ko.exportSymbol('nativeTemplateEngine', ko.nativeTemplateEngine);
(function() {
    ko.jqueryTmplTemplateEngine = function () {
        // Detect which version of jquery-tmpl you're using. Unfortunately jquery-tmpl
        // doesn't expose a version number, so we have to infer it.
        // Note that as of Knockout 1.3, we only support jQuery.tmpl 1.0.0pre and later,
        // which KO internally refers to as version "2", so older versions are no longer detected.
        var jQueryTmplVersion = this.jQueryTmplVersion = (function() {
            if (!jQueryInstance || !(jQueryInstance['tmpl']))
                return 0;
            // Since it exposes no official version number, we use our own numbering system. To be updated as jquery-tmpl evolves.
            try {
                if (jQueryInstance['tmpl']['tag']['tmpl']['open'].toString().indexOf('__') >= 0) {
                    // Since 1.0.0pre, custom tags should append markup to an array called "__"
                    return 2; // Final version of jquery.tmpl
                }
            } catch(ex) { /* Apparently not the version we were looking for */ }

            return 1; // Any older version that we don't support
        })();

        function ensureHasReferencedJQueryTemplates() {
            if (jQueryTmplVersion < 2)
                throw new Error("Your version of jQuery.tmpl is too old. Please upgrade to jQuery.tmpl 1.0.0pre or later.");
        }

        function executeTemplate(compiledTemplate, data, jQueryTemplateOptions) {
            return jQueryInstance['tmpl'](compiledTemplate, data, jQueryTemplateOptions);
        }

        this['renderTemplateSource'] = function(templateSource, bindingContext, options) {
            options = options || {};
            ensureHasReferencedJQueryTemplates();

            // Ensure we have stored a precompiled version of this template (don't want to reparse on every render)
            var precompiled = templateSource['data']('precompiled');
            if (!precompiled) {
                var templateText = templateSource['text']() || "";
                // Wrap in "with($whatever.koBindingContext) { ... }"
                templateText = "{{ko_with $item.koBindingContext}}" + templateText + "{{/ko_with}}";

                precompiled = jQueryInstance['template'](null, templateText);
                templateSource['data']('precompiled', precompiled);
            }

            var data = [bindingContext['$data']]; // Prewrap the data in an array to stop jquery.tmpl from trying to unwrap any arrays
            var jQueryTemplateOptions = jQueryInstance['extend']({ 'koBindingContext': bindingContext }, options['templateOptions']);

            var resultNodes = executeTemplate(precompiled, data, jQueryTemplateOptions);
            resultNodes['appendTo'](document.createElement("div")); // Using "appendTo" forces jQuery/jQuery.tmpl to perform necessary cleanup work

            jQueryInstance['fragments'] = {}; // Clear jQuery's fragment cache to avoid a memory leak after a large number of template renders
            return resultNodes;
        };

        this['createJavaScriptEvaluatorBlock'] = function(script) {
            return "{{ko_code ((function() { return " + script + " })()) }}";
        };

        this['addTemplate'] = function(templateName, templateMarkup) {
            document.write("<script type='text/html' id='" + templateName + "'>" + templateMarkup + "<" + "/script>");
        };

        if (jQueryTmplVersion > 0) {
            jQueryInstance['tmpl']['tag']['ko_code'] = {
                open: "__.push($1 || '');"
            };
            jQueryInstance['tmpl']['tag']['ko_with'] = {
                open: "with($1) {",
                close: "} "
            };
        }
    };

    ko.jqueryTmplTemplateEngine.prototype = new ko.templateEngine();
    ko.jqueryTmplTemplateEngine.prototype.constructor = ko.jqueryTmplTemplateEngine;

    // Use this one by default *only if jquery.tmpl is referenced*
    var jqueryTmplTemplateEngineInstance = new ko.jqueryTmplTemplateEngine();
    if (jqueryTmplTemplateEngineInstance.jQueryTmplVersion > 0)
        ko.setTemplateEngine(jqueryTmplTemplateEngineInstance);

    ko.exportSymbol('jqueryTmplTemplateEngine', ko.jqueryTmplTemplateEngine);
})();
}));
}());
})();

/*! knockout-secure-binding - v0.5.1 - 2014-10-07
 *  https://github.com/brianmhunt/knockout-secure-binding
 *  Copyright (c) 2013 - 2014 Brian M Hunt; License: MIT */
;(function(factory) {
    //AMD
    if (typeof rjs.define === 'function' && rjs.define.amd) {
        rjs.define('lib/knockout-secure-binding',["lib/knockout", "exports"], factory);
        //normal script tag
    } else {
        factory(ko);
    }
}(function(ko, exports, undefined) {
  var Identifier, Expression, Parser, Node;

  function value_of(item) {
    if (item instanceof Identifier || item instanceof Expression) {
      return item.get_value();
    }
    return item;
  }

  // The following are also in ko.*, but not exposed.
  function _object_map(source, mapping) {
    // ko.utils.objectMap
    if (!source) {
      return source;
    }
    var target = {};
    for (var prop in source) {
      if (source.hasOwnProperty(prop)) {
        target[prop] = mapping(source[prop], prop, source);
      }
    }
    return target;
  }

  // ko.virtualElements.virtualNodeBindingValue
  var commentNodesHaveTextProperty = document && document.createComment("test").text === "<!--test-->";
  var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko(?:\s+([\s\S]+))?\s*-->$/ : /^\s*ko(?:\s+([\s\S]+))?\s*$/;

  function _virtualNodeBindingValue(node) {
    var regexMatch = (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(startCommentRegex);
    return regexMatch ? regexMatch[1] : null;
  }



Identifier = (function () {
  function Identifier(parser, token, dereferences) {
    this.token = token;
    this.dereferences = dereferences;
    this.parser = parser;
  }

  /**
   * Return the value of the given
   *
   * @param  {Object} parent  (optional) source of the identifier e.g. for
   *                          membership. e.g. `a.b`, one would pass `a` in as
   *                          the parent when calling lookup_value for `b`.
   * @return {Mixed}          The value of the token for this Identifier.
   */
  Identifier.prototype.lookup_value = function (parent) {
    var token = this.token,
        parser = this.parser,
        $context = parser.context,
        $data = $context.$data || {},
        globals = parser.globals || {};

    if (parent) {
      return value_of(parent)[token];
    }

    // short circuits
    switch (token) {
      case '$element': return parser.node;
      case '$context': return $context;
      case '$data': return $data;
      default:
    }

    return $data[token] || $context[token] || globals[token];
  };

  /**
   * Apply all () and [] functions on the identifier to the lhs value e.g.
   * a()[3] has deref functions that are essentially this:
   *     [_deref_call, _deref_this where this=3]
   *
   * @param  {mixed} value  Should be an object.
   * @return {mixed}        The dereferenced value.
   */
  Identifier.prototype.dereference = function (value) {
    var member,
        refs = this.dereferences || [],
        parser = this.parser,
        $context = parser.context || {},
        $data = $context.$data || {},
        self = { // top-level `this` in function calls
          $context: $context,
          $data: $data,
          globals: parser.globals || {},
          $element: parser.node
        },
        last_value,  // becomes `this` in function calls to object properties.
        i, n;

    for (i = 0, n = refs.length; i < n; ++i) {
      member = refs[i];
      if (member === true) {
        value = value.call(last_value || self);
        last_value = value;
      } else {
        last_value = value;
        value = value[value_of(member)];
      }
    }
    return value;
  };

  /**
   * Return the value as one would get it from the top-level i.e.
   * $data.token/$context.token/globals.token; this does not return intermediate
   * values on a chain of members i.e. $data.hello.there -- requesting the
   * Identifier('there').value will return $data/$context/globals.there.
   *
   * This will dereference using () or [arg] member.
   * @param  {object | Identifier | Expression} parent
   * @return {mixed}  Return the primitive or an accessor.
   */
  Identifier.prototype.get_value = function (parent) {
    return this.dereference(this.lookup_value(parent));
  };

  /**
   * Set the value of the Identifier.
   *
   * @param {Mixed} new_value The value that Identifier is to be set to.
   */
  Identifier.prototype.set_value = function (new_value) {
    var parser = this.parser,
        $context = parser.context,
        $data = $context.$data || {},
        globals = parser.globals || {},
        refs = this.dereferences || [],
        leaf = this.token,
        i, n, root;

    if (Object.hasOwnProperty.call($data, leaf)) {
      root = $data;
    } else if (Object.hasOwnProperty.call($context, leaf)) {
      root = $context;
    } else if (Object.hasOwnProperty.call(globals, leaf)) {
      root = globals;
    } else {
      throw new Error("Identifier::set_value -- " +
        "The property '" + leaf + "' does not exist " +
        "on the $data, $context, or globals.");
    }

    // Degenerate case. {$data|$context|global}[leaf] = something;
    n = refs.length;
    if (n === 0) {
      root[leaf] = new_value;
    }

    // First dereference is {$data|$context|global}[token].
    root = root[leaf];

    // We cannot use this.dereference because that gives the leaf; to evoke
    // the ES5 setter we have to call `obj[leaf] = new_value`
    for (i = 0; i < n - 1; ++i) {
      leaf = refs[i];
      if (leaf === true) {
        root = root();
      } else {
        root = root[value_of(leaf)];
      }
    }

    // We indicate that a dereference is a function when it is `true`.
    if (refs[i] === true) {
      throw new Error("Cannot assign a value to a function.");
    }

    // Call the setter for the leaf.
    root[value_of(refs[i])] = new_value;
  };

  return Identifier;
})();

/**
 * Determine if a character is a valid item in an identifier.
 * Note that we do not check whether the first item is a number, nor do we
 * support unicode identifiers here.
 *
 * See: http://docstore.mik.ua/orelly/webprog/jscript/ch02_07.htm
 * @param  {String}  ch  The character
 * @return {Boolean}     True if [A-Za-z0-9_]
 */
function is_identifier_char(ch) {
  return (ch >= 'A' && ch <= 'Z') ||
         (ch >= 'a' && ch <= 'z') ||
         (ch >= '0' && ch <= 9) ||
          ch === '_' || ch === '$';
}


Node = (function () {
  function Node(lhs, op, rhs) {
    this.lhs = lhs;
    this.op = op;
    this.rhs = rhs;
  }

  var operators =  {
    // unary
    '!': function not(a, b) { return !b; },
    '!!': function notnot(a, b) { return !!b; },
    // mul/div
    '*': function mul(a, b) { return a * b; },
    '/': function div(a, b) { return a / b; },
    '%': function mod(a, b) { return a % b; },
    // sub/add
    '+': function add(a, b) { return a + b; },
    '-': function sub(a, b) { return a - b; },
    // relational
    '<': function lt(a, b) { return a < b; },
    '<=': function le(a, b) { return a <= b; },
    '>': function gt(a, b) { return a > b; },
    '>=': function ge(a, b) { return a >= b; },
    //    TODO: 'in': function (a, b) { return a in b; },
    //    TODO: 'instanceof': function (a, b) { return a instanceof b; },
    // equality
    '==': function equal(a, b) { return a === b; },
    '!=': function ne(a, b) { return a !== b; },
    '===': function sequal(a, b) { return a === b; },
    '!==': function sne(a, b) { return a !== b; },
    // bitwise
    '&': function bit_and(a, b) { return a & b; },
    '^': function xor(a, b) { return a ^ b; },
    '|': function bit_or(a, b) { return a | b; },
    // logic
    '&&': function logic_and(a, b) { return a && b; },
    '||': function logic_or(a, b) { return a || b; },
  };

  /* In order of precedence, see:
  https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table
  */
    // logical not
  operators['!'].precedence = 4;
  operators['!!'].precedence = 4; // explicit double-negative
    // multiply/divide/mod
  operators['*'].precedence = 5;
  operators['/'].precedence = 5;
  operators['%'].precedence = 5;
    // add/sub
  operators['+'].precedence = 6;
  operators['-'].precedence = 6;
    // relational
  operators['<'].precedence = 8;
  operators['<='].precedence = 8;
  operators['>'].precedence = 8;
  operators['>='].precedence = 8;
  // operators['in'].precedence = 8;
  // operators['instanceof'].precedence = 8;
    // equality
  operators['=='].precedence = 9;
  operators['!='].precedence = 9;
  operators['==='].precedence = 9;
  operators['!=='].precedence = 9;
    // bitwise
  operators['&'].precedence = 10;
  operators['^'].precedence = 11;
  operators['|'].precedence = 12;
    // logic
  operators['&&'].precedence = 13;
  operators['||'].precedence = 14;

  Node.operators = operators;


  Node.prototype.get_leaf_value = function (leaf, member_of) {
    if (typeof(leaf) === 'function') {
      // Expressions on observables are nonsensical, so we unwrap any
      // function values (e.g. identifiers).
      return ko.unwrap(leaf());
    }

    // primitives
    if (typeof(leaf) !== 'object') {
      return member_of ? member_of[leaf] : leaf;
    }

    // Identifiers and Expressions
    if (leaf instanceof Identifier || leaf instanceof Expression) {
      // lhs is passed in as the parent of the leaf. It will be defined in
      // cases like a.b.c as 'a' for 'b' then as 'b' for 'c'.
      return ko.unwrap(leaf.get_value(member_of));
    }

    if (leaf instanceof Node) {
      return leaf.get_node_value(member_of);
    }

    throw new Error("Invalid type of leaf node: " + leaf);
  };

  /**
   * Return a function that calculates and returns an expression's value
   * when called.
   * @param  {array} ops  The operations to perform
   * @return {function}   The function that calculates the expression.
   *
   * Exported for testing.
   */
  Node.prototype.get_node_value = function () {
    return this.op(this.get_leaf_value(this.lhs),
                   this.get_leaf_value(this.rhs));
  };

  return Node;
})();

Expression = (function () {
  function Expression(nodes) {
    this.nodes = nodes;
    this.root = this.build_tree(nodes);
  }

  // Exports for testing.
  Expression.operators = Node.operators;
  Expression.Node = Node;

  /**
   *  Convert an array of nodes to an executable tree.
   *  @return {object} An object with a `lhs`, `rhs` and `op` key, corresponding
   *                      to the left hand side, right hand side, and
   *                      operation function.
   */
  Expression.prototype.build_tree = function (nodes) {
    var root,
        leaf,
        op,
        value;

    // console.log("build_tree", nodes.slice(0))

    // primer
    leaf = root = new Node(nodes.shift(), nodes.shift(), nodes.shift());

    while (nodes) {
      op = nodes.shift();
      value = nodes.shift();
      if (!op) {
        break;
      }
      if (op.precedence > root.op.precedence) {
        // rebase
        root = new Node(root, op, value);
        leaf = root;
      } else {
        leaf.rhs = new Node(leaf.rhs, op, value);
        leaf = leaf.rhs;
      }
    }
    // console.log("tree", root)
    return root;
  }; // build_tree

  Expression.prototype.get_value = function () {
    if (!this.root) {
      this.root = this.build_tree(this.nodes);
    }
    return this.root.get_node_value();
  };

  return Expression;
})();


/**
 * Originally based on (public domain):
 * https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js
 */
/* jshint -W083 */
Parser = (function () {
  var escapee = {
    "'": "'",
    '"':  '"',
    '\\': '\\',
    '/':  '/',
    b:    '\b',
    f:    '\f',
    n:    '\n',
    r:    '\r',
    t:    '\t'
  },
    operators = Expression.operators;

  /**
   * Construct a new Parser instance with new Parser(node, context)
   * @param {Node} node    The DOM element from which we parsed the
   *                         content.
   * @param {object} context The Knockout context.
   * @param {object} globals An object containing any desired globals.
   */
  function Parser(node, context, globals) {
    this.node = node;
    this.context = context;
    this.globals = globals || {};
  }

  // exported for testing.
  Parser.Expression = Expression;
  Parser.Identifier = Identifier;
  Parser.Node = Node;

  Parser.prototype.white = function () {
    var ch = this.ch;
    while (ch && ch <= ' ') {
      ch = this.next();
    }
    return ch;
  };

  Parser.prototype.next = function (c) {
    if (c && c !== this.ch) {
      this.error("Expected '" + c + "' but got '" + this.ch + "'");
    }
    this.ch = this.text.charAt(this.at);
    this.at += 1;
    return this.ch;
  };

  Parser.prototype.error = function (m) {
      // console.trace()
      throw {
          name:    'SyntaxError',
          message: m,
          at:      this.at,
          text:    this.text
      };
  };

  Parser.prototype.name = function () {
    // A name of a binding
    var name = '';
    this.white();

    ch = this.ch;

    while (ch) {
      if (ch === ':' || ch <= ' ' || ch === ',') {
          return name;
      }
      name += ch;
      ch = this.next();
    }

    return name;
  };

  Parser.prototype.number = function () {
    var number,
        string = '',
        ch = this.ch;

    if (ch === '-') {
      string = '-';
      ch = this.next('-');
    }
    while (ch >= '0' && ch <= '9') {
      string += ch;
      ch = this.next();
    }
    if (ch === '.') {
      string += '.';
      ch = this.next();
      while (ch && ch >= '0' && ch <= '9') {
        string += ch;
        ch = this.next();
      }
    }
    if (ch === 'e' || ch === 'E') {
      string += ch;
      ch = this.next();
      if (ch === '-' || ch === '+') {
        string += ch;
        ch = this.next();
      }
      while (ch >= '0' && ch <= '9') {
        string += ch;
        ch = this.next();
      }
    }
    number = +string;
    if (!isFinite(number)) {
      error("Bad number");
    } else {
      return number;
    }
  };

  /**
   * Add a property to 'object' that equals the given value.
   * @param  {Object} object The object to add the value to.
   * @param  {String} key    object[key] is set to the given value.
   * @param  {mixed}  value  The value, may be a primitive or a function. If a
   *                         function it is unwrapped as a property.
   */
  Parser.prototype.object_add_value = function (object, key, value) {
    if (value instanceof Identifier || value instanceof Expression) {
      Object.defineProperty(object, key, {
        get: function () {
          return value.get_value();
        },
        enumerable: true,
      });
    } else {
      // primitives
      object[key] = value;
    }
  };

  Parser.prototype.object = function () {
    var key,
        object = {},
        ch = this.ch;

    if (ch === '{') {
      this.next('{');
      ch = this.white();
      if (ch === '}') {
        ch = this.next('}');
        return object;
      }
      while (ch) {
        if (ch === '"' || ch === "'") {
          key = this.string();
        } else {
          key = this.name();
        }
        this.white();
        ch = this.next(':');
        if (Object.hasOwnProperty.call(object, key)) {
          this.error('Duplicate key "' + key + '"');
        }

        this.object_add_value(object, key, this.expression());

        ch = this.white();
        if (ch === '}') {
          ch = this.next('}');
          return object;
        }

        this.next(',');
        ch = this.white();
      }
    }
    this.error("Bad object");
  };


  /**
   * Read up to delim and return the string
   * @param  {string} delim The delimiter, either ' or "
   * @return {string}       The string read.
   */
  Parser.prototype.read_string = function (delim) {
    var string = '',
        hex,
        i,
        uffff,
        ch = this.next();

    while (ch) {
      if (ch === delim) {
        ch = this.next();
        return string;
      }
      if (ch === '\\') {
        ch = this.next();
        if (ch === 'u') {
          uffff = 0;
          for (i = 0; i < 4; i += 1) {
            hex = parseInt(ch = this.next(), 16);
            if (!isFinite(hex)) {
              break;
            }
            uffff = uffff * 16 + hex;
          }
          string += String.fromCharCode(uffff);
        } else if (typeof escapee[ch] === 'string') {
          string += escapee[ch];
        } else {
          break;
        }
      } else {
        string += ch;
      }
      ch = this.next();
    }

    this.error("Bad string");
  };

  Parser.prototype.string = function () {
    var ch = this.ch;
    if (ch === '"') {
      return this.read_string('"');
    } else if (ch === "'") {
      return this.read_string("'");
    }

    this.error("Bad string");
  };

  Parser.prototype.array = function () {
    var array = [],
        ch = this.ch;
    if (ch === '[') {
      ch = this.next('[');
      this.white();
      if (ch === ']') {
        ch = this.next(']');
        return array;
      }
      while (ch) {
        array.push(this.expression());
        ch = this.white();
        if (ch === ']') {
          ch = this.next(']');
          return array;
        }
        this.next(',');
        ch = this.white();
      }
    }
    this.error("Bad array");
  };

  Parser.prototype.value = function () {
    var ch;
    this.white();
    ch = this.ch;
    switch (ch) {
      case '{': return this.object();
      case '[': return this.array();
      case '"': case "'": return this.string();
      case '-': return this.number();
      default:
      return ch >= '0' && ch <= '9' ? this.number() : this.identifier();
    }
  };

  /**
   * Get the function for the given operator.
   * A `.precedence` value is added to the function, with increasing
   * precedence having a higher number.
   * @return {function} The function that performs the infix operation
   */
  Parser.prototype.operator = function () {
    var op = '',
        op_fn,
        ch = this.white();

    while (ch) {
      if (is_identifier_char(ch) || ch <= ' ' || ch === '' ||
          ch === '"' || ch === "'" || ch === '{' || ch === '[' ||
          ch === '(') {
        break;
      }
      op += ch;
      ch = this.next();
    }

    if (op !== '') {
      op_fn = operators[op];

      if (!op_fn) {
        this.error("Bad operator: '" + op + "'.");
      }
    }

    return op_fn;
  };

  /**
   * Parse an expression – builds an operator tree, in something like
   * Shunting-Yard.
   *   See: http://en.wikipedia.org/wiki/Shunting-yard_algorithm
   *
   * @return {function}   A function that computes the value of the expression
   *                      when called or a primitive.
   */
  Parser.prototype.expression = function () {
    var root,
        nodes = [],
        node_value,
        ch = this.white();

    while (ch) {
      // unary prefix operators
      op = this.operator();
      if (op) {
        nodes.push(undefined);  // padding.
        nodes.push(op);
      }

      if (ch === '(') {
        this.next();
        nodes.push(this.expression());
        this.next(')');
      } else {
        node_value = this.value();
        nodes.push(node_value);
      }
      ch = this.white();
      if (ch === ':' || ch === '}' || ch === ',' || ch === ']' ||
          ch === ')' || ch === '') {
        break;
      }
      // infix operators
      op = this.operator();
      if (op) {
        nodes.push(op);
      }
      ch = this.white();
    }

    if (nodes.length === 0) {
      return undefined;
    }

    if (nodes.length === 1) {
      return nodes[0];
    }

    return new Expression(nodes);
  };

  /**
   * A dereference applies to an identifer, being either a function
   * call "()" or a membership lookup with square brackets "[member]".
   * @return {fn or undefined}  Dereference function to be applied to the
   *                            Identifier
   */
  Parser.prototype.dereference = function () {
    var member,
        ch = this.white();

    while (ch) {
      if (ch === '(') {
        // a() function call
        this.next('(');
        this.white();
        this.next(')');
        return true;  // in Identifier::dereference we check this
      } else if (ch === '[') {
        // a[x] membership
        this.next('[');
        member = this.expression();
        this.white();
        this.next(']');

        return member;
      } else if (ch === '.') {
        // a.x membership
        member = '';
        this.next('.');
        ch = this.white();
        while (ch) {
          if (!is_identifier_char(ch)) {
            break;
          }
          member += ch;
          ch = this.next();
        }
        return member;
      } else {
        break;
      }
      ch = this.white();
    }
    return;
  };

  Parser.prototype.identifier = function () {
    var token = '', ch, deref, dereferences = [];
    ch = this.white();
    while (ch) {
      if (!is_identifier_char(ch)) {
        break;
      }
      token += ch;
      ch = this.next();
    }
    switch (token) {
      case 'true': return true;
      case 'false': return false;
      case 'null': return null;
      // we use `void 0` because `undefined` can be redefined.
      case 'undefined': return void 0;
      default:
    }
    while (ch) {
      deref = this.dereference();
      if (deref !== undefined) {
        dereferences.push(deref);
      } else {
        break;
      }
    }
    return new Identifier(this, token, dereferences);
  };

  Parser.prototype.bindings = function () {
    var key,
        bindings = {},
        sep,
        ch = this.ch;

    while (ch) {
      key = this.name();
      sep = this.white();
      if (!sep || sep === ',') {
        if (sep) {
          ch = this.next(',');
        } else {
          ch = '';
        }
        // A "bare" binding e.g. "text"; substitute value of 'null'
        // so it becomes "text: null".
        bindings[key] = null;
      } else {
        ch = this.next(':');
        bindings[key] = this.expression();
        this.white();
        if (this.ch) {
          ch = this.next(',');
        } else {
          ch = '';
        }
      }
    }
    return bindings;
  };

/**
 * Convert result[name] from a value to a function (i.e. `valueAccessor()`)
 * @param  {object} result [Map of top-level names to values]
 * @return {object}        [Map of top-level names to functions]
 *
 * Accessors may be one of constAccessor (below), identifierAccessor or
 * expressionAccessor.
 */
  Parser.prototype.convert_to_accessors = function (result) {
    var propertyWriters = {};
    ko.utils.objectForEach(result, function (name, value) {
      if (value instanceof Identifier) {
        // use _twoWayBindings so the binding can update Identifier
        // See http://stackoverflow.com/questions/21580173
        result[name] = function () {
          return value.get_value();
        };

        if (ko.expressionRewriting._twoWayBindings[name]) {
          propertyWriters[name] = function(new_value) {
            value.set_value(new_value);
          };
        }
      } else if (value instanceof Expression) {
        result[name] = function expressionAccessor() {
          return value.get_value();
        };
      } else if (typeof(value) != 'function') {
        result[name] = function constAccessor() {
          return value;
        };
      }
    });

    if (Object.keys(propertyWriters).length > 0) {
      result._ko_property_writers = function () {
        return propertyWriters;
      };
    }

    return result;
  };

  /**
   * Get the bindings as name: accessor()
   * @param  {string} source The binding string to parse.
   * @return {object}        Map of name to accessor function.
   */
  Parser.prototype.parse = function (source) {
    this.text = (source || '').trim();
    this.at = 0;
    this.ch = ' ';

    if (!this.text) {
      return null;
    }

    result = this.bindings();

    this.white();
    if (this.ch) {
      this.error("Syntax Error");
    }

    return this.convert_to_accessors(result);
  };

  return Parser;
})();


// See knockout/src/binding/bindingProvider.js

function secureBindingsProvider(options) {
    var existingProvider = new ko.bindingProvider();
    options = options || {};

    // override the attribute
    this.attribute = options.attribute || "data-sbind";

    // do we bind to the ko: virtual elements
    this.noVirtualElements = options.noVirtualElements || false;

    // set globals
    this.globals = options.globals || {};

    // the binding classes -- defaults to ko bindingsHandlers
    this.bindings = options.bindings || ko.bindingHandlers;

    // Cache the result of parsing binding strings.
    // TODO
    // this.cache = {};
}

function registerBindings(newBindings) {
    ko.utils.extend(this.bindings, newBindings);
}

function nodeHasBindings(node) {
    var value;
    if (node.nodeType === node.ELEMENT_NODE) {
        return node.getAttribute(this.attribute) ||
            ko.components.getComponentNameForNode(node);
    } else if (node.nodeType === node.COMMENT_NODE) {
        if (this.noVirtualElements) {
            return false;
        }
        value = ("" + node.nodeValue || node.text).trim();
        // See also: knockout/src/virtualElements.js
        return value.indexOf("ko ") === 0;
    }
}

function getBindingsString(node) {
    switch (node.nodeType) {
        case node.ELEMENT_NODE:
            return node.getAttribute(this.attribute);
        case node.COMMENT_NODE:
            return _virtualNodeBindingValue(node);
        default:
            return null;
    }
}

function nodeParamRawMapper(param) {
    return param();
}

function nodeParamsToObject(node, parser) {
    var accessors = parser.parse(node.getAttribute('params'));
    if (!accessors || Object.keys(accessors).length === 0) {
        return {$raw: {}};
    }
    var $raw = _object_map(accessors, nodeParamRawMapper);
    var params = _object_map($raw, ko.unwrap);
    if (!params.hasOwnProperty('$raw')) {
        params.$raw = $raw;
    }
    return params;
}


// Note we do not seem to need both getBindings and getBindingAccessors; just
// the latter appears to suffice.
//
// Return the name/valueAccessor pairs.
// (undocumented replacement for getBindings)
// see https://github.com/knockout/knockout/pull/742
function getBindingAccessors(node, context) {
    var bindings = {},
        component_name,
        parser = new Parser(node, context, this.globals),
        sbind_string = this.getBindingsString(node);

    if (node.nodeType === node.ELEMENT_NODE) {
        component_name = ko.components.getComponentNameForNode(node);
    }

    if (sbind_string) {
        bindings = parser.parse(sbind_string || '');
    }

    // emulate ko.components.addBindingsForCustomElement(bindings, node,
    //     context, true);
    if (component_name) {
        if (bindings.component) {
            throw new Error("Cannot use a component binding on custom elements");
        }
        var componentBindingValue = {
            'name': component_name,
            'params': nodeParamsToObject(node, parser),
        };
        bindings.component =  function() { return componentBindingValue; };
    }

    return bindings;
}


ko.utils.extend(secureBindingsProvider.prototype, {
    registerBindings: registerBindings,
    nodeHasBindings: nodeHasBindings,
    getBindingAccessors: getBindingAccessors,
    getBindingsString: getBindingsString,
    nodeParamsToObject: nodeParamsToObject,
    Parser: Parser
});

    if (!exports) {
        ko.secureBindingsProvider = secureBindingsProvider;
    }

    return secureBindingsProvider;
}));

rjs.define('gui/controls/ComboBox',["lib/knockout", "lib/knockout-secure-binding"], function (ko, ksb) {
  /**
   * Combo box for setup view.
   * Unlike the SSSP / LG the ChromeOS uses a <select> element and knockout secure bindings
   * @param {[type]} id           [description]
   * @param {[type]} data         [description]
   * @param {[type]} itemSelected [description]
   */
  function ComboBox(id, data, itemSelected, label, styles, disabled) {
    var self = this;
    self.id = id;

    var viewHtml = "<div id=" + self.id + ' style="width:100%">';
    viewHtml += '<div style="margin: 0 auto;">';
    viewHtml +=
      "<select style=\"margin: 10px; padding: 10px; width: 500px;\" data-bind=\"options: options, optionsText: 'displayName', optionsValue: 'id', value: selectedOption\"></select>";
    viewHtml += "</div>";
    viewHtml += "</div>";
    self.html = viewHtml;

    self.data = data;
    self.options = ko.observableArray(data);
    self.selectedOption = ko.observable(itemSelected.id);
    self.selectedObject = itemSelected;

    /**
     * Add to canvas
     * @return {[type]} [description]
     */
    self.addedToCanvas = function () {
      // Show all options, more restricted setup than the Knockout regular binding.
      var options = {
        attribute: "data-bind", // defaults to data-sbind which breaks this!
        globals: {}, // no globals
        bindings: ko.bindingHandlers, // still use default binding handlers
        noVirtualElements: true, // no virtual elements
      };

      // Apply Bindings (Use Secure Bindings)
      ko.bindingProvider.instance = new ksb(options);
      ko.applyBindings(self, $("#" + self.id)[0]);

      // Log Selection changes
      $("#" + self.id).change(function () {
        if (getObjectById(self.selectedOption()) != null) {
          self.selectedObject = getObjectById(self.selectedOption());
        }
        logger.debug("Selected: " + self.selectedOption());
      });
    };

    /**
     * [getObjectById description]
     * @param  {[type]} id [description]
     * @return {[type]}    [description]
     */
    function getObjectById(id) {
      for (i = 0; i < data.length; i++) {
        if (data[i].id === id) return data[i];
      }
    }

    /**
     * Get the selected item
     * @return {[type]} [description]
     */
    self.selectedItem = function () {
      return self.selectedObject;
    };

    /**
     * hoverOn
     * @return {[type]} [description]
     */
    self.hoverOn = function () {
      // NOT IMPLEMENTED
    };

    /**
     * hoverOff
     * @return {[type]} [description]
     */
    self.hoverOff = function () {
      // NOT IMPLEMENTED
    };

    /**
     * select
     * @return {[type]} [description]
     */
    self.select = function () {
      // NOT IMPLEMENTED
    };

    /**
     * deselect
     * @return {[type]} [description]
     */
    self.deselect = function () {
      // NOT IMPLEMENTED
    };

    /**
     * handleKeyPress
     * @param  {[type]} keyCode [description]
     * @return {[type]}         [description]
     */
    self.handleKeyPress = function (keyCode) {
      // NOT IMPLEMENTED
    };
  }
  return ComboBox;
});

rjs.define('gui/controls/Button',[], function() {
	/**
	 * [Button description]
	 * @param {[type]} id        [description]
	 * @param {[type]} label     [description]
	 * @param {[type]} callbacks [description]
	 */
	function Button(id, label, callbacks) {
		var self = this;
		self.id = id;
		self.label = label;
		self.callbacks = callbacks;
		self.hasFocus = false;

		self.html = '<div id="' + self.id + '" class="button" tabindex="0">' + self.label + '</div>';

		/**
		 * [select description]
		 * @return {[type]} [description]
		 */
		self.select = function() {
			self.hasFocus = true;

			$('#' + self.id).addClass('selected');
		};

		/**
		 * [deselect description]
		 * @return {[type]} [description]
		 */
		self.deselect = function() {
			self.hasFocus = false;

			$('#' + self.id).removeClass('selected');
		};
		
		/**
		 * [hoverOn description]
		 * @return {[type]} [description]
		 */
		self.hoverOn = function() {
			$('#' + self.id).addClass('hover');
		};
		
		/**
		 * [hoverOff description]
		 * @return {[type]} [description]
		 */
		self.hoverOff = function() {
			$('#' + self.id).removeClass('hover');
		};

		/**
		 * [addedToCanvas description]
		 * @return {[type]} [description]
		 */
		self.addedToCanvas = function() {
		};

		/**
		 * [handleKeyPress description]
		 * @param  {[type]} keyCode [description]
		 * @return {[type]}         [description]
		 */
		self.handleKeyPress = function(keyCode) {
			// NOT Implemented in generic app
		};
	}

	return Button;
});

rjs.define('gui/controls/CheckBox',[], function () {
  /**
   * Checkbox box for setup view.
   */
  function CheckBox(id, checked, label, styles, onChangeCallback, arguments, isDisabled, selectedAudioChannel) {
    var self = this;
    self.id = id;
    self.checked = checked;
    self.hasFocus = false;
    self.onChangeCallback = onChangeCallback;
    self.arguments = arguments;
    self.isDisabled = isDisabled;

    var viewHtml = `<input type="checkbox" id="${
      self.id
    }" style="${styles}" ${isDisabled || (selectedAudioChannel !== 'hdmi' && selectedAudioChannel !== undefined) ? "disabled" : ""}>`;
    self.html = viewHtml;

    // Ensures discreet audio checkboxes are not just disabled, but also unticked if user has set global audio output channel to be anything other than 'HDMI' 
    if (selectedAudioChannel !== 'hdmi' && selectedAudioChannel !== undefined) {
        self.checked = false;
    }

    //* Sets checked/unchecked state of Checkbox to match its property value
    //* Adds event listener(s)
    self.addedToCanvas = function () {
      if (self.checked) {
        self.check();
      } else {
        self.uncheck();
      }

      //* Listen for Enter and Spacebar keypress events
      $("#" + self.id).keyup(function (event) {
        switch (event.which) {
          case 13:
          case 32:
            // Added to prevent the default JavaScript spacebar event handler behaviour (otherwise the keystroke will fire the toggling twice)
            event.preventDefault();
            self.toggle();
            if (!id.endsWith('AudioCheckBox') && onChangeCallback) {
            self.onChangeCallback(self.arguments);
            };
            break;
          default:
            //Do nothing
            break;
        }
      });
    };

    //* Toggles the DOM checkbox element between checked and unchecked
    self.toggle = function () {
      $("#" + self.id)[0].checked = !$("#" + self.id)[0].checked;
      self.checked = !self.checked;
    };

    //* Calls either check() or uncheck(), depending on state value passed in
    self.setState = function (state) {
      if (state) {
        self.check();
      } else {
        self.uncheck();
      }
    };

    //* Explicitly checks the checkbox
    self.check = function () {
      $("#" + self.id)[0].checked = true;
      self.checked = true;
    };

    //* Explicitly unchecks the checkbox
    self.uncheck = function () {
      $("#" + self.id)[0].checked = false;
      self.checked = false;
    };

    //* Enables / disables checkbox, based on the value passed in
    self.enabled = (enabled) => {
      if (enabled) {
        $(`#${self.id}`).prop("readonly", false);
        $(`#${self.id}`).prop("disabled", false);
      } else {
        $(`#${self.id}`).prop("readonly", true);
        $(`#${self.id}`).prop("disabled", true);
      }
    };

    //* Returns the 'checked' value of the form control element
    self.isChecked = function () {
      return self.checked;
    };
  }
  return CheckBox;
});

rjs.define('models/RotationCollection',[],function() {
	var rotations = [{
		displayName : 'Landscape',
		angle:0,
		id : 0
	}, {
		displayName : 'Portrait',
		angle:90,
		id : 90
	}, {
		displayName : 'Portrait (270)',
		angle:270,
		id : 270
	}];

    return rotations;
});

rjs.define('gui/controls/TextBox',[], function() {
	/**
	 * @param {[type]} id           [description]
	 * @param {[type]} data         [description]
	 * @param {[type]} itemSelected [description]
	 */
	function TextBox(id, defaultValue, type, min, max, onChangeHandler, disabled) {

		var self = this;
		self.id = id;
		self.disabled = disabled;

		if(!type)
			type = "text";

		var viewHtml =
      min !== undefined && max !== undefined && type == "number"
        ? '<div id="' +
          self.id +
          '" style="width:100%"> <input type="number" min="' +
          min +
          '" max="' +
          max +
          '" id="' +
          self.id +
          '_textBox" style="font-size:18px; margin: 10px; padding: 10px; width: 100px;"></input>'
        : '<div id="' +
          self.id +
          '" style="width:100%"> <input type="' +
          type +
          '" id="' +
          self.id +
          '_textBox" style="font-size:18px; margin: 10px; padding: 10px; width: 100px;"></input>';
	
		viewHtml += '</div>';
		self.html = viewHtml;

		self.value = defaultValue;

		/**
		 * Add to canvas
		 * @return {[type]} [description]
		 */
		self.addedToCanvas = function() {

			$( '#'+self.id + "_textBox" ).val(self.value);

			if(self.disabled) {
				self.enabled(false);
			}

			$( '#'+self.id + "_textBox" ).change(function() {
				self.value = $( '#'+self.id + "_textBox" ).val();
				if (onChangeHandler) {
					onChangeHandler(self.value);
				}
			});
		};

		self.enabled = function(enabled)
		{
			if (enabled)
			{
				$('#'+self.id + "_textBox").prop("readonly", false);
				$('#'+self.id + "_textBox").prop("disabled", false);
			}
			else
			{
				$('#'+self.id + "_textBox").prop("readonly", true);
				$('#'+self.id + "_textBox").prop("disabled", true);
			}
		};
		
		/**
		 * [getObjectById description]
		 * @param  {[type]} id [description]
		 * @return {[type]}    [description]
		 */
		function getObjectById(id) {

			//
		}

		/**
		 * Get the selected item
		 * @return {[type]} [description]
		 */
		self.getValue = function() {
			return self.value;
		};
		
		/**
		 * hoverOn
		 * @return {[type]} [description]
		 */
		self.hoverOn = function() {
			// NOT IMPLEMENTED
		};
		
		/**
		 * hoverOff
		 * @return {[type]} [description]
		 */
		self.hoverOff = function() {
			// NOT IMPLEMENTED
		};

		/**
		 * select
		 * @return {[type]} [description]
		 */
		self.select = function() {
			// NOT IMPLEMENTED
		};

		/**
		 * deselect
		 * @return {[type]} [description]
		 */
		self.deselect = function() {
			// NOT IMPLEMENTED
		};

		/**
		 * handleKeyPress
		 * @param  {[type]} keyCode [description]
		 * @return {[type]}         [description]
		 */
		self.handleKeyPress = function(keyCode) {
			// NOT IMPLEMENTED
		};
	}
	return TextBox;
});

rjs.define('gui/controls/BasicNumberBox',[], function() {
	function BasicNumberBox(id, defaultValue, styles, disabled, min, max, onChange, arguments) {
		const self = this;
		self.id = id;
		var viewHtml = (min !== undefined && max !== undefined) ? `<input type="number" min="`+ min + `" max="` + max + `" id="${self.id}" ${styles ? 'style="' + styles + '"' : ''} ${disabled === true ? 'disabled' : ''} /> <span class="validationError" id="${self.id}-error" style="display: none;">Must be between ` + min + ` and ` + max + `</span>` : `<input type="number" id="${self.id}" ${styles ? 'style="' + styles + '"' : ''} ${disabled === true ? 'disabled' : ''} />`;
		// viewHtml += `<span class="ValidationError" id="${self.id}-error" style="display: none'">The value can only be between ` + min + ` and ` + max + `" </span>`;
		self.html = viewHtml;
		self.value = defaultValue;
		self.arguments = arguments;

		self.addedToCanvas = () => {
			var error = $(`#${self.id}-error`);
			$(`#${self.id}`).val(self.value);
			$(`#${self.id}`).change(function() {
				self.value = $(`#${self.id}`).val();
				if(min !== undefined && max !== undefined && self.value >= min && self.value <= max) {
					if(error) {
						error.hide();
						if (onChange) {
							onChange(self.arguments, self.id, true);
						}
					}
				} else {
					if(error) {
						error.show();
						if (onChange) {
							onChange(self.arguments, self.id, false);
						}
					}
				}
			});
		};

		self.enabled = (enabled) => {
			if (enabled) {
				$(`#${self.id}`).prop("readonly", false);
				$(`#${self.id}`).prop("disabled", false);
			}
			else {
				$(`#${self.id}`).prop("readonly", true);
				$(`#${self.id}`).prop("disabled", true);
			}
		};

		function getObjectById(id) { }

		self.getValue = () => self.value;
		self.hoverOn = () => {};
		self.hoverOff = () => {};
		self.select = () => {};
		self.deselect = () => {};
		self.handleKeyPress = (keyCode) => {};
	}
	return BasicNumberBox;
});

rjs.define('gui/controls/BasicComboBox',["lib/knockout", "lib/knockout-secure-binding"], function (ko, ksb) {
  function BasicComboBox(id, data, itemSelected, styles, isDisabled, onChangeCallback, arguments) {
    const self = this;
    self.id = id;
    self.isDisabled = isDisabled;
    
    // Select box HTML
    var viewHtml = `<select id="${self.id}" ${
      styles ? `style="${styles}"` : ""
    } data-bind="options: options, optionsText: 'displayName', valueAllowUnset: true, optionsValue: 'id', value: selectedOption" ${
      isDisabled ? "disabled" : ""
    }></select>`;
    
    self.html = viewHtml;
    
    // If this is the video mode combo box, remove ':fullres:gfxmemlarge' from the video mode string, so as to render initial value correctly.
    if (id.includes('videoModeComboBox')) {
        itemSelected = itemSelected.split(':')[0]
    }

    // Knockout bindings
    self.data = data;
    self.options = ko.observableArray(data);
    self.selectedOption = ko.observable(itemSelected);
    self.selectedObject = itemSelected;
    self.arguments = arguments;

    self.addedToCanvas = () => {
      // Show all options, more restricted setup than the Knockout regular binding.
      var options = {
        attribute: "data-bind", // defaults to data-sbind which breaks this!
        globals: {}, // no globals
        bindings: ko.bindingHandlers, // still use default binding handlers
        noVirtualElements: true, // no virtual elements
      };
      // Apply Bindings (Use Secure Bindings)
      ko.bindingProvider.instance = new ksb(options);
      ko.applyBindings(self, $(`#${self.id}`)[0]);

      // Log Selection changes
      $(`#${self.id}`).change(function () {
        if (getObjectById(self.selectedOption()) != null) {
          self.selectedObject = getObjectById(self.selectedOption());
        }
        logger.debug("Selected: " + self.selectedOption());
        if (onChangeCallback) {
          onChangeCallback(self.arguments, self.id);
        }
      });
    };

    function getObjectById(id) {
      for (i = 0; i < data.length; i++) {
        if (data[i].id === id) return data[i];
      }
    }

    self.enabled = (enabled) => {
      if (enabled) {
        $(`#${self.id}`).prop("readonly", false);
        $(`#${self.id}`).prop("disabled", false);
      } else {
        $(`#${self.id}`).prop("readonly", true);
        $(`#${self.id}`).prop("disabled", true);
      }
    };

    self.selectedItem = () => self.selectedObject;
    self.hoverOn = () => {};
    self.hoverOff = () => {};
    self.updateData = (data) => {
      // Remove the current options available for the select dropdown
      self.options.removeAll();
      ko.cleanNode($(`#${self.id}`)[0]);
      // Update the options with the new filtered list for the select dropdown
      self.data = data;
      self.options = ko.observableArray(data);
      // By default select the first option - this will also remove the previously selected choice if it was invalid
      var firstOption = data[0] ? data[0].id : "";
      self.selectedOption = ko.observable(firstOption);
      self.selectedObject = firstOption;
      ko.applyBindings(self, $(`#${self.id}`)[0]);
    };
    self.select = () => {};
    self.deselect = () => {};
    self.handleKeyPress = (keyCode) => {};
  }
  return BasicComboBox;
});

rjs.define('system/registry/RegistryInterface',["properties/App.Settings"], (Settings) => {

    // https://brightsign.atlassian.net/wiki/spaces/DOC/pages/370678545/registry
    // Currently we only change PTP information in the registry

    function RegistryInterface() {
        var self = this;
        var registryClass = require("@brightsign/registry");
        var registry = new registryClass();
        self.ptp = null;
        
        // On player startup read the registry so we can then use that information in the SetupView
        self.init = function() {
            var dfd = $.Deferred();
            self.read().then(function() {
                dfd.resolve();
            });
            return dfd;
        };

        self.read = function () {
            var dfd = $.Deferred();
            var ptpData = {
                enabled: false,
                domain: null
            }
            //Reads the networking registry (for PTP)
            registry.read("networking").then(function(reg) {
                if(reg.ptp_domain) {
                    ptpData.enabled = true;
                    ptpData.domain = reg.ptp_domain;
                    self.ptp = ptpData;
                } else {
                    ptpData.domain = "0"; //If Not Set then we should still default the PTP Domain to 0
                    self.ptp = ptpData;
                }
                logger.debug("PTP Registry Information is- Enabled: " + self.ptp.enabled + " Domain: " + self.ptp.domain);
                dfd.resolve();
            });
            return dfd;
        };

        self.write = function (PtpData) {
            var dfd = $.Deferred();
            if (PtpData.enabled == true) {
                var newPtpValue = (parseInt(PtpData.domain) >= 0 && parseInt(PtpData.domain) <= 127) ? String(parseInt(PtpData.domain)) : "0"; //JIC for any reason default this back to 0 if there is an issue.
                logger.debug("Need to update PTP Registry Information: " + newPtpValue);
                registry.write("networking", {
                    ptp_domain: newPtpValue
                }).then(function () {
                    // logger.debug("Write Successful");
                    registry.flush().then(function() {
                        logger.debug("PTP: Flush to Registry Successful");
                        setTimeout(function () { //set a slight delay before calling back as we will reboot the client after this change
                            dfd.resolve();
                        }, 250);
                    });
                });
            } else {
                self.delete().then(function() {
                        dfd.resolve(); 
                });
            }

            return dfd;
        };

        self.delete = function () {
            var dfd = $.Deferred();
            logger.debug("Need to delete PTP Registry Information");
            registry.write("networking", {
                ptp_domain: null
            }).then(function () {
                // logger.debug("Delete Successful");
                registry.flush().then(function() {
                    logger.debug("PTP: Flush to Registry Successful");
                    setTimeout(function () { //set a slight delay before calling back as we will reboot the client after this change
                        dfd.resolve();
                    }, 250);
                });
            });
            return dfd;
        };
    }


    return new RegistryInterface();
});
rjs.define('models/audioChannelCollection',[],function () {
  var audioChannelsCollection = [
    {
      displayName: "HDMI",
      id: "hdmi",
    },
    {
      displayName: "s/pdif",
      id: "spdif",
    },
    {
      displayName: "analog",
      id: "analog",
    },
    {
      displayName: "USB",
      id: "usb",
    },
    {
      displayName: "none",
      id: "none",
    },
  ];
  return audioChannelsCollection;
});

rjs.define('models/DOMScalableDevicesCollection',[],function() {
	return [
        'xc4055',
        'xc2055',
        'ls445',
        'xt1144',
        'xt1144-t',
        'xt244'
    ]
});

rjs.define('gui/views/SetupView',["properties/App.Settings", 'system/DateTime', "controllers/UserConfigurationController", "gui/controls/ComboBox", "gui/controls/Button", "gui/controls/CheckBox", "models/RotationCollection", "models/TimeZoneCollection", "properties/App.Settings", "gui/controls/TextBox","gui/controls/BasicNumberBox","gui/controls/BasicComboBox", "system/graphics/GraphicsInterface", "system/registry/RegistryInterface", "models/audioChannelCollection", "models/DOMScalableDevicesCollection"], function (Settings, DateTime, UserConfigurationController, ComboBox, Button, CheckBox, RotationCollection, TimeZoneCollection, AppSettings, TextBox, BasicNumberBox, BasicComboBox, GraphicsInterface, RegistryInterface, audioChannelCollection, DOMScalableDevicesCollection) {
/**
     * Setup view for ChromeOS
     * Allows the user to configure timezone and orientation of application
     * @param {[type]}  callbacks     [description]
     * @param {Boolean} isCancellable [description]
     */
    function SetupView(callbacks, isCancellable) {
        var self = this;
        self.id = 'setupView';

        var _callbacks = null; 
        var _controls = [];
        var isSeries5 = Settings.application.features.deviceSeries == 5

        // Primary Settings
        var _timeZoneComboBox = null;
        var _rotationComboBox = null;
        var _audioEnabledCheckbox = null;
        var _alwaysEmbedWebpagesCheckbox = null;
        var _PtpEnabledCheckbox = null;
        var _PtpDomainTextbox = null;
        var _videoTransparencyEnabledCheckbox = null;
        var _kioskModeEnabledCheckbox = null;
        var _kioskModeIdleTimeoutBox = null;
        var _enableGaplessWidgetTransitionsCheckbox = null;
        if(isSeries5) {
            var _originalScreens = GraphicsInterface.screens;
            var _screens = $.extend(true, [], _originalScreens);
            var _audioChannelComboBox = null;
        };
        var _videoModes = GraphicsInterface.availableVideoModes;
        var is8kResolutionEnabled = false;

        var rotationEnabled = true;
        var timezoneEnabled = true;
        var transparencyEnabled = false;
        var gaplessWidgetOptionEnabled = true;
        var removeAutoResolution = false; // flag used to identify panels where 'auto' resolution can be problematic
        var isSoc = false; // flag used to identify SoC devices where users should not be able to disable screen 1

        //Enable settings based on features
        var device_info = new BSDeviceInfo();

        self.initialize = function(callbacks, isCancellable) {
            switch (device_info.model.toLowerCase()) {
                case "xt1144":
                case "xt1143":
                    transparencyEnabled = true;
                    gaplessWidgetOptionEnabled = false; //XTs don't need it
                    break;
                case "lguv5n":
                    removeAutoResolution = true;
                    isSoc = true;
                    break;
                default:
                    transparencyEnabled = false;
                    gaplessWidgetOptionEnabled = true;
                    break;
            }


            if (isCancellable == undefined)
                isCancellable = true;

            _callbacks = callbacks;

            if (timezoneEnabled) {
                // Timezone Selection
                _timeZoneComboBox = new ComboBox('timeZoneComboBox', TimeZoneCollection, UserConfigurationController.timeZone());
                _controls.push(_timeZoneComboBox);
            }

            // Screen Rotation
            if(Settings.application.features.deviceSeries !== 5 && Settings.application.features.maxSupportedOutputs == 1) {
                if (rotationEnabled) {
                    _rotationComboBox = new ComboBox('rotationComboBox', RotationCollection, UserConfigurationController.rotation());
                    _controls.push(_rotationComboBox);
                }
            }

            // PTP
            var PtpEnabled = (UserConfigurationController.ptp() !== undefined) ? UserConfigurationController.ptp().enabled : true;
            var PtpDomain = (UserConfigurationController.ptp() !== undefined) ? String(parseInt(UserConfigurationController.ptp().domain)) : 0;
            _PtpEnabledCheckbox = new CheckBox('ptpCheckbox', PtpEnabled, '', 'width: 4rem; height: 4rem;', onPtpChange);
            _PtpDomainTextbox = new TextBox('ptpDomainTextbox', PtpDomain, "number", 0, 127, onPtpChange, !PtpEnabled);
            _controls.push(_PtpEnabledCheckbox);
            _controls.push(_PtpDomainTextbox);

            // Audio
            if (isSeries5) {
                // Global Audio Channel Combo Box
                _audioChannelComboBox = new BasicComboBox(`audioChannelComboBox`, audioChannelCollection, (UserConfigurationController.audio().globalAudioChannel ? UserConfigurationController.audio().globalAudioChannel : audioChannelCollection[0].id), "font-size:18px; margin: 10px; padding: 10px;", false, onComboBoxChangeHandler, null);
                _controls.push(_audioChannelComboBox);
            } else {
                // Global Audio Enabled Check Box
                _audioEnabledCheckbox = new CheckBox('audioCheckbox', !UserConfigurationController.isMuted(), '', 'width: 4rem; height: 4rem;');
                _controls.push(_audioEnabledCheckbox);
            }

            // Video transparency 
            if (transparencyEnabled) {
                _videoTransparencyEnabledCheckbox = new CheckBox('videoTransparencyCheckbox', UserConfigurationController.videoTransparency().enabled, '', 'width: 4rem; height: 4rem;');
                _controls.push(_videoTransparencyEnabledCheckbox);
            }

            // Kiosk mode
            _kioskModeEnabledCheckbox = new CheckBox('kioskCheckbox', UserConfigurationController.interactiveKiosk().enabled, '', 'width: 4rem; height: 4rem;');
            _kioskModeIdleTimeoutBox = new TextBox('kioskTextbox', UserConfigurationController.interactiveKiosk().idleIntervalInSeconds, "number");
            _controls.push(_kioskModeEnabledCheckbox);
            _controls.push(_kioskModeIdleTimeoutBox);
            
            // 'Always Embed Webpages'
            _alwaysEmbedWebpagesCheckbox = new CheckBox('alwaysEmbedWebpagesCheckbox', UserConfigurationController.alwaysEmbedWebpages().enabled, '', 'width: 4rem; height: 4rem;', UserConfigurationController.toggleAlwaysEmbedWebpages);
            _controls.push(_alwaysEmbedWebpagesCheckbox);

            // Gapless widget transitions
            if (gaplessWidgetOptionEnabled) {
                _enableGaplessWidgetTransitionsCheckbox = new CheckBox('gaplessWidgetCheckbox', UserConfigurationController.gaplessWidgetTransitions(), '', 'width: 4rem; height: 4rem;');
                _controls.push(_enableGaplessWidgetTransitionsCheckbox);
            }

            // Save button
            var _saveButton = new Button('saveBtn', 'Save', {
                // Save button callback
                click: function() {
                    saveSettings();
                }
            });
            _controls.push(_saveButton);

            // Cancel button
            if (isCancellable) {
                var _cancelButton = new Button('cancelBtn', 'Cancel', {
                    click: function() {
                        _callbacks.cancel();
                    }
                });
                _controls.push(_cancelButton);
            }

            // Setup View HTML
            var setupViewWidth = '100%';
            var setupViewHeight = '100%';
            var enabledDisplays = _screens.filter(function (screen) {
                if (screen.enabled) {
                    return screen;
                };
            });
            var primaryDisplay = enabledDisplays[0];
            var isDOMRotated = (primaryDisplay.transform == "normal") ? false : true;
            var setupViewClassName = (isDOMRotated) ? "isPortrait " : '';

            if(isSeries5) {
                // Do we need to apply conditional styling to the player's Setup View?
                var model = device_info.model.toLowerCase();
                var isDOMScalableDevice = (DOMScalableDevicesCollection.includes(model));
                if (!isDOMRotated) {
                    var videoModeWidth = Number(primaryDisplay.videoMode.split('x')[0]);
                    var videoModeHeight = Number(primaryDisplay.videoMode.split('x')[1]);
                } else if (isDOMRotated) {
                    var videoModeWidth = Number(primaryDisplay.videoMode.split('x')[1]);
                    var videoModeHeight = Number(primaryDisplay.videoMode.split('x')[0]);
                }

                if (isDOMScalableDevice) {
                    if ((videoModeWidth >= 7680 && videoModeHeight >= 4320) || (videoModeWidth >= 4320 && videoModeHeight >= 7680)) {
                        setupViewClassName += 'is8k'
                    } else if ((videoModeWidth >= 3840 && videoModeHeight >= 2160) || (videoModeWidth >= 2160 && videoModeHeight >= 3840)) {
                        setupViewClassName += 'is4k';
                    }
                }

                // Is it a multi-screen setup? If so, the Setup View should only be as large as the primnary display
                var isUsingMultipleOutputs = (_screens.length > 1);

                if (isUsingMultipleOutputs && primaryDisplay.videoMode !== "auto") {
                    videoModeWidth += 'px';
                    videoModeHeight += 'px';
                    setupViewWidth = videoModeWidth;
                    setupViewHeight = videoModeHeight;
                }
            }
            
            // Setting the absolute co-ordiantes to the primary screenX and screenY values ensures the Setup View will always show on the first enabled display
            var viewHtml = `<div id='${self.id}' class='${setupViewClassName}' style='position:absolute;left:${primaryDisplay.screenX}px;top:${primaryDisplay.screenY}px;width:${setupViewWidth};height:${setupViewHeight};background-color:#FFF;text-align:center'>`;

            viewHtml += "<a id='" + self.id + "KeyFocus' href='javascript:void(0);'></a>";
            //	viewHtml += "<img style='margin:20px;' src='images/sl_logo.png' />";
            viewHtml += "<h1 style='margin-bottom:20px; margin-top: 50px; color:#000''>Signagelive BrightSign Setup</h1>";

            if (timezoneEnabled) {
                viewHtml += "<h2 class='time-zone-header' style='color:#000;'>Time zone:</h2>";
                viewHtml += _timeZoneComboBox.html;
            }

            if (isSeries5) {
                viewHtml += "<h2 id='screen-config-header' style='color:#000;'>Screen Config:</h2>";
                viewHtml += `<div class="datagrid">`;
                viewHtml += `<table>`;
                viewHtml += `<thead>`;
                viewHtml += `<tr><th class="w-6">Name</th><th class="w-8">Enabled?</th><th class="w-6">Audio</th><th class="w-8">Screen X</th><th class="w-8">Screen Y</th><th class="w-11">Rotation</th><th class="w-11">Video Mode</th><th class="w-8">Color Space</th><th class="w-8">Bit Depth</th></tr>`; //<th class="w-10">Frequency</th></tr>`;
                viewHtml += `</thead>`;
                viewHtml += `<tbody>`;

                var availableStates = {
                    "videoMode": ["auto"],
                    "colorSpace": [],
                    "colorDepth": []
                };

                var refObj = {
                    "videoMode": [{
                        "displayName": "auto",
                        "id": "auto"
                    }],
                    "colorSpace": [],
                    "colorDepth": []
                };

                var otherScreens = {
                    "videoMode": [{
                        "displayName": "auto",
                        "id": "auto"
                    }]
                };

                _videoModes.forEach(function (vm) {
                    if (availableStates.colorDepth.indexOf(vm.colorDepth) == -1) {
                        availableStates.colorDepth.push(vm.colorDepth);
                        refObj.colorDepth.push({ "displayName": vm.colorDepth, "id": vm.colorDepth });
                    }
                    if (availableStates.colorSpace.indexOf(vm.colorSpace) == -1) {
                        availableStates.colorSpace.push(vm.colorSpace);
                        refObj.colorSpace.push({ "displayName": vm.colorSpace, "id": vm.colorSpace });
                    }
                    if (availableStates.videoMode.indexOf(vm.modeName) == -1) {
                        availableStates.videoMode.push(vm.modeName);
                        // Only the primary display should allow an 8k orientation to be set
                        if (vm.height < 7680 && vm.width < 7680) {
                            otherScreens.videoMode.push({"displayName": vm.modeName, "id": vm.modeName });
                        }
                        refObj.videoMode.push({ "displayName": vm.modeName, "id": vm.modeName });
                    }
                });

                // If this is a panel where we want to remove the 'auto' resolution option, do it here (it will always be the first option)
                if (removeAutoResolution) {
                    refObj.videoMode.shift();
                }

                _screens.forEach(function (screen, index) {
                    screen.formControls = {};
                    _videoModes.filter(function (screenMode, ind) {                       
                        if (screenMode.modeName == screen.videoMode) {
                            // if a display that is not the primary display tries to set a resolution to 8k then default it back to auto
                            if ((screenMode.height >= 7680 || screenMode.width >= 7680) && ind > 0) {
                                logger.warning("Unsupported screen resolution for " + screen.outputName + " setting it back to auto");
                                screen.videoMode = "auto";
                            } else {
                                screen.videoMode = screenMode.modeName;
                            }
                            screen.colorSpace = screenMode.colorSpace;
                            screen.colorDepth = screenMode.colorDepth;
                        }
                    });
                    
                    if (screen.transform == "normal") { screen.transform = 0; }; //0 is not an option when saving the screen configuration so this will get converted back later on.
                    
                    viewHtml += `<tr>`;
                    viewHtml += `<td><strong>` + screen.outputName + `</strong> </td><td>`;

                    // Screen Enabled control
                    screen.formControls.enabledCheckBox = new CheckBox(`screen-${screen.outputName}-EnabledCheckBox`, screen.enabled, '', 'width: 4rem; height: 4rem;', onCheckBoxChangeHandler, screen.outputName, isSoc ? true : false);
                    _controls.push(screen.formControls.enabledCheckBox);
                    viewHtml += screen.formControls.enabledCheckBox.html;
                    viewHtml += `</td><td>`;

                    // Discreet Audio Enabled Checkbox Control
                    screen.formControls.audioCheckBox = new CheckBox(`screen-${screen.outputName}-AudioCheckBox`, (UserConfigurationController.audio().discreetAudioChannels.includes(screen.outputName)), '', 'width: 4rem; height: 4rem;', null, null, (!screen.enabled), UserConfigurationController.audio().globalAudioChannel);
                    _controls.push(screen.formControls.audioCheckBox);
                    viewHtml += screen.formControls.audioCheckBox.html;
                    viewHtml += `</td><td>`;

                    // Screen ScreenX control
                    screen.formControls.screenXPositionTextBox = new BasicNumberBox(`screen-${screen.outputName}-XPositionTextBox`, (screen.screenX !== undefined) ? screen.screenX : 0, "width: 70%; margin: 0.25em; padding: 0.25em;", (!screen.enabled), 0, 12000, onScreenPositionChange, [screen, "screenXPositionTextBox"]);
                    _controls.push(screen.formControls.screenXPositionTextBox);
                    viewHtml += screen.formControls.screenXPositionTextBox.html;
                    viewHtml += `</td><td>`;

                    // Screen ScreenY control
                    screen.formControls.screenYPositionTextBox = new BasicNumberBox(`screen-${screen.outputName}-YPositionTextBox`, (screen.screenY !== undefined) ? screen.screenY : 0, "width: 70%; margin: 0.25em; padding: 0.25em;", (!screen.enabled), 0, 12000, onScreenPositionChange, [screen, "screenYPositionTextBox"]);
                    _controls.push(screen.formControls.screenYPositionTextBox);
                    viewHtml += screen.formControls.screenYPositionTextBox.html;
                    viewHtml += `</td><td>`;

                    // Screen Rotation/Transform control
                    try {
                        var selectedRotationOption = (screen.transform !== undefined) ? RotationCollection.filter(function(rotation) { return Number(screen.transform) == rotation.id })[0] : RotationCollection[0];
                    } catch (e) {
                        // If for some reason we fail to identify the current rotation of the device then set it back to landscape
                        var selectedRotationOption = RotationCollection[0];
                    };

                    screen.formControls.rotationComboBox = new BasicComboBox(`screen-${screen.outputName}-RotationComboBox`, RotationCollection, selectedRotationOption.id, "width: 80%; margin: 0.25em; padding: 0.25em;", (!screen.enabled));
                    _controls.push(screen.formControls.rotationComboBox);
                    viewHtml += screen.formControls.rotationComboBox.html;
                    viewHtml += `</td><td>`;

                    // Screen videoMode control
                    screen.formControls.videoModeComboBox = new BasicComboBox(`screen-${screen.outputName}-videoModeComboBox`, (index == 0) ? refObj.videoMode : otherScreens.videoMode, (screen.videoMode !== undefined ? screen.videoMode : 'auto'), "margin: 0.25em; padding: 0.25em;", (!screen.enabled), onComboBoxChangeHandler, screen.outputName);
                    _controls.push(screen.formControls.videoModeComboBox);
                    viewHtml += screen.formControls.videoModeComboBox.html;
                    viewHtml += `</td><td>`;

                    // Screen Color Space control
                    screen.formControls.colorSpaceComboBox = new BasicComboBox(`screen-${screen.outputName}-colorSpaceComboBox`, supportedDropdownOptions(screen, "colorSpace"), (screen.colorSpace !== undefined ? screen.colorSpace : (supportedDropdownOptions(screen, "colorSpace")[0]) ? supportedDropdownOptions(screen, "colorSpace")[0].id : ''), "width: 80%; margin: 0.25em; padding: 0.25em;", (screen.videoMode === 'auto' || !screen.enabled));
                    _controls.push(screen.formControls.colorSpaceComboBox);
                    viewHtml += screen.formControls.colorSpaceComboBox.html;
                    viewHtml += `</td><td>`;

                    // Screen Bit Depth control
                    screen.formControls.colorDepthComboBox = new BasicComboBox(`screen-${screen.outputName}-colorDepthComboBox`, supportedDropdownOptions(screen, "colorDepth"), (screen.colorDepth !== undefined ? screen.colorDepth : (supportedDropdownOptions(screen, "colorDepth")[0]) ? supportedDropdownOptions(screen, "colorDepth")[0].id : ''), "width: 80%; margin: 0.25em; padding: 0.25em;", (screen.videoMode === 'auto' || !screen.enabled));
                    _controls.push(screen.formControls.colorDepthComboBox);
                    viewHtml += screen.formControls.colorDepthComboBox.html;
                    viewHtml += `</td>`;
                });

                viewHtml += `</tbody>`;
                viewHtml += `</table>`;
                viewHtml += `</div>`;

                viewHtml += "<div id='options' class='options' style='display: -webkit-flex; -webkit-flex-direction: row;'>";

                viewHtml += "<div style='-webkit-flex-direction: column; -webkit-flex-basis: 25%;'><h2 style='color:#000; height: 3em;'>Audio Channel:</h2>";
                viewHtml += _audioChannelComboBox.html;
                viewHtml += "</div>";

                viewHtml += "<div style='-webkit-flex-direction: column; -webkit-flex-basis: 25%;'><h2 style='color:#000; height: 3em;'>PTP Enabled:</h2>";
                viewHtml += _PtpEnabledCheckbox.html;
                viewHtml += "</div>";
    
                viewHtml += "<div style='-webkit-flex-direction: column; -webkit-flex-basis: 25%;'><h2 style='color:#000; height: 3em;'>PTP Domain:</h2>";
                viewHtml += _PtpDomainTextbox.html;
                viewHtml += "</div>";

                if (transparencyEnabled) {
                    viewHtml += "<div style='-webkit-flex-direction: column; -webkit-flex-basis: 25%;'><h2 style='color:#000; height: 3em;'>Video Transparency Enabled:</h2>";
                    viewHtml += _videoTransparencyEnabledCheckbox.html;
                    viewHtml += "</div>";
                }

                if (gaplessWidgetOptionEnabled) {
                    viewHtml += "<div style='-webkit-flex-direction: column; -webkit-flex-basis: 25%;'><h2 style='color:#000; height: 3em;'>Gapless Widget Transitions Enabled:</h2>";
                    viewHtml += _enableGaplessWidgetTransitionsCheckbox.html;
                    viewHtml += "</div>";
                }

                viewHtml += "<div style='-webkit-flex-direction: column; -webkit-flex-basis: 25%;'> <h2 style='color:#000; height: 3em;'>Kiosk Mode Enabled:</h2>";
                viewHtml += _kioskModeEnabledCheckbox.html;
                viewHtml += "</div>";

                viewHtml += "<div style='-webkit-flex-direction: column; -webkit-flex-basis: 25%;'><h2 style='color:#000; height: 3em;'>Kiosk Mode Idle Timeout (seconds):</h2>";
                viewHtml += _kioskModeIdleTimeoutBox.html;
                viewHtml += "</div>";

                viewHtml += "<div style='-webkit-flex-direction: column; -webkit-flex-basis: 25%;'><h2 style='color:#000; height: 3em;'>Always Embed Webpages:</h2>";
                viewHtml += _alwaysEmbedWebpagesCheckbox.html;
                viewHtml += "</div></div>";

                viewHtml += "<div id='validationError'></div>"
                viewHtml += "<h2 style='color:#000;'>&nbsp;</h2>";
                viewHtml += _saveButton.html;

                var _cancelButton = new Button('cancelBtn', 'Cancel', {
                    click: function () {
                        _callbacks.cancel();
                    }
                });

                if (isCancellable)
                    viewHtml += _cancelButton.html;

                var _idDisplaysBtn = new Button('idDisplaysBtn', 'Identify Displays', {
                    click: function () {
                        identifyDisplays();
                    }
                });

                _controls.push(_idDisplaysBtn);
                viewHtml += _idDisplaysBtn.html;
                viewHtml += `</div>`;
                viewHtml += `</div>`;
                viewHtml += "</div>";

                $("#canvas").css("background-color", "#FFF");
            } else {
                // See note above - only visible for users that already have rotation enabled
                if (rotationEnabled) {
                    viewHtml += "<h2 style='color:#000;'>Screen Rotation:</h2>";
                    viewHtml += _rotationComboBox.html;
                }

                viewHtml += "<h2 style='color:#000;'>Audio Enabled:</h2>";
                viewHtml += _audioEnabledCheckbox.html;

                if (transparencyEnabled) {
                    viewHtml += "<h2 style='color:#000;'>Video Transparency Enabled:</h2>";
                    viewHtml += _videoTransparencyEnabledCheckbox.html;
                }

                viewHtml += "<h2 style='color:#000;'>Kiosk Mode Enabled:</h2>";
                viewHtml += _kioskModeEnabledCheckbox.html;
                
                viewHtml += "<h2 style='color:#000;'>Kiosk Mode Idle Timeout (seconds):</h2>";
                viewHtml += _kioskModeIdleTimeoutBox.html;

                if(gaplessWidgetOptionEnabled)
                {
                    viewHtml += "<h2 style='color:#000;'>Gapless Widget Transitions Enabled:</h2>";
                    viewHtml += _enableGaplessWidgetTransitionsCheckbox.html;
                }
                viewHtml += "<h2 style='color:#000;'>&nbsp;</h2>";
                viewHtml += _saveButton.html;

                if (isCancellable)
                    viewHtml += _cancelButton.html;

                viewHtml += "</div>";
            }

            self.html = viewHtml;
        };

        /**
         * Adds the Setup View to the canvas
         * @return {[type]} [description]
         */
        self.addedToCanvas = function() {

            // Add all the controls
            $(_controls).each(function(i, control) {
                control.addedToCanvas();
            });

            // This needs to be hidden as it can prevent users from being able to use elements of the form
            $("#userMessage").css("visibility", "hidden");

            $("select")[0].focus();

            $('#saveBtn').keyup(function(event) {
                if (event.keyCode == 13 || event.keyCode == 176) {
                    $("#canvas").css("background-color", "");
                    saveSettings();
                }

            });
            $('#cancelBtn').keyup(function(event) {
                if (event.keyCode == 13 || event.keyCode == 176) {
                    $("#canvas").css("background-color", "");
                    _callbacks.cancel();
                }
            });

            $('#idDisplaysBtn').keyup(function(event) {
                if (event.keyCode == 13 || event.keyCode == 176) {
                    identifyDisplays();
                }
            });

            // ESC key cancels / exits settings screen
            $('#'+ self.id).keyup(function(event){
                if (event.keyCode == 27) {
                    $("#canvas").css("background-color", "");
                    _callbacks.cancel();
                }
            });
        };

        function supportedDropdownOptions(screen, optionName) {
            var options = [];
            if(screen.formControls.videoModeComboBox.selectedItem().id !== "auto") {
                var selectedvideoMode = screen.formControls.videoModeComboBox.selectedItem().id ? screen.formControls.videoModeComboBox.selectedItem().id : screen.formControls.videoModeComboBox.selectedObject;
            } else {
                // TODO: Disable Options for Auto dropdown
                var selectedvideoMode = "auto";
                return options;
            }

            var width = Number(selectedvideoMode.split('x')[0]);
            var height = Number(selectedvideoMode.split('x')[1]);
            
            _videoModes.filter(function (vm) {
                var trimmedSelectedVideoMode = selectedvideoMode.split(':')[0] // removes ':fullres:gfxmemlarge' from video mode string, so as to not fail below IF statements.
                if (vm.height == height && vm.width == width && vm.modeName == trimmedSelectedVideoMode) {
                    if (optionName == "colorSpace") {
                        if(!checkIfExists(vm.colorSpace, options)) {
                            options.push({"displayName": vm.colorSpace,"id": vm.colorSpace});
                        }
                    }
                    if (optionName == "colorDepth") {
                        if(!checkIfExists(vm.colorDepth, options)) {
                            options.push({"displayName": vm.colorDepth,"id": vm.colorDepth});
                        }
                    }
                }
            });

            return options;
        };

        function checkIfExists(value, list) {
            var exists = false;
            list.filter(function(item, ind) {
                if(item.id == value) {
                    exists = true;
                }
            });
            return exists;
        }

        function onCheckBoxChangeHandler(outputName) {
            logger.info("Enabled state updated");
            updateControls(outputName);
        }

        function onResolutionChange(screen, id, option) {
            var updatedOptions = supportedDropdownOptions(screen, option);
            screen.formControls[id].updateData(updatedOptions);
        }

        function onComboBoxChangeHandler(outputName, caller) {
            if (caller === 'audioChannelComboBox') {
                var selectedAudioChannel = _audioChannelComboBox.selectedItem().id ? _audioChannelComboBox.selectedItem().id : _audioChannelComboBox.selectedObject;
                _screens.forEach(function(screen) {
                    if(screen.formControls.enabledCheckBox.isChecked()) {
                        (selectedAudioChannel !== 'hdmi' && selectedAudioChannel !== undefined) ? 
                        (screen.formControls.audioCheckBox.enabled(false),
                        screen.formControls.audioCheckBox.setState(false)) :
                        screen.formControls.audioCheckBox.enabled(true);
                    } else if (selectedAudioChannel !== 'hdmi' && selectedAudioChannel !== undefined) {
                        screen.formControls.audioCheckBox.setState(false);
                    }
                     else if (is8kResolutionEnabled && selectedAudioChannel === 'hdmi' && screen.outputName !== "HDMI-1") {
                        screen.formControls.audioCheckBox.enabled(false);
                    }
                });
            } else {
                primaryOutput = _screens[0].outputName; //it must always be the primary display
                is8kResolutionEnabled = false;
                logger.info("Screen resolution state updated");
                _screens.map(function(screen, ind) {
                    if(screen.enabled) {
                        var selectedvideoMode = screen.formControls.videoModeComboBox.selectedItem().id ? screen.formControls.videoModeComboBox.selectedItem().id : screen.formControls.videoModeComboBox.selectedObject;
                        var width = Number(selectedvideoMode.split('x')[0]);
                        var height = Number(selectedvideoMode.split('x')[1]);
                        if(height >= 7680 || width >= 7680) {
                            logger.info("8k resolution enabled");
                            is8kResolutionEnabled = true;
                        }
                    }
                });
                updateControls(primaryOutput, caller);
                updateAdditionalConfigOptions(outputName);
        }};

        function updateAdditionalConfigOptions(outputName) {
            var screen = _screens.filter(function(screen) { return screen.outputName == outputName; })[0];
            onResolutionChange(screen, "colorSpaceComboBox", "colorSpace");
            onResolutionChange(screen, "colorDepthComboBox", "colorDepth");
        };

        function onScreenPositionChange([screen, positionName], id, isValueValid) {
            // Update the current form control to reflect whether the screen position is valid or not
            screen.formControls[positionName+"-Error"] = isValueValid;
            checkIfFormValid();
        }

        function checkIfFormValid() {
            // Check all form controls to see if any are invalid - as that will then prevent the form being saved
            var screenPositionValidityCheck = [];
            _screens.forEach(function (screen) {
                screenState = screen.formControls.enabledCheckBox.isChecked();
                if (screenState) {
                    if (screen.formControls["screenXPositionTextBox-Error"] !== undefined) {
                        screenPositionValidityCheck.push(screen.formControls["screenXPositionTextBox-Error"]);
                    };
                    if (screen.formControls["screenYPositionTextBox-Error"] !== undefined) {
                        screenPositionValidityCheck.push(screen.formControls["screenYPositionTextBox-Error"]);
                    };
                }
            });

            // Do not allow the config to be saved if no screens are enabled
            var screensEnabled = _screens.filter(function(screen, ind) {
                screenState = screen.formControls.enabledCheckBox.isChecked();
                if(screenState) {
                    return screen;
                }
            });

            var isValid = screenPositionValidityCheck.indexOf(false) == -1;
            if(isValid && screensEnabled.length > 0) {
                $('#saveBtn').removeClass("disabled").attr("disabled", false).attr('tabindex', '0');
            } else {
                $('#saveBtn').addClass("disabled").attr('disabled', 'disabled').attr('tabindex', '-1');
            }
        };

        function updateControls(output, caller) {
            var screenToUpdate = []; var otherScreens = [];

            // Prevents the form being saved if form is invalid
            checkIfFormValid();

            // Sorts outputs into the appropriate array
            _screens.filter(function(screen, ind) {
                if(screen.outputName == output) {
                    screenToUpdate.push(screen);
                } else {
                    otherScreens.push(screen);
                }
            });

        try {
            Object.keys(screenToUpdate[0].formControls).forEach(function(displayFormControl) {
                screenState = screenToUpdate[0].formControls.enabledCheckBox.isChecked();
                // Rotation box for primary display should always be enabled
                switch (displayFormControl) {
                    case "audioCheckBox":
                        var selectedAudioChannel = _audioChannelComboBox.selectedItem().id ? _audioChannelComboBox.selectedItem().id : _audioChannelComboBox.selectedObject;
                        if (selectedAudioChannel !== 'hdmi' && selectedAudioChannel !== undefined) {
                            screenToUpdate[0].formControls[displayFormControl].enabled(false);
                            screenToUpdate[0].formControls[displayFormControl].setState(false);
                        } else {
                            screenToUpdate[0].formControls[displayFormControl].enabled(screenState);
                        }
                        break;
                    case "rotationComboBox":
                    case "screenXPositionTextBox":
                    case "screenYPositionTextBox":
                    case "offsetTextBox":
                    case "videoModeComboBox":
                    case "colorSpaceComboBox":
                    case "colorDepthComboBox":
                        var selectedvideoMode = screenToUpdate[0].formControls.videoModeComboBox.selectedItem().id ? screenToUpdate[0].formControls.videoModeComboBox.selectedItem().id : screenToUpdate[0].formControls.videoModeComboBox.selectedObject;
                        if (selectedvideoMode == "auto" && (displayFormControl == "colorSpaceComboBox" || displayFormControl == "colorDepthComboBox")) {
                            screenToUpdate[0].formControls[displayFormControl].enabled(false);
                        } else if (!is8kResolutionEnabled) {
                            screenToUpdate[0].formControls[displayFormControl].enabled(screenState);
                        } else {
                            screenToUpdate[0].formControls[displayFormControl].enabled(screenState);
                        }
                        break;
                    case "enabledCheckBox":
                        // Don't want to run this case if the update was triggered by the videoModeComboBox, as it can needlessly tick the discreet audio checkbox 
                        if (caller && caller.endsWith("videoModeComboBox")) {
                            break;
                        } else {
                            screenToUpdate[0].formControls["audioCheckBox"].setState(screenState);
                        }
                        break;
                    default:
                        break;
                }
            });

            for(var i = 0; i < otherScreens.length; i++) {
            Object.keys(otherScreens[i].formControls).forEach(function(displayFormControl) {
                screenState = otherScreens[i].formControls.enabledCheckBox.isChecked();
                // Rotation box for primary display should always be enabled
                switch (displayFormControl) {
                    case "audioCheckBox":
                        var selectedAudioChannel = _audioChannelComboBox.selectedItem().id ? _audioChannelComboBox.selectedItem().id : _audioChannelComboBox.selectedObject;
                        if (selectedAudioChannel !== 'hdmi' && selectedAudioChannel !== undefined) {
                            otherScreens[i].formControls[displayFormControl].enabled(false);
                            otherScreens[i].formControls[displayFormControl].setState(false);
                        } else if (is8kResolutionEnabled) {
                            otherScreens[i].formControls[displayFormControl].enabled(false);
                            otherScreens[i].formControls[displayFormControl].setState(false);
                        } else {
                            otherScreens[i].formControls[displayFormControl].enabled(screenState);
                        }
                        break;
                    case "rotationComboBox":
                    case "screenXPositionTextBox":
                    case "screenYPositionTextBox":
                    case "offsetTextBox":
                    case "videoModeComboBox":
                    case "colorSpaceComboBox":
                    case "colorDepthComboBox":
                        var selectedvideoMode = otherScreens[i].formControls.videoModeComboBox.selectedItem().id ? otherScreens[i].formControls.videoModeComboBox.selectedItem().id : otherScreens[i].formControls.videoModeComboBox.selectedObject;
                        if (selectedvideoMode == "auto" && (displayFormControl == "colorSpaceComboBox" || displayFormControl == "colorDepthComboBox")) {
                            otherScreens[i].formControls[displayFormControl].enabled(false);
                        } else
                        // if 8k resolution is enabled for the primary screen then disable the other screens from being enabled
                        if(is8kResolutionEnabled) {
                            otherScreens[i].formControls[displayFormControl].enabled(false);
                        } else if (!is8kResolutionEnabled && screenState) {
                        // else if 8k resolution has not been set then you need to pass back the original screen state they were set to
                            otherScreens[i].formControls[displayFormControl].enabled(screenState); 
                        } else {
                            otherScreens[i].formControls[displayFormControl].enabled(screenState); 
                        }
                        break;
                    case "enabledCheckBox":
                        if (is8kResolutionEnabled) {
                            otherScreens[i].formControls[displayFormControl].enabled(false);
                            otherScreens[i].formControls.enabledCheckBox.setState(screenState);
                        } else if (!is8kResolutionEnabled) {
                            otherScreens[i].formControls[displayFormControl].enabled(true);
                            otherScreens[i].formControls.enabledCheckBox.setState(screenState);
                        }
                    default:
                        break;
                }
            });
            }
        } catch (e) {
            // we should do something if this fails?
        }
        };

        function identifyDisplays() {
            clearTimeout(identifyDisplays);
            _originalScreens.forEach(function(screen, index) {
                if(screen.enabled) {
                    var topPos = screen.screenY + "px";
                    var leftPos = screen.screenX + "px";
                    var screenName = screen.outputName;
                    var div = document.createElement('div');
                    div.className = "temp-screenID";
                    div.style.width = "10em";
                    div.style.height = "5em";
                    div.style.fontSize = "2.5em";
                    div.style.position = "absolute";
                    div.style.backgroundColor = "#FFF";
                    div.style.top = topPos;
                    div.style.left = leftPos;
                    div.style.display = "flex";
                    div.style.justifyContent = "center";
                    div.style.alignItems = "center";
                    div.innerText = screenName;
                    document.body.appendChild(div);
                }
            });
            setTimeout(function() {
                document.querySelectorAll(".temp-screenID").forEach(function(el) { el.remove() });
            }, 3000);
        }        

        function updateScreenConfig() {
            var dfd = $.Deferred();

            _screens.forEach(function(screen) {
                screen.enabled = screen.formControls.enabledCheckBox.isChecked();
                if ((isNaN(Number(screen.formControls.screenXPositionTextBox.getValue())) || isNaN(Number(screen.formControls.screenYPositionTextBox.getValue()))) && screen.enabled) {
                    // logger.warning("Invalid config attempted to be save -- do not save it");
                    removeScreenConfig(screen);
                } else if(screen.enabled) {
                    screen.screenX = Number(screen.formControls.screenXPositionTextBox.getValue());
                    screen.screenY = Number(screen.formControls.screenYPositionTextBox.getValue());
                    screen.transform = (screen.formControls.rotationComboBox.selectedItem().id !== undefined) ? (screen.formControls.rotationComboBox.selectedItem().id == 0) ? "normal" : Number(screen.formControls.rotationComboBox.selectedItem().id) : (screen.formControls.rotationComboBox.selectedObject == 0) ? "normal" : String(screen.formControls.rotationComboBox.selectedObject);
                    screen.videoMode = (screen.formControls.videoModeComboBox.selectedItem().id !== undefined) ? String(screen.formControls.videoModeComboBox.selectedItem().id) : String(screen.formControls.videoModeComboBox.selectedObject);
                    screen.colorSpace = (screen.formControls.colorSpaceComboBox.selectedItem().id !== undefined) ? String(screen.formControls.colorSpaceComboBox.selectedItem().id) : String(screen.formControls.colorSpaceComboBox.selectedObject);
                    screen.colorDepth = (screen.formControls.colorDepthComboBox.selectedItem().id !== undefined) ? String(screen.formControls.colorDepthComboBox.selectedItem().id) : String(screen.formControls.colorDepthComboBox.selectedObject);

                } else {
                    // Screen is not enabled - make sure additional configuration values are not set
                    removeScreenConfig(screen);
                }

                // We do not want to return this information as videoMode is the parameter required
                delete screen.colorSpace;
                delete screen.colorDepth;
                delete screen.formControls;

            });

            dfd.resolve(_screens);

            return dfd;
        };

        // If a screen config is invalid for any reason or the screen state has been updated so that the screen needs to be disabled, then we should make sure to update/remove these values
        function removeScreenConfig(screen) {
            screen.enabled = false;
            delete screen.screenX;
            delete screen.screenY;
            delete screen.transform;
            delete screen.videoMode;
        };

        function onPtpChange() {
            var ptpEnabledState = _PtpEnabledCheckbox.isChecked();
            var ptpDomainVal = _PtpDomainTextbox.getValue();
            if(ptpEnabledState == false) {
                $('#ptpDomainTextbox_textBox').attr("disabled", 'disabled').attr("readonly", 'readonly').attr('tabindex', '-1');
            } else {
                $('#ptpDomainTextbox_textBox').attr("disabled", false).attr("readonly", false).attr('tabindex', '0');
            }

            if(ptpDomainVal && Number(ptpDomainVal) >= 0 && Number(ptpDomainVal) <= 127) {
                $('#saveBtn').removeClass("disabled").attr("disabled", false).attr('tabindex', '0');
                $('#validationError').css('display', 'none');
            } else {
                $('#saveBtn').addClass("disabled").attr('disabled', 'disabled').attr('tabindex', '-1');
                $('#validationError').text("PTP Domain Setting must be between 0 and 127.");
                $('#validationError').css('display', 'block');
            }
        }

        function saveSettings() {
            try {
                // Call the Setup Complete callback and pass selected options
                var settings = {};

                if (timezoneEnabled) {
                    // Update timezone and rotation settings
                    settings.timeZone = _timeZoneComboBox.selectedItem();
                } else {
                    settings.timeZone = {
                        displayName: '(GMT) Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London',
                        bsid: 'GMTBST',
                        id: 'Europe/London'
                    }
                }

                if(isSeries5) {

                    // Declaration placed at top of function to avoid losing reference to 'formControls' (which gets deleted in anonymous function below)
                    var audioEnabledScreens = _screens.filter(function (screen) {
                        return screen.formControls.audioCheckBox.isChecked();
                      });

                      var enabledAudioCheckboxes = [];
                      audioEnabledScreens.forEach(screen => {
                        enabledAudioCheckboxes.push(screen.outputName);
                      });

                    // Iterate over the form controls and get all the new screen configuration options
                    updateScreenConfig().then(function(config) {
                        settings.screenConfig = config;

                        settings.interactiveKiosk = {
                            enabled : false,
                            idleIntervalInSeconds : 15,
                            trigger : {
                                key : "t",
                                type : "KeyboardEvent"
                            }
                        };

                        // S5 rotation should always be stored as 0 due to the way rotation is handled
                        settings.rotation = {
                            displayName: 'Custom rotation',
                            angle: 0,
                            id: 0
                        }
        
                        settings.interactiveKiosk.enabled = _kioskModeEnabledCheckbox.isChecked();
                        settings.interactiveKiosk.idleIntervalInSeconds = 15;

                        settings.ptp = {
                            enabled: _PtpEnabledCheckbox.isChecked(),
                            domain: String(_PtpDomainTextbox.getValue())
                        };


                        settings.audio = {
                            globalAudioChannel: (_audioChannelComboBox.selectedItem().id ? _audioChannelComboBox.selectedItem().id : _audioChannelComboBox.selectedItem()),
                            discreetAudioChannels: enabledAudioCheckboxes
                        };
        
                        var selectedIdleInterval = parseInt(_kioskModeIdleTimeoutBox.getValue());
        
                        if(!isNaN(selectedIdleInterval))
                        {
                            settings.interactiveKiosk.idleIntervalInSeconds = selectedIdleInterval;
                        }
        
                        settings.isMuted = (_audioChannelComboBox.selectedItem() === 'none' || _audioChannelComboBox.selectedItem().id === 'none')
        
                        if (transparencyEnabled) {
                            settings.videoTransparency = {
                                enabled: _videoTransparencyEnabledCheckbox.isChecked()
                            };
                        } else {
                            settings.videoTransparency = {
                                enabled: false
                            };
                        }
        
                        if(gaplessWidgetOptionEnabled)
                        {
                            settings.gaplessWidgetTransitions = _enableGaplessWidgetTransitionsCheckbox.isChecked();
                        } else {
                            //If the setting is disabled, we are having gapless transitions anyway
                            settings.gaplessWidgetTransitions = true;
                        }
        
                        settings.modules = {};
        
                        // Can be reshown here
                        $("#userMessage").css("visibility", "visible");

                        _callbacks.complete(settings);
                    });
                    
                // If not Series 5 then continue with the original setup steps
                } else {
                    if (rotationEnabled) {
                        settings.rotation = _rotationComboBox.selectedItem();
                    } else {
                        settings.rotation = {
                            displayName: 'No rotation',
                            angle: 0,
                            id: 0
                        }
                    }

                settings.interactiveKiosk = {
                    enabled : false,
                    idleIntervalInSeconds : 15,
                    trigger : {
                        key : "t",
                        type : "KeyboardEvent"
                    }
                };

                settings.ptp = {
                    enabled: _PtpEnabledCheckbox.isChecked(),
                    domain: String(_PtpDomainTextbox.getValue())
                };

                settings.interactiveKiosk.enabled = _kioskModeEnabledCheckbox.isChecked();
                settings.interactiveKiosk.idleIntervalInSeconds = 15;

                var selectedIdleInterval = parseInt(_kioskModeIdleTimeoutBox.getValue());

                if(!isNaN(selectedIdleInterval))
                {
                    settings.interactiveKiosk.idleIntervalInSeconds = selectedIdleInterval;
                }

                settings.isMuted = !_audioEnabledCheckbox.isChecked();

                if (transparencyEnabled) {
                    settings.videoTransparency = {
                        enabled: _videoTransparencyEnabledCheckbox.isChecked()
                    };
                } else {
                    settings.videoTransparency = {
                        enabled: false
                    };
                }

                if(gaplessWidgetOptionEnabled)
                {
                    settings.gaplessWidgetTransitions = _enableGaplessWidgetTransitionsCheckbox.isChecked();
                } else {
                    //If the setting is disabled, we are having gapless transitions anyway
                    settings.gaplessWidgetTransitions = true;
                }

                settings.modules = {};

                // Can be reshown here
                $("#userMessage").css("visibility", "visible");

                _callbacks.complete(settings);
                }
            } catch (e) {
                logger.error("Error trying to save updated settings");
            }
        }

        // Initialize the setup page
        self.initialize(callbacks, isCancellable);
    };

    return SetupView;
});
rjs.define('controllers/LocalAPIController',["system/IDConverter","controllers/InterruptController"], function(IDConverter,InterruptController) {
  var device_info = new BSDeviceInfo();
  var storagePath = device_info.model.toUpperCase() === "LGUV5N" ? "flash" : "sd";

  var express = require('/storage/' + storagePath + '/node_modules/express');
  var bodyParser = require('/storage/' + storagePath + '/node_modules/body-parser');
  var app = express();

  function LocalAPIServer(configuration) {
      var self = this;
      self.onEvent = undefined;
      var _KeepAliveIntervalTime = null;
      var _currentConfiguration = configuration;

      self.initialize = function() {

          //Keep alive every 10 minutes
          _KeepAliveIntervalTime = setInterval(function()
          {         
              $.ajax({
                  url: "http://127.0.0.1:6200/ping",
                  type: "GET",
                  contentType: 'text/plain',
                  dataType: 'text',
                  async: true,
                  data: "1",
                  timeout: 60000
              }).done(function(data, textStatus, jqXHR) {                
              }).fail(function(jqXHR, textStatus, errorThrown) {});              
              
          },600000);

          app.use(bodyParser.json());
          app.use(bodyParser.urlencoded({ extended: true }));

          app.set('port', (6200));

          app.listen(app.get('port'), function() {
              console.log('Listening on port ' + app.get('port'));
          });


          app.get('/interrupts', function(req, res, next) {
              if (_currentConfiguration == null) {
                  res.send({ error: "Configuration not loaded" });
                  return;
              }

              var interrupts = [];
              for (var i = 0; i < _currentConfiguration.content.interrupts.length; i++) {
                  var interrupt = {
                      name : _currentConfiguration.content.interrupts[i].name,
                      triggerKey : _currentConfiguration.content.interrupts[i].trigger.triggerKey
                  } 
                  interrupts.push(interrupt);
              }

              res.send(interrupts);            
          });

          app.post('/interrupts/trigger', function(req, res, next) {
              if (_currentConfiguration == null) {
                  res.send({ error: "Configuration not loaded" });
                  return;
              }

              var triggerKey = req.body.triggerKey;

              var interrupt = null;
              for (var i = 0; i < _currentConfiguration.content.interrupts.length; i++) {
                  if(_currentConfiguration.content.interrupts[i].trigger.triggerKey == triggerKey)
                  {
                      interrupt = _currentConfiguration.content.interrupts[i]
                      break;
                  }                    
              }

              if(interrupt)
              {
                  InterruptController.triggerInterrupt(interrupt.trigger.triggerKey, interrupt.trigger.type, false, true).done(function() {                      
                  });

                  res.sendStatus(200);
              } else {
                  res.sendStatus(404);
              }           

          });     

          app.post('/interrupts/stop', function(req, res, next) {

              if (InterruptController.isPlayingInterrupt()) {
                  InterruptController.cancelInterrupt();
                  res.sendStatus(202);
              } else {
                  //No interrupt is playing
                  res.sendStatus(400);
              }
          });

          app.get('/nec-alp/inventory', function(req, res, next) {
              if (_currentConfiguration == null) {
                  res.send({ error: "Configuration not loaded" });
                  return;
              }

              var playlistToSearch = null;
              for (var i = 0; i < _currentConfiguration.content.interrupts.length; i++) {
                  var interrupt = _currentConfiguration.content.interrupts[i];
                  if (interrupt.trigger.triggerKey == "NEC ALP") {
                      // get the layout                        
                      playlistToSearch = interrupt.action.layout.zones[0].defaultPlaylist;
                      break;
                  }
              }

              if (playlistToSearch != null) {
                  var idConverter = new IDConverter()
                  var necALPInventory = [];

                  for (var i = 0; i < playlistToSearch.playlistMediaAssets.length; i++) {
                      var pma = playlistToSearch.playlistMediaAssets[i];

                      if (pma.mediaAsset.mimeType == 'application/widget')
                          necALPInventory.push(idConverter.getObjectId(pma.mediaAsset.metaData.media_asset_id));
                      else
                          necALPInventory.push(idConverter.getObjectId(pma.mediaAssetId));
                  }

                  res.send(necALPInventory);
              } else {
                  res.send({ error: "NEC ALP not configured for this player" })
              }
          });

          app.post('/nec-alp/soft-trigger', function(req, res, next) {
              if (_currentConfiguration == null) {
                  res.send({ error: "Configuration not loaded" });
                  return;
              }

              var success = false;
              var playerId = req.body.player;
              var requestedMediaAssetId = req.body.msg_id;

              var idConverter = new IDConverter()

              // validate if the asset exists
              var playlistToSearch = null;
              for (var i = 0; i < _currentConfiguration.content.interrupts.length; i++) {
                  var interrupt = _currentConfiguration.content.interrupts[i];
                  if (interrupt.trigger.triggerKey == "NEC ALP") {
                      // get the layout                        
                      playlistToSearch = interrupt.action.layout.zones[0].defaultPlaylist;
                      break;
                  }
              }

              if (playlistToSearch != null) {
                  var idConverter = new IDConverter()
                  var necALPInventory = [];

                  for (var i = 0; i < playlistToSearch.playlistMediaAssets.length; i++) {
                      var pma = playlistToSearch.playlistMediaAssets[i];
                      var mediaAssetId = idConverter.getObjectId(pma.mediaAssetId);

                      // if this is a widget - the media asset id is delivered by meta data as the id is the network widget id
                      if (pma.mediaAsset.mimeType == 'application/widget')
                          mediaAssetId = idConverter.getObjectId(pma.mediaAsset.metaData.media_asset_id);

                      if (mediaAssetId == requestedMediaAssetId) {

                          if(playlistToSearch.includeInProofOfPlay)
                              pma.includeInProofOfPlay = true;
                              
                          // tell the player to soft trigger the media asset that has been found
                          if (self.onEvent != undefined && self.onEvent != null)
                              self.onEvent('SoftTrigger', {
                                  pma: pma,
                                  timeLogged: new Date(),
                                  validity: 2000
                              });

                          success = true;
                          res.send(req.body);
                      }
                  }
              }

              if (!success)
                  res.send({ error: "Asset not found" });
          });
          
          app.get('/ping', function(req, res, next) {
              res.sendStatus(200);         
          });

      }

      self.updateConfiguration = function(configuration) {
          _currentConfiguration = configuration;
      }
  }

  return LocalAPIServer;
});
rjs.define('controllers/GPIOBasedInterruptsController',["controllers/InterruptController", "properties/App.Settings"], function(InterruptController, Settings) {
	function GPIOBasedInterruptsController(){

		var self = this;

		var _started = false;
		var _initialized = false;
		var _gpioPort = null;
		var _usbGPIOPort = null;

		/**
		 * initializes the module
		 * @param  {any initialization options for this module}
		 * @return {Jquery Promise}
		 */
		self.initialize = function(){
			var dfd = $.Deferred();

            logger.debug("Initializing GPIO Interrupt Controller");
            
            if(_gpioPort != null)
			{
				gpio.Close();
				gpio = null;
			}
			_gpioPort = new BSControlPort("BrightSign");

			_gpioPort.oncontrolup = onControlUp;

			_gpioPort.oncontroldown = onControlDown;
/*
			if(_usbGPIOPort != null)
			{
				_usbGPIOPort.Close();
				_usbGPIOPort = null;
			}
			
			_usbGPIOPort = new BSControlPort("Expander-0-GPIO");

			if(_usbGPIOPort)
			{
				_usbGPIOPort.oncontrolup = function(e)
				{
					logger.debug("USB GPIO Port " + e.code + " - UP");
					sendTrigger(e.code,"Up");
				}
	
				_usbGPIOPort.oncontroldown = function(e)
				{
					logger.debug("USB GPIO Port " + e.code + " - DOWN");
					sendTrigger(e.code,"Down");
				}	
			}
  */          
            _initialized = true;

            dfd.resolve();

			return dfd;
		};

		function onControlDown(e)
		{
			logger.debug("GPIO Port " + e.code + " - DOWN");
			sendTrigger(e.code,"Down");
		}

		function onControlUp(e)
		{
			logger.debug("GPIO Port " + e.code + " - UP");
			sendTrigger(e.code,"Up");
		}		

        function sendTrigger(port,state)
        {
            var triggerKey = "GPI" + port + state;
            InterruptController.triggerInterrupt(triggerKey, "KeyboardEvent", false, false).fail(function()
			{
				logger.debug("Interrupt not found, try cancelling");
				//Interrupt not found, is it a cancel?
				InterruptController.triggerCancel(triggerKey, "KeyboardEvent");
			})
        }
		/**
		 * Returns true if module is initizialized
		 * @return {Boolean}
		 */
		self.isInitialized = function()
		{
			return _initialized;
		};

		/**
		 * Returns true if sensor is available. In this case
		 * if initialization was successful we know the sensor was available.
		 * No other way to test this.
		 * @return {Boolean}
		 */
		self.isAvailable = function()
		{
			if (self.isInitialized())
				return true;
			else
				return false;
        };
        
		/**
		 * Returns true if supported in this client, false otherwise
		 * @return {Boolean}
		 */
		self.isSupported = function()
		{
			return true;
		};

		/**
		 * Returns true if the module is enabled, false otherwise
		 * @return {Boolean}
		 */
		self.isEnabled = function()
		{
			return true;
		};
	}
	
	return new GPIOBasedInterruptsController();
}); 
rjs.define('core/SLClientEngine',["properties/App.Settings", "properties/App.Properties", "system/io/File", "system/io/Directory", "system/io/DirectoryInfo", "system/network/NetworkInterface", "system/power/PowerInterface", "system/DateTime", "models/ConfigurationModel", "core/SLAPIInterface", "controllers/UserMessageController", "gui/views/MediaPlayerView", "controllers/ApplicationUpdateController", "controllers/FirmwareUpdateController", "controllers/UserConfigurationController", "system/remotecontrol/RemoteControlInterface", "models/KeyboardShortcutsModel", "controllers/InterruptController", "models/PlaybackStartEndReasons", "controllers/ScreenCaptureController", "controllers/WebBasedInterruptsController", "gui/views/DevicePropertiesView", "gui/views/SetupView", "controllers/RealTimeEventsController", "controllers/LocalAPIController","controllers/GPIOBasedInterruptsController","models/TimeZoneCollection", "system/graphics/GraphicsInterface", "system/registry/RegistryInterface", "controllers/ScreenTakeoverController"], function(Settings, Properties, File, Directory, DirectoryInfo, NetworkInterface, PowerInterface, DateTime, ConfigurationModel, SLAPIInterface, UserMessageController, MediaPlayerView, ApplicationUpdateController, FirmwareUpdateController, UserConfigurationController, RemoteControlInterface, KeyboardShortcutsModel, InterruptController, PlaybackStartEndReasons, ScreenCaptureController, WebBasedInterruptsController, DevicePropertiesView, SetupView, RealTimeEventsController, LocalAPIController, GPIOBasedInterruptsController, TimeZoneCollection, GraphicsInterface, RegistryInterface, ScreenTakeoverController) {
    function SLClientEngine(callbacks) {
        var self = this;
        var _callbacks = callbacks;
        var _apiInterface = null;
        var _currentConfiguration = null;
        var _previousConfiguration = null;
        var _messagePort = new BSMessagePort();
        var _mediaPlayerView = new MediaPlayerView({ showSetup: showSetupView }, _messagePort);
        var _pointerClass = require("@brightsign/pointer");
        var _pointer = new _pointerClass();
        var _appUpdateController = null;
        var _firmwareUpdateController = null;
        var _screenCapController = null;
        var _isFirstBoot = false;
        var _scheduledRebootTimer = null;
        var _modules = [{}];
        var _lastUserInputReceived = null;

        var _localAPIServer = null;
        self._screenTakeoverController = ScreenTakeoverController;

        // Used to manage Views - Initially both are not visible
        self.propertiesViewVisible = false;
        self.setupViewVisible = false;

        /**
         * Client initialization
         */
        self.initialize = function() {
            logger.info('Initializing SLClientEngine');
            logger.debug('Node Version: ' + process.version);

            // Setup Keyboard Shortcuts
            new KeyboardShortcutsModel().setup({
                showSetup: showSetupView,
                showInfo: showDeviceInformationView,
                default: onKeyPress
            });

            // Setup listening for remote
            if (Settings.application.features.canListenForRemoteKeyPresses)
                setupRemoteControlListener();

            UserConfigurationController.initialize().done(function() {
                // check if the screen configuration is valid
                checkScreenConfiguration();
                // is this the first run
                if (File.exists(Settings.application.fileSystem.systemFiles.settingsFilePath) == false)
                    firstBootHandler();
                else {
                    // check the date time is correct - for the specified time-zone?
                    if (Settings.user.settingsLoaded) {

                        // Load module data from local storage
                        //$.extend(true, Settings.application.modules, UserConfigurationController.modules());

                        // Initialize User Configuration Controller
                        if (Settings.application.features.canSyncDateTime) {

                            var userSettings = Settings.user.mySettings;
                            checkDateTimeIsCorrectFirstRun(userSettings);
                        } else {
                            initializeCoreServices();
                        }
                    } else {
                        // TODO critical error as there is no valid settings file
                        logger.error("Settings have not loaded");
                    }
                }
            }).fail(function() {
                logger.error("SLClientEngine: Failed to initialize User Configuration Controller.");
            });
        };

        function onKeyPress(e)
        {
            //Don't show interrupts when are using the setup view
            if (self.setupViewVisible)
                return;

            //e is from onKeyDown;
            if (Settings.application.features.canPlayInterrupts) {

                var key = null;

                if(e.key)
                    key = e.key;
                else 
                {
                    var keyCode = e.keyCode;
                    var chrCode = keyCode - 48 * Math.floor(keyCode / 48);
                    key = String.fromCharCode((96 <= keyCode) ? chrCode: keyCode);
                }

                //Convert numbers as necessary
                if(e.keyCode >= 48 && e.keyCode <= 57)
                {
                    key = "D" + key;
                } else if (!isNaN(key) && e.keyCode == 0)
                {
                    key = "NumPad" + key;
                }

                // Trigger the appropriate interrupt
                InterruptController.triggerInterrupt(key, "KeyboardEvent", false, false).fail(function() {
                    // If no such interrupt exists...
                    var kioskSettings = UserConfigurationController.interactiveKiosk();
                    if (kioskSettings.enabled && !InterruptController.isPlayingInterrupt() && key !== 'Control') { // ...kiosk is enabled, no interrupts are currently playing & the key is not 'CTRL'...
                        // ...then treat it as user interactivity and trigger the interactive kiosk content 
                        _lastUserInputReceived = DateTime.now();
                        InterruptController.triggerInterrupt(kioskSettings.trigger.key, kioskSettings.trigger.type, true, true).done(function() {
                            handleInterrupt(kioskSettings);
                        });
                    }
                })
            }
        }

        function onInterruptComplete() {
            if (self.setupViewVisible) {
                $("#" + setupView.id).remove();
                self.setupViewVisible = false;
            }
        };

        /**
         * Check date time on first run
         */
        function checkDateTimeIsCorrectFirstRun(userSettings) {
            try {

                if (Settings.application.features.canSyncDateTime) {
                    // the date time is incorrect - tell the user they need to connect the unit to the internet
                    UserMessageController.postMessage('Checking the date time is correct');

                    var jqXHR = $.ajax({
                        url: Settings.application.dateTimeServer + userSettings.timeZone.id,
                        contentType: 'application/json'
                    }).done(function(data, textStatus, jqXHR) {
                        logger.debug('Local Time for ' + data.time_zone + ' is ' + data.local_time);

                        var isDateTimeCorrect = true;

                        var twoMinsForwards = DateTime.now().addMinutes(2);
                        var twoMinsBackwards = DateTime.now().addMinutes(-2);

                        var serverTime = DateTime.parse(data.local_time);

                        if (serverTime > twoMinsBackwards & serverTime < twoMinsForwards)
                            isDateTimeCorrect = true;
                        else
                            isDateTimeCorrect = false;

                        if (isDateTimeCorrect) {

                            // init time sync check
                            initializeTimeSync();

                            // init media player
                            initializeCoreServices();

                        } else {
                            logger.debug("Resyncing date time on the panel");
                            UserMessageController.postMessage('Resetting the date time, your panel may automatically restart in a moment');

                            DateTime.setDateTime(DateTime.parse(data.local_time))
                                .done(function() {
                                    logger.debug("Date time successfully updated.");

                                    // Everything worked so continue to load client

                                    // init time sync check
                                    initializeTimeSync();

                                    // init media player
                                    initializeCoreServices();
                                })
                                .fail(function() {
                                    logger.error("There was an error updating the date time on the panel. Retrying in 30 seconds...");

                                    // retry in 30 seconds
                                    setTimeout(function() {
                                        checkDateTimeIsCorrect();
                                    }, 30000);
                                });
                        }
                    }).fail(function(jqXHR, textStatus, errorThrown) {

                        if (DateTime.now() <= DateTime.parse('2013-10-01T00:00:00')) {
                            // the date time is incorrect - tell the user they need to connect the unit to the internet
                            UserMessageController.postMessage('The date time is incorrect, please re-connect this panel to the internet to automatically re-sync the date time');

                            checkDateTimeIsCorrect();
                        } else {
                            // attempt to initalize the media player
                            initializeCoreServices();
                        }
                    });
                } else {
                    // init media player
                    logger.debug("Cannot sync date time for this player type. Starting Core services...");
                    initializeCoreServices();
                }
            } catch (e) {
                logger.error('Date time could not be checked');
            }
        }

        /**
         * Starts the time sync process
         */
        function initializeTimeSync() {
            setTimeout(function() {
                checkDateTimeIsCorrect();
            }, 3600000);
        }

        /**
         * Starts the player initialization after first boot
         */
        self.firstBootSequenceComplete = function() {
            initializeCoreServices();
        };

        /**
         * Initializes the player
         */
        function initializeCoreServices() {

            rotateCanvas();

            // by default delay the API interface by 10 seconds
            // to give the media player an opportunity to start playing content
            var apiInterfaceStartDelay = 10000;
            _apiInterface = new SLAPIInterface();

            // check if the configuration file exists - if it does load it and start the media player
            if (File.exists(Settings.application.fileSystem.systemFiles.lastConfigFilePath)) {
                logger.debug('Loading last configuration');
                try {
                    File.read(Settings.application.fileSystem.systemFiles.lastConfigFilePath).done(function(configurationData) {

                        // Store configuration from disk in memory
                        _currentConfiguration = new ConfigurationModel(JSON.parse(configurationData));

                        if (Settings.application.features.canTakeScreenshots) {
                            if (_screenCapController == null)
                                _screenCapController = new ScreenCaptureController();
                        }

                        // Delete old files on startup - This only does anything if Settings.application.features.deleteOldFilesOnStartup = true in the
                        // client-specific configuration.
                        // NOTE: we always continue even if the file deletion fails as we do not want to prevent media playback from starting
                        deleteOldFilesOnStartUp().always(function() {

                            // If interrupts are supported then load then initialize the interrupt controller, check whether screen control is enabled and atart media playback
                            // Else just check whether screen control is enabled and start media playback
                            if (Settings.application.features.canPlayInterrupts) {
                                InterruptController.setCallback(onInterruptComplete);
                                InterruptController.initialize(_mediaPlayerView, _currentConfiguration.content.interrupts)
                                    .always(function() {
                                        GPIOBasedInterruptsController.initialize()
                                        .always(function(){
                                        // Setup Screen Control
                                            // Only Setup screen control if it is a supported feature of this app
                                            // If screen control is enabled the media player will be started in the method that switches the screen on.
                                            if (Settings.application.features.canSwitchScreenOnOff) {
                                                setupScreenOnOffController();
                                            }
                                            // otherwise just start the media player
                                            else {
                                                startMediaPlayer();
                                            }
                                        });     
                                    });
                            } else {
                                // Setup Screen Control
                                // Only Setup screen control if it is a supported feature of this app
                                // If screen control is enabled the media player will be started in the method that switches the screen on.
                                if (Settings.application.features.canSwitchScreenOnOff) {
                                    setupScreenOnOffController();
                                }
                                // Otherwise start the media player in the standard way
                                else {
                                    startMediaPlayer();
                                }
                            }

                            //Set networking
                            if (Settings.application.features.canConfigureNetworking) {
                                setupNetworking();
                            }

                            // Start Scheduled reboots process
                            setupScheduledReboots();

                            //Scheduled screenshots
                            setupScheduledScreenshots();

                            // Start API Interface
                            startAPIInterface(apiInterfaceStartDelay);

                            // Start real Time Event Controller
                            startRealTimeEventsController();

                            // start the local API
                            startLocalAPI();

                            //In case the system time changes
                            startClockMonitor();
                        });
                    });
                } catch (e) {
                    logger.error('An error occurred loading configuration data at startup ' + e.message);

                    // start the API interface immediately as there is nothing configured on the display
                    apiInterfaceStartDelay = 0;
                    startAPIInterface(apiInterfaceStartDelay);
                }
            } else {
                logger.debug('No configuration to load');

                // start the API interface immediately as there is nothing configured on the display
                apiInterfaceStartDelay = 0;
                startAPIInterface(apiInterfaceStartDelay);

            }

            // Start Application Update Controller
            if (Settings.application.features.canPerformApplicationUpdates) {
                _appUpdateController = new ApplicationUpdateController();
                _appUpdateController.start();
            }

            // Start Firmware Update Controller
            if (Settings.application.features.canPerformFirmwareUpdates) {
                _firmwareUpdateController = new FirmwareUpdateController(_apiInterface);
                _firmwareUpdateController.start();
            }

            // Setup Screen Capture Controller
            if (Settings.application.features.canTakeScreenshots) {
                if (_screenCapController == null) {
                    _screenCapController = new ScreenCaptureController();
                }
            }

            if (Settings.application.features.canListenForWebBasedInterrupts) {
                //Web Based Interrupts
                setupWebBasedInterrupts();
            }

        }

        function deleteOldFilesOnStartUp() {
            var dfd = $.Deferred();

            _screenCapController.reset().always(function()
            {
                if (Settings.application.features.deleteOldFilesOnStartup) {
                    logger.debug('Removing old files...');
                    removeOldMediaFilesFromStorage().done(function() {
                       
                            dfd.resolve();
                                         
                    }).fail(function() {
                        logger.error('Error removing old files.');
                        dfd.reject();
                    });
                } else {
                    setTimeout(function() {
                        logger.debug('Player not configured to clear storage on startup');
                        dfd.resolve();
                    }, 50);
                }
            });  
    
            return dfd;
        }

        /**
         * Starts the API Interface
         */
        function startAPIInterface(apiInterfaceStartDelay) {
            // Start Api Interface
            //_apiInterface = new SLAPIInterface();
            setTimeout(function() {
                _apiInterface.initialize(_currentConfiguration, {
                    deviceRegistered: deviceRegisteredCallbackHandler,
                    deviceAlreadyRegistered: deviceAlreadyRegisteredCallbackHandler,
                    configuration: configurationCallbackHandler,
                    configurationNoChange: configurationNoChangeCallbackHandler
                });

                if(_currentConfiguration != null && _currentConfiguration.firmwareUpdate != null)
                {
                    //This will check the current firmware version and either do the update or register an update complete
                    logger.debug("Checking for firmware update required on bootup");
                    doFirmwareUpdate(_currentConfiguration.firmwareUpdate);
                }

            }, apiInterfaceStartDelay);

            // Initialize the Proof of Play Logger
            if (_currentConfiguration != null && _currentConfiguration.proofOfPlayConfig != null) {
                proofOfPlayLogger.init(_currentConfiguration.proofOfPlayConfig, Settings.application.api);
            }
        }

        function startRealTimeEventsController() {
            RealTimeEventsController.updateConfiguration(_currentConfiguration);
        }

        function startLocalAPI() {
            if (_localAPIServer != null)
                return;

            _localAPIServer = new LocalAPIController(_currentConfiguration);
            _localAPIServer.initialize();

            _localAPIServer.onEvent = function(type, data) {

                switch (type) {
                    case "SoftTrigger":
                        _mediaPlayerView.softTrigger(data);
                        break;
                }
            }
        }

        /**
         * [deviceRegisteredCallbackHandler description]
         */
        function deviceRegisteredCallbackHandler(clientKey) {
            logger.debug('Registration Callback ' + clientKey);

            var d = JSON.stringify({
                clientKey: clientKey
            });
            File.write(Settings.application.fileSystem.systemFiles.clientKeyFilePath, d);

            // remember the client key
            Settings.application.api.clientKey = clientKey;
        }

        /**
         * Callback that is called when the device is already registered. Shows a message.
         */
        function deviceAlreadyRegisteredCallbackHandler() {
            UserMessageController.postMessage('This player has been previously connected to Signagelive, please deactivate it to continue with re-activation.\n ID: ' + Settings.application.api.clientId);
        }

        /**
         * This will run after every configuration head request IF there are no changes
         */
        function configurationNoChangeCallbackHandler() {
            logger.info("Configuration checked - no update");

            uploadStoredScreenshots();

            if (Settings.application.features.canListenForWebBasedInterrupts) {
                setupWebBasedInterrupts();

                //If running, check for config update
                if (WebBasedInterruptsController.isRunning()) {
                    WebBasedInterruptsController.checkForConfigurationUpdate();
                }
            }
        }


        /**
         * Callback method called after a configuration update
         */
        function configurationCallbackHandler(configuration) {
            logger.info("Configuration checked - updating configuration");

            //logger.debug("New Configuration: " + JSON.stringify(configuration));

            // remember the previous config as we need this to handle any asset delete events
            _previousConfiguration = _currentConfiguration;

            // Stored updated configuration in memory
            _currentConfiguration = configuration;

            //Check to see if the timezone has been configured by the user
            var tzString = _currentConfiguration.clientDetails.timeZone;

            if(tzString)
            {
                if(UserConfigurationController.timeZone().id != tzString)
                {
                    logger.debug("Timezone received from cloud, check againt current tz");
                    var timezoneMatch = $.grep(TimeZoneCollection, function(tz, index) {
                        return tz.id == tzString;
                    });

                    if (timezoneMatch.length == 1)
                    {
                        logger.debug("Timezone found in collection, saving...");
                        UserConfigurationController.timeZone(timezoneMatch[0]);

                        DateTime.updateTimeZone(timezoneMatch[0]).done(function(rebootRequired) {
                            UserConfigurationController.save().done(function(){
                                if (rebootRequired && Settings.application.features.canReboot) {
                                    //Reboot
                                    PowerInterface.reboot();
                                }
                            }).fail(function()
                            {
                                logger.error("Error saving settings");
                            });
                        });
                    }
                }
            }

            // If interrupts are supported then load then initialize the interrupt controller, check whether screen control is enabled and atart media playback
            // Else just check whether screen control is enabled and start media playback
            if (Settings.application.features.canPlayInterrupts) {
                InterruptController.setCallback(onInterruptComplete);
                InterruptController.initialize(_mediaPlayerView, _currentConfiguration.content.interrupts)
                    .always(function() {
                        // Setup Screen Control
                        // Only Setup screen control if it is a supported feature of this app
                        // If screen control is enabled the media player will be started in the method that switches the screen on.
                        if (Settings.application.features.canSwitchScreenOnOff) {
                            setupScreenOnOffController();
                        }
                        // otherwise just start the media player
                        else {
                            startMediaPlayer();
                        }
                    });
            } else {
                // Setup Screen Control
                // Only Setup screen control if it is a supported feature of this app
                // If screen control is enabled the media player will be started in the method that switches the screen on.
                if (Settings.application.features.canSwitchScreenOnOff) {
                    setupScreenOnOffController();
                }
                // Otherwise start the media player in the standard way
                else {
                    startMediaPlayer();
                }
            }

            //Set networking
            if (Settings.application.features.canConfigureNetworking) {
                setupNetworking();
            }

            // Update Scheduled reboots process
            setupScheduledReboots();

            // Scheduled screenshots
            setupScheduledScreenshots();

            // Schedule cleanup of the local storage
            if (Settings.application.features.deleteOldFilesOnContentUpdate) {
                setTimeout(function() {
                    removeOldMediaFilesFromStorage();
                }, 15000);
            }


            //Taking an ond deman screenshot in 20 seconds
            if (_currentConfiguration.logging.screencapture.screenshotAtNextContentCheck && _screenCapController != null) {
                setTimeout(function() {
                    logger.info("Taking on demand screenshot");
                    _screenCapController.takeOnDemandScreenshot();
                }, 20000);
            }

            var instantReboot = false;
            // If an instant reboot is required then shedule it 30 seconds from now
            if (_currentConfiguration.connectivity.instantReboot) {
                logger.debug("Performing instant reboot in 30 seconds.");
                instantReboot = true;
                setTimeout(function() {
                    PowerInterface.reboot();
                }, 30000);
            } else {
                //No instant reboot

                //Upload screenshots
                uploadStoredScreenshots();
            }

            // Initialize the Proof of Play Logger
            if (_currentConfiguration.proofOfPlayConfig != null) {
                logger.debug("Config Update: Updating Proof of Play config");
                proofOfPlayLogger.updateConfig(_currentConfiguration.proofOfPlayConfig, Settings.application.api);
            } else {
                logger.debug("Config Update: Proof of Play Configuration is null");
            }

            // Check firmware updates - if instant reboot is scheduled, we will check for firmware update on a reboot.
            if (_currentConfiguration.firmwareUpdate != null && !instantReboot) {
                doFirmwareUpdate(_currentConfiguration.firmwareUpdate);
            }

            if (Settings.application.features.canListenForWebBasedInterrupts) {
                setupWebBasedInterrupts();

                //If running, check for config update
                if (WebBasedInterruptsController.isRunning()) {
                    WebBasedInterruptsController.checkForConfigurationUpdate();
                }
            }

            
            // Notify Real Time Event Controller of any changes
            RealTimeEventsController.updateConfiguration(_currentConfiguration);

            if (_localAPIServer == null)
                startLocalAPI();

            // Notify local API of Config Change
            _localAPIServer.updateConfiguration(_currentConfiguration);

            
        }

        function setupNetworking() {
            NetworkInterface.SetNetworkingConfiguration(_currentConfiguration.network);
        }

        function doFirmwareUpdate(firmwareUpdate) {
            var updateRequired = _firmwareUpdateController.isFirmwareUpdateRequired(firmwareUpdate);
            if (updateRequired) {
                _firmwareUpdateController.updateFirmware(firmwareUpdate);
            }
            else {
                //Send the update notification in 15 seconds
                setTimeout(function()
                {
                    _firmwareUpdateController.cancelUpdate();
                },15000);              
            }
        }

        function setupWebBasedInterrupts() {

            var interruptsAvailable = false; // _currentConfiguration.content.interrupts.length > 0;

            if (_currentConfiguration && _currentConfiguration.content.interrupts.length > 0) {

                //We have interrupts, set up WBI
                if (WebBasedInterruptsController.isRunning() == false) {
                    logger.info("Interrupts available - start web based interrupts controller");
                    var options = {
                        url: Settings.application.webBasedInterrupts.url,
                        configurationCheckInterval: Settings.application.webBasedInterrupts.configurationCheckInterval,
                        serialNo: _currentConfiguration.clientDetails.signageliveSerialNumber
                    };

                    WebBasedInterruptsController.initialize(options).done(function() {

                    }).fail(function() {
                        logger.error("Error: Failed to initialize web based interrupts controller");
                    });
                } else {
                    logger.info("Web based interrupts controller is already running");
                }
            } else {
                logger.info("No interrupts available - will not start web based interrupts controller");

                if (WebBasedInterruptsController.isInitialized()) {
                    logger.info("WebBasedInterruptsController running - stopping");
                    WebBasedInterruptsController.stop();
                }
            }

        }

        function stopWebBasedInterrupts() {
            if (WebBasedInterruptsController.isInitialized() == true) {
                WebBasedInterruptsController.stop();
            }
        }


        /**
         *  Upload screenshots
         */
        function uploadStoredScreenshots() {
            if (_screenCapController != null) {
                setTimeout(function() {
                    _screenCapController.uploadAllScreenshots();
                }, 30000);
            }
        }

        /**
         * Setup scheduled screenshots
         */
        function setupScheduledScreenshots() {
            logger.info("Setting up scheduled screenshots");
            if (Settings.application.features.canTakeScreenshots) {
                if (_screenCapController == null)
                    _screenCapController = new ScreenCaptureController();

                // Wait 5 seconds before restarting screenshots
                setTimeout(function() {
                    logger.info("Stopping and starting screenshots");
                    try {
                        // Stop the controller regardless
                        _screenCapController.stop();

                        // Supply the new config
                        _screenCapController.initialize(_currentConfiguration);

                        // Restart the controller - the controller will do nothing if it is not enabled
                        _screenCapController.start();
                    } catch (e) {
                        logger.error("Error configuring screenshots: " + e.message);
                    }
                }, 5000);
            }
        }

        var _screenOnOffTimer = 0;

        /**
         * [setupScreenOnOffController description]
         */
        function setupScreenOnOffController() {
            // clear any previous screen control timer
            clearTimeout(_screenOnOffTimer);

            // switch the screen on/off according to the current setting
            if (_currentConfiguration.screenControl.isScreenCurrentlyScheduledOn()) {
                switchScreenOn();
            } else {
                switchScreenOff();
            }

            // are there any schedules?
            if (_currentConfiguration.screenControl.nextAction() != null) {
                setUpScreenOnOffTimer();
            }
        }

        /**
         * Setup Screen Control timer
         */
        function setUpScreenOnOffTimer() {
            var nextAction = _currentConfiguration.screenControl.nextAction();

            if (nextAction == null)
                return;

            // calculate the time to the next action
            var interval = nextAction.time.subtract(DateTime.now());

            if (nextAction.action == 'on') {
                // set the on timer
                _screenOnOffTimer = setTimeout(function() {
                    switchScreenOn();

                    setUpScreenOnOffTimer();
                }, interval);
            } else {
                // set the off timer
                _screenOnOffTimer = setTimeout(function() {
                    switchScreenOff();

                    setUpScreenOnOffTimer();
                }, interval);
            }
        }

        /**
         * Screen Control - Turn Screen On
         */
        function switchScreenOn() {

            // Use PowerInterface to switch the screen on
            PowerInterface.switchScreenOn()
                .done(function() {

                    // Restart the media player
                    startMediaPlayer();
                });
        }

        /**
         * Screen Control - Turn screen off
         */
        function switchScreenOff() {

            // Stop the media player
            stopMediaPlayer(PlaybackStartEndReasons.SCREEN_OFF);

            // Use PowerInterface to switch the screen off
            PowerInterface.switchScreenOff().done(function() {
                // Do nothing
            });
        }

        /**
         * Checks the date time is correct
         */
        function checkDateTimeIsCorrect() {
            try {
                logger.debug('Checking date time is correct');
                // check the date time is correct - for the specified time-zone?
                var settingsData = File.read(Settings.application.fileSystem.systemFiles.settingsFilePath);
                var settings = JSON.parse(settingsData);
                $.ajax({
                    url: Settings.application.dateTimeServer + settings.timeZone.id,
                    contentType: 'application/json'
                }).done(function(data, textStatus, jqXHR) {
                    logger.debug('Local Time for ' + data.time_zone + ' is ' + data.local_time);

                    var isDateTimeCorrect = true;

                    var twoMinsForwards = DateTime.now().addMinutes(2);
                    var twoMinsBackwards = DateTime.now().addMinutes(-2);

                    var serverTime = DateTime.parse(data.local_time);

                    if (serverTime > twoMinsBackwards & serverTime < twoMinsForwards)
                        isDateTimeCorrect = true;
                    else
                        isDateTimeCorrect = false;

                    if (!isDateTimeCorrect) {

                        logger.debug('DateTime is incorrect... resetting it...');

                        // Set DateTime
                        DateTime.setDateTime(DateTime.parse(data.local_time))
                            .done(function() {
                                logger.debug("Date time successfully updated.");
                                // check in specified frequency
                                setTimeout(function() {
                                    checkDateTimeIsCorrect();
                                }, Settings.application.features.syncDateTimeFrequency * 1000);
                            })
                            .fail(function() {
                                logger.error("There was an error updating the date time on the panel.");
                                // check in 30 seconds
                                setTimeout(function() {
                                    checkDateTimeIsCorrect();
                                }, 30000);
                            });

                    } else {
                        logger.debug('DateTime is correct');
                        // check in specified frequency
                        setTimeout(function() {
                            checkDateTimeIsCorrect();
                        }, Settings.application.features.syncDateTimeFrequency * 1000);

                    }
                }).fail(function() {
                    // check in 30 seconds
                    setTimeout(function() {
                        checkDateTimeIsCorrect();
                    }, 30000);
                });
            } catch (e) {
                // check in 30 seconds
                setTimeout(function() {
                    checkDateTimeIsCorrect();
                }, 30000);
            }
        }

        /**
         * Removes old media files
         * !! Note that this method needs to implement an interface so that each app can have their own implementation
         * as there is platform specific code here.
         */
        function removeOldMediaFilesFromStorage() {

            var processDfd = $.Deferred();

            try {
                if (_currentConfiguration == null || Settings.application.features.canDownloadFiles == false)
                    return;

                logger.debug('Checking directory: ' + Settings.application.fileSystem.root + "/" + Settings.application.fileSystem.mediaDirectory);

                // get a list of all the files in the media directory
                var mediaDirPath = "";
                if (Settings.application.fileSystem.root !== "") {
                    mediaDirPath = mediaDirPath + Settings.application.fileSystem.root + "/";
                }
                if (Settings.application.fileSystem.mediaDirectory !== "") {
                    mediaDirPath = mediaDirPath + Settings.application.fileSystem.mediaDirectory;
                }

                var mediaDirectory = new DirectoryInfo(mediaDirPath);

                mediaDirectory.initialize()
                    .done(function() {

                        logger.info('There are ' + mediaDirectory.files.length + ' files in the media directory');

                        var filesToDeleteCount = 0;

                        // see if each file is in the list of media assets
                        var filesToDelete = [];
                        $(mediaDirectory.files).each(function(i, file) {
                            if (file.type != 'folder') {
                                logger.info('Checking if ' + file.name + ' is required');

                                var mediaAssets = $.grep(_currentConfiguration.content.mediaAssets, function(mediaAsset, index) {
                                    return mediaAsset.localFileName === file.name;
                                });

                                if (mediaAssets.length != 1) {

                                    var mediaAssetToDelete = null;
                                    // find the media asset in the previous config
                                    if (_previousConfiguration != null) {

                                        var mediaAssetsInPreviousConfig = $.grep(_previousConfiguration.content.mediaAssets, function(ma, index) {
                                            return ma.localFileName === file.name;
                                        });

                                        if (mediaAssetsInPreviousConfig.length > 0)
                                            mediaAssetToDelete = mediaAssetsInPreviousConfig[0];

                                    }
                                    filesToDelete.push({ mediaAsset: mediaAssetToDelete, file: file });
                                    filesToDeleteCount++;
                                }
                            }
                        });

                        logger.info('There are ' + filesToDeleteCount + ' to delete');

                        // delete each file synchronously
                        var dfd = $.Deferred().resolve();
                        filesToDelete.forEach(function(fileToDelete) {
                            dfd = dfd.then(function() {
                                return deleteOldFile(fileToDelete);
                            });
                        });

                        // report that deletion process has completed
                        dfd.done(function() {
                            logger.debug('Old files deleted.');
                            processDfd.resolve();
                        });
                    })
                    .fail(function() {
                        logger.error('Error loading directory information for ' + mediaDirPath);
                        processDfd.resolve();
                    });
            } catch (e) {
                logger.error('Error whilst removing files that are not required from the media directory: ' + e.message);
                processDfd.reject();
            }

            return processDfd;
        }

        // Delete a single media file
        function deleteOldFile(fileToDelete) {
            var dfd = $.Deferred();
            var file = fileToDelete.file;

            setTimeout(function() {
                try {
                    // This is a widget - need to delete the file and the extracted widget directory
                    if (file.path.indexOf('.wgt') != -1) {
                        // Delete .wgt file
                        logger.info('File ' + file.name + ' is to be deleted.');

                        // !!!!! Temp solution for LG gapless files do not get deleted
                        if (file.name.lastIndexOf("gaplessPlaylist_", 0) === 0) {
                            logger.debug("Not deleting: " + file.name + " as this is a gapless playlist file.");
                            dfd.resolve();
                        } else {
                            file.delete().always(function(deleteSuccess) {
                                // Report file delete success
                                if (deleteSuccess) {
                                    logger.debug(file.name + ' deleted.');

                                    // Log external systems this has been deleted
                                    if (fileToDelete.mediaAsset != null)
                                        RealTimeEventsController.sendMediaAssetRemoved(fileToDelete.mediaAsset);
                                } else {
                                    logger.warning('Warning: ' + file.name + ' not deleted.');
                                }

                                // Delete extracted .wgt directory
                                logger.debug('File was a widget. Delete the widget files');
                                var start = file.path.lastIndexOf('/') + 1;
                                var widgetName = file.path.substr(start).replace('.wgt', '');
                                var widgetDir = "";
                                if (Settings.application.fileSystem.root !== "") {
                                    widgetDir = widgetDir + Settings.application.fileSystem.root + "/";
                                }
                                if (Settings.application.fileSystem.widgetDirectory !== "") {
                                    widgetDir = widgetDir + Settings.application.fileSystem.widgetDirectory + '/';
                                }
                                widgetDir = widgetDir + widgetName;
                                Directory.deleteDirectory(widgetDir).always(function() {
                                    // resolve delete
                                    dfd.resolve();
                                });
                            });

                        }
                    }
                    // Everything else - just delete the file
                    else {
                        // !!!!! Temp solution for LG gapless files do not get deleted
                        if (file.name.lastIndexOf("gaplessPlaylist_", 0) === 0) {
                            logger.debug("Not deleting: " + file.name + " as this is a gapless playlist file.");
                            dfd.resolve();
                        } else {
                            logger.info('File ' + file.name + ' is to be deleted.');
                            file.delete().always(function(deleteSuccess) {
                                // Report file delete success
                                if (deleteSuccess) {
                                    logger.debug(file.name + ' deleted.');

                                    // Log external systems this has been deleted
                                    if (fileToDelete.mediaAsset != null)
                                        RealTimeEventsController.sendMediaAssetRemoved(fileToDelete.mediaAsset);
                                } else {
                                    logger.warning('Warning: ' + file.name + ' not deleted.');
                                }
                                dfd.resolve();
                            });
                        }
                    }
                } catch (e) {
                    logger.error('Error deleting ' + file.name + ' error: ' + e.message);
                    dfd.resolve();
                }
            }, 1000);

            return dfd.promise();
        }

        /**
         * Configures remote control listener
         */
        function setupRemoteControlListener() {

            // give focus to the remote focus anchor to capture events from the remote control
            $('#remoteFocus').focus();

            RemoteControlInterface.initialize();

            // Handle Key Events
            $('#remoteFocus').keydown(function(event) {
                var keyCode = event.keyCode;

                switch (keyCode) {
                    case RemoteControlInterface.keys.KEY_RW:
                        logger.info('RW key pressed');
                        _mediaPlayerView.back();

                        break;

                    case RemoteControlInterface.keys.KEY_FF:
                        logger.info('FF key pressed');
                        _mediaPlayerView.skip();

                        break;

                    case RemoteControlInterface.keys.KEY_PLAY:
                        logger.info('PLAY key pressed');
                        _mediaPlayerView.play();

                        break;

                    case RemoteControlInterface.keys.KEY_PAUSE:
                        logger.info('PAUSE key pressed');
                        //_mediaPlayerView.pause();

                        try {
                            var screenCap = new ScreenCaptureController();

                            //screenCap.takeOnDemandScreenshot();
                            screenCap.takeScreenshot();
                        } catch (e) {
                            logger.error(e.message);
                        }

                        break;

                    case RemoteControlInterface.keys.KEY_MUTE:
                        logger.info('MUTE key pressed');
                        UserConfigurationController.toggleMute();

                        break;

                    case RemoteControlInterface.keys.KEY_VOL_UP:
                        logger.info('VOL_UP key pressed');
                        UserConfigurationController.volumeUp();

                        break;

                    case RemoteControlInterface.keys.KEY_VOL_DOWN:
                        logger.info('VOL_DOWN key pressed');
                        UserConfigurationController.volumeDown();

                        break;

                    case RemoteControlInterface.keys.KEY_INFO:
                        logger.info('INFO key pressed');

                        // If it isnt already being shown then show the device Information View
                        if (!self.propertiesViewVisible)
                            showDeviceInformationView();

                        break;

                    case RemoteControlInterface.keys.KEY_SETUP:
                        logger.info('SETUP key pressed');

                        // Show setup view
                        if (!self.setupViewVisible)
                            showSetupView();

                        break;
                    case RemoteControlInterface.keys.KEY_ENTER:
                        logger.info('ENTER key pressed');
                        try {


                            var screenCap = new ScreenCaptureController();

                            //screenCap.takeOnDemandScreenshot();
                            screenCap.takeScreenshot();
                        } catch (e) {
                            logger.error(e.message);
                        }
                        break;
                    default:
                        logger.info("Key pressed: " + keyCode);
                        // Do Nothing
                        break;
                }
            });
        }

        /**
         * Shows setup view
         */
        function showSetupView() {

            if (self.setupViewVisible)
                return;

            // Stop the media player
            stopMediaPlayer(PlaybackStartEndReasons.SETUP_SCREEN);

            // Remove the box that opens the settings menu
            //removeSettingsSelectionSquare();

            // Configure new Setup View
            var setupView = new SetupView({
                complete: function(updatedSettings) {

                    // Hide the Setup View
                    $("#" + setupView.id).remove();
                    self.setupViewVisible = false;

                    // Call the setupCompleteHandler and pass updated settings
                    setupCompleteHandler(updatedSettings);
                },
                cancel: function() {

                    // Hide the Setup View
                    $("#" + setupView.id).remove();
                    self.setupViewVisible = false;

                    // restart media player
                    startMediaPlayer();
                }
            });

            // Add to Canvas
            self.setupViewVisible = true;
            $('#canvas').append(setupView.html);
            setupView.addedToCanvas();
        }

        /**
         * Shows transparent device information overlay
         */
        function showDeviceInformationView() {

            if (self.propertiesViewVisible)
                return;

            // Instantiate new DevicePropertiesView and initialize it (load the model and HTML)
            var propertiesView = new DevicePropertiesView();
            propertiesView.initialize().done(function() {

                // initialized so show it
                self.propertiesViewVisible = true;
                $('#canvas').append(propertiesView.html);

                // And then hide it automatically after 15000 millisecodnds
                setTimeout(function() {
                    self.propertiesViewVisible = false;
                    $('#' + propertiesView.id).remove();
                }, 15000);
            });
        }

        /**
         * Called only on firstboot. Shows the SetupView.
         */
        function firstBootHandler() {

            logger.debug("First boot Handled Called");
            _isFirstBoot = true;

            var setupView = new SetupView({
                complete: function(updatedSettings) {
                    $("#" + setupView.id).remove();
                    setupCompleteHandler(updatedSettings);
                }
            }, false);

            $('#canvas').append(setupView.html);
            setupView.addedToCanvas();
        }

        /**
         * Callback that gets called after setup is completed
         */
        function setupCompleteHandler(updatedSettings) {
            logger.debug('Settings complete ' + JSON.stringify(updatedSettings));

            try {
                updatePtpRegistry(updatedSettings).then(function (ptpRebootRequired) {
                    setupScreenConfiguration(updatedSettings).then(function (configUpdated) {
                        UserConfigurationController.videoTransparency(updatedSettings.videoTransparency);
                        UserConfigurationController.interactiveKiosk(updatedSettings.interactiveKiosk);
                        UserConfigurationController.gaplessWidgetTransitions(updatedSettings.gaplessWidgetTransitions);
                        UserConfigurationController.alwaysEmbedWebpages(updatedSettings.alwaysEmbedWebpages);

                        // if the current muted value has changed, update it
                        if (UserConfigurationController.isMuted() != updatedSettings.isMuted) {
                            UserConfigurationController.toggleMute();
                        }

                        UserConfigurationController.audio(updatedSettings.audio);
                        UserConfigurationController.modules(updatedSettings.modules);

                        if (updatedSettings.rotateVideos !== undefined) {
                            UserConfigurationController.rotateVideos(updatedSettings.rotateVideos);
                        }

                        // Else if it is the first boot OR Timezone settings have changed
                        if (_isFirstBoot || updatedSettings.timeZone.id != UserConfigurationController.timeZone().id) {
                            logger.debug('Timezone updated.. Rechecking DateTime...');

                            if (_isFirstBoot || (updatedSettings.timeZone.id != UserConfigurationController.timeZone().id)) {
                                //We make sure we set the timezone on the first boot!    
                                // save the timezone
                                UserConfigurationController.timeZone(updatedSettings.timeZone);

                                DateTime.updateTimeZone(updatedSettings.timeZone).done(function (rebootRequired) {
                                    UserConfigurationController.save().done(function() {
                                        if ((ptpRebootRequired || rebootRequired || configUpdated) && Settings.application.features.canReboot) {
                                            //Reboot
                                            PowerInterface.reboot();
                                            //Don't need to do anything else really!
                                        } else {
                                            if (_isFirstBoot) {
                                                _isFirstBoot = false;
                                                self.firstBootSequenceComplete();
                                            } else
                                                startMediaPlayer();
                                        }
                                    }).fail(function () {
                                        logger.error("Error saving settings");
                                    });
                                }).fail(function () {
                                    setTimeout(function () {
                                        retryUpdateDateTime(updatedSettings, true);
                                    }, 30000);
                                });
                            } else {
                                //Timezone not changed, sync date time only              
                                DateTime.syncDateTime(updatedSettings.timeZone.id)
                                    .done(function () {
                                        UserConfigurationController.save();

                                        if (ptpRebootRequired || configUpdated) {
                                            PowerInterface.reboot();
                                        };

                                        if (_isFirstBoot) {
                                            _isFirstBoot = false;
                                            self.firstBootSequenceComplete();
                                        } else
                                            startMediaPlayer();

                                    }).fail(function () {
                                        setTimeout(function () {
                                            retryUpdateDateTime(updatedSettings, false);
                                        }, 30000);
                                    });
                            }
                        }
                        // Otherwise just restart playback (as we now stop playback when the setup screen is showing)
                        else {
                            //Save the updated settings as we won't have done it yet
                            UserConfigurationController.save().done(function () {
                                if ((ptpRebootRequired || configUpdated) && Settings.application.features.canReboot) {
                                    //Reboot
                                    PowerInterface.reboot();
                                } else {
                                    // Start media player
                                    startMediaPlayer();
                                }
                            });
                        }
                    });
                });
            } catch (e) {
                logger.error('Error whilst saving settings : ' + e.message);
                setTimeout(function() {
                    retryUpdateDateTime(updatedSettings, true);
                }, 30000);
            }
        }

        function checkScreenConfiguration() {
            var dfd = $.Deferred();
            if(Settings.user.mySettings.isScreenConfigInvalid == true) {
                UserConfigurationController.screenConfig(Settings.user.mySettings.screenConfig);
                UserConfigurationController.save();
            } 
            
            dfd.resolve();

            return dfd;            
        };

        function setupScreenConfiguration(updatedSettings) {
            var dfd = $.Deferred();

            // if a Series 5 single output player and rotation has been updated..
            if (Settings.application.features.deviceSeries == 5 && JSON.stringify(updatedSettings.screenConfig) !== JSON.stringify(GraphicsInterface.screens)) {

                GraphicsInterface.setScreenModes(updatedSettings.screenConfig)
                    .then(function (requiresReboot, error) {
                        if(error) {
                            logger.info("Screen config does not need to change...");
                            dfd.resolve(false);
                        } else {
                            // Update Screen Configuration
                            UserConfigurationController.screenConfig(updatedSettings.screenConfig);
                            // Update Rotation
                            UserConfigurationController.rotation(updatedSettings.rotation);
                            dfd.resolve(true);
                        }
                    }).catch(function(e) {
                        logger.warning("Screen config settings not applied due to:" + e);
                        dfd.resolve(false);
                    });
            } else if (Settings.application.features.deviceSeries !== 5 && updatedSettings.rotation.angle != UserConfigurationController.rotation().angle) {
                // If not first boot and the Rotation setting has changed...

                // Update Rotation
                UserConfigurationController.rotation(updatedSettings.rotation);

                // If this isn't the first boot and timezones havent changed then rotate the canvas and restart playback
                // Else this will happen automatically when the firstBootSequenceComplete() mthos is called
                if (!_isFirstBoot && updatedSettings.timeZone.id == UserConfigurationController.timeZone().id) {
                    rotateCanvas();
                }

                dfd.resolve(false);

            } else {
                dfd.resolve(false);
            }

            return dfd;
        };

        function updatePtpRegistry(updatedSettings) {
            var dfd = $.Deferred();
            // If enabled state changes, or if PTP is enabled and the domain setting has changed

            if ((UserConfigurationController.ptp().enabled !== updatedSettings.ptp.enabled) ||
            updatedSettings.ptp.enabled == true && (UserConfigurationController.ptp().domain !== updatedSettings.ptp.domain)) {
                ptpRebootRequired = true;

                UserConfigurationController.ptp(updatedSettings.ptp);

                RegistryInterface.write(updatedSettings.ptp).then(function () {
                    // Success
                    dfd.resolve(true); // Always reboot (?)
                });

            } else {
                UserConfigurationController.ptp(updatedSettings.ptp);
                dfd.resolve(false);
            }
            return dfd;
        }

        function retryUpdateDateTime(updatedSettings, setTimezone) {
            try {

                logger.debug('Retrying to update the date time');

                if (setTimezone) {
                    DateTime.updateTimeZone(updatedSettings.timeZone).done(function(rebootRequired) {
                        UserConfigurationController.save();

                        if (rebootRequired && Settings.application.features.canReboot) {
                            //Reboot
                            PowerInterface.reboot();
                        } else {
                            if (_isFirstBoot) {
                                _isFirstBoot = false;
                                self.firstBootSequenceComplete();
                            } else
                                startMediaPlayer();

                        }

                    }).fail(function() {
                        setTimeout(function() {
                            retryUpdateDateTime(updatedSettings, true);
                        }, 30000);
                    });
                } else {
                    //Timezone not changed, sync date time only              
                    DateTime.syncDateTime(updatedSettings.timeZone.id)
                        .done(function() {
                            UserConfigurationController.save();

                            if (_isFirstBoot) {
                                _isFirstBoot = false;
                                self.firstBootSequenceComplete();
                            } else
                                startMediaPlayer();

                        }).fail(function() {
                            setTimeout(function() {
                                retryUpdateDateTime(updatedSettings, false);
                            }, 30000);
                        });
                }
            } catch (e) {
                logger.error('Error whilst saving settings : ' + e.message);
                // check in 30 seconds
                setTimeout(function() {
                    retryUpdateDateTime(updatedSettings, setTimezone);
                }, 30000);
            }
        }

        /**
         * Rotates the canvas - used to enable portrait support
         */
        function rotateCanvas() {
            if (UserConfigurationController.isRotated() && Settings.application.features.maxSupportedOutputs == 1 && Settings.application.features.deviceSeries < 5) {
                switch (UserConfigurationController.rotationAngle()) {
                    case 90:

                        $('#canvas').width(document.documentElement.clientHeight);
                        $('#canvas').height(document.documentElement.clientWidth);
                        $('#canvas').css('left', (document.documentElement.clientWidth - document.documentElement.clientHeight) / 2 + 'px');
                        $('#canvas').css('top', (document.documentElement.clientHeight - document.documentElement.clientWidth) / 2 + 'px');
                        $('#canvas').removeClass('rotate180');
                        $('#canvas').removeClass('rotate270');
                        $('#canvas').addClass('rotate90');

                        $('#layoutBackgroundCanvas').attr('width', document.documentElement.clientHeight);
                        $('#layoutBackgroundCanvas').attr('height', document.documentElement.clientWidth);
                        $('#layoutBackgroundCanvas').width(document.documentElement.clientHeight);
                        $('#layoutBackgroundCanvas').height(document.documentElement.clientWidth);
                        $('#layoutBackgroundCanvas').css('left', (document.documentElement.clientWidth - document.documentElement.clientHeight) / 2 + 'px');
                        $('#layoutBackgroundCanvas').css('top', (document.documentElement.clientHeight - document.documentElement.clientWidth) / 2 + 'px');
                        $('#layoutBackgroundCanvas').removeClass('rotate180');
                        $('#layoutBackgroundCanvas').removeClass('rotate270');
                        $('#layoutBackgroundCanvas').addClass('rotate90');

                        _mediaPlayerView.adjustLayoutViewDimensions(document.documentElement.clientHeight, document.documentElement.clientWidth);

                        break;
                    case 180:

                        $('#canvas').css('left', '0px');
                        $('#canvas').css('top', '0px');
                        $('#canvas').width(document.documentElement.clientWidth);
                        $('#canvas').height(document.documentElement.clientHeight);
                        $('#canvas').removeClass('rotate90');
                        $('#canvas').removeClass('rotate270');
                        $('#canvas').addClass('rotate180');

                        $('#layoutBackgroundCanvas').css('left', '0px');
                        $('#layoutBackgroundCanvas').css('top', '0px');
                        $('#layoutBackgroundCanvas').attr('width', document.documentElement.clientWidth);
                        $('#layoutBackgroundCanvas').attr('height', document.documentElement.clientHeight);
                        $('#layoutBackgroundCanvas').width(document.documentElement.clientWidth);
                        $('#layoutBackgroundCanvas').height(document.documentElement.clientHeight);
                        $('#layoutBackgroundCanvas').removeClass('rotate90');
                        $('#layoutBackgroundCanvas').removeClass('rotate270');
                        $('#layoutBackgroundCanvas').addClass('rotate180');

                        _mediaPlayerView.adjustLayoutViewDimensions(document.documentElement.clientWidth, document.documentElement.clientHeight);

                        break;
                    case 270:
                        $('#canvas').width(document.documentElement.clientHeight);
                        $('#canvas').height(document.documentElement.clientWidth);
                        $('#canvas').css('left', (document.documentElement.clientWidth - document.documentElement.clientHeight) / 2 + 'px');
                        $('#canvas').css('top', (document.documentElement.clientHeight - document.documentElement.clientWidth) / 2 + 'px');
                        $('#canvas').removeClass('rotate90');
                        $('#canvas').removeClass('rotate180');
                        $('#canvas').addClass('rotate270');

                        $('#layoutBackgroundCanvas').attr('width', document.documentElement.clientHeight);
                        $('#layoutBackgroundCanvas').attr('height', document.documentElement.clientWidth);
                        $('#layoutBackgroundCanvas').width(document.documentElement.clientHeight);
                        $('#layoutBackgroundCanvas').height(document.documentElement.clientWidth);
                        $('#layoutBackgroundCanvas').css('left', (document.documentElement.clientWidth - document.documentElement.clientHeight) / 2 + 'px');
                        $('#layoutBackgroundCanvas').css('top', (document.documentElement.clientHeight - document.documentElement.clientWidth) / 2 + 'px');
                        $('#layoutBackgroundCanvas').removeClass('rotate90');
                        $('#layoutBackgroundCanvas').removeClass('rotate180');
                        $('#layoutBackgroundCanvas').addClass('rotate270');

                        _mediaPlayerView.adjustLayoutViewDimensions(document.documentElement.clientHeight, document.documentElement.clientWidth);

                        break;
                }


            } else {
                // remove rotation

                $('#canvas').removeClass('rotate90');
                $('#canvas').removeClass('rotate180');
                $('#canvas').removeClass('rotate270');
                $('#canvas').css('left', '0px');
                $('#canvas').css('top', '0px');
                $('#canvas').width(document.documentElement.clientWidth);
                $('#canvas').height(document.documentElement.clientHeight);

                $('#layoutBackgroundCanvas').removeClass('rotate90');
                $('#layoutBackgroundCanvas').removeClass('rotate180');
                $('#layoutBackgroundCanvas').removeClass('rotate270');
                $('#layoutBackgroundCanvas').css('left', '0px');
                $('#layoutBackgroundCanvas').css('top', '0px');
                $('#layoutBackgroundCanvas').attr('width', document.documentElement.clientWidth);
                $('#layoutBackgroundCanvas').attr('height', document.documentElement.clientHeight);
                $('#layoutBackgroundCanvas').width(document.documentElement.clientWidth);
                $('#layoutBackgroundCanvas').height(document.documentElement.clientHeight);

                _mediaPlayerView.adjustLayoutViewDimensions(document.documentElement.clientWidth, document.documentElement.clientHeight);
            }
        }

        /**
         * Setup Scheduled Reboots
         */
        function setupScheduledReboots() {

            logger.debug("Configuring Scheduled Reboots");

            try {

                // clear any previous scheduledReboots timer
                if (_scheduledRebootTimer != null)
                    clearTimeout(_scheduledRebootTimer);

                if (_currentConfiguration.connectivity.scheduledRebootsEnabled) {

                    logger.debug("Scheduled Reboots Enabled");

                    if (_currentConfiguration.connectivity.systemRebootTime == null) {
                        logger.error("System Reboot Time is Null. Cannot Configure Scheduled Reboots.");
                        return;
                    }

                    // The timeout till the next reboot
                    var rebootTimeout = 0;

                    // work out timeout
                    var now = DateTime.now();
                    var todaysRebootTime = DateTime.now().setTime(_currentConfiguration.connectivity.systemRebootTime);
                    var tomorrowsRebootTime = DateTime.now().setTime(_currentConfiguration.connectivity.systemRebootTime).addDays(1);

                    // Reboot Later today
                    if (now.getTime() < todaysRebootTime.getTime()) {
                        rebootTimeout = todaysRebootTime.getTime() - now.getTime();
                        logger.debug("Next Scheduled Reboot Today. Reboot Time: " + todaysRebootTime.toLocaleString());
                    }
                    // Reboot Tomorrow
                    else {
                        rebootTimeout = tomorrowsRebootTime.getTime() - now.getTime();
                        logger.debug("Next Scheduled Reboot Tomorrow. Reboot Time: " + tomorrowsRebootTime.toLocaleString());
                    }

                    // Only reboot if reboot Time out in the future
                    if (rebootTimeout > 0) {

                        _scheduledRebootTimer = setTimeout(function() {
                            PowerInterface.reboot();
                            logger.debug("Reboot Scheduled. Timeout (ms): " + rebootTimeout);
                        }, rebootTimeout + 30000);

                    }
                } else
                    logger.debug("Scheduled Reboots Not Enabled.");
            } catch (e) {
                logger.error(e.message);
            }
        }

        /**
         * Legacy - no longer used.
         */
        function removeSettingsSelectionSquare() {
            $('#settingsOptionBox').remove();
        }


        /**
         * Method that starts the media player.
         */
        function startMediaPlayer() {
            // Start the media player
            logger.debug("Starting Media Player");
            _mediaPlayerView.loadContent(_currentConfiguration.content, _currentConfiguration.synchronization, _currentConfiguration.takeoverGroup, _currentConfiguration.clientDetails);

            // Load Generic Modules

            // NOTE: Start Screen Takeovers - only start if not already running and there are takeovers available on the publication
            if(_currentConfiguration.takeoverGroup && Settings.application.features.canTakeoverContent) {
                if(self._screenTakeoverController.running == false && _currentConfiguration.content.takeovers) {
                    self._screenTakeoverController.start(_mediaPlayerView, _currentConfiguration.content.layouts, _currentConfiguration.content.takeovers, _currentConfiguration.clientDetails, _currentConfiguration.takeoverGroup);
                }
            }
            logger.debug("Starting Interrupt Modules");
            $.each(Settings.application.modules, function(moduleName, module) {

                // We are only interested in loading modules that are supported and enabled
                logger.debug("Loading Module: " + moduleName + ". Supported: " + module.supported + " Enabled: " + module.enabled);
                if (module.supported && module.enabled) {
                    // Attempt to load the module.
                    // Module Controller is located in controllers/[moduleName]Controller.js
                    var moduleController = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Controller";
                    logger.debug("Controller Name: " + moduleController);
                    var controller = findModule(moduleController);
                    if (controller != null) {
                        logger.debug("Controller found");
                        // Custom logic for web-based interrupts - Pass the signagelive serial number
                        if (moduleName === 'webBasedInterrupts')
                            module.options.serialNo = _currentConfiguration.clientDetails.signageliveSerialNumber;
                        // Controller loaded via RequireJS. Initialize it passing the module options
                        controller.initialize(module.options)
                            // Successfully initialized
                            .done(function() {
                                // Attempt to start the controller
                                controller.start()
                                    // Successfully started
                                    .done(function() {
                                        logger.debug("Module: " + moduleName + " started.");
                                    })
                                    // Failed to start
                                    .fail(function() {
                                        logger.error("Error: Failed to start module: " + moduleName);
                                    });
                            })
                            // Failed to initialize
                            .fail(function() {
                                logger.error("Error: Failed to initialize module: " + moduleName);
                            });
                    }
                }
            });
        }

        function stopMediaPlayer(endReason) {
            // Stop the media player
            logger.debug("Stopping Media Player");
            _mediaPlayerView.stop(endReason);

            stopWebBasedInterrupts();

            // NOTE: Stop screen takeovers
            if(_currentConfiguration.takeoverGroup && Settings.application.features.canTakeoverContent && self._screenTakeoverController.running == true) {
                self._screenTakeoverController.stop();
            }

            // Stop all modules
            logger.debug("Stopping Interrupt Modules");
            $.each(Settings.application.modules, function(moduleName, module) {

                // We are only interested in loading modules that are supported and enabled
                if (module.supported && module.enabled) {
                    var moduleController = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Controller";
                    var controller = findModule(moduleController);
                    if (controller != null) {
                        // Stop the module
                        controller.stop()
                            // Success
                            .done(function() {
                                logger.debug("Module: " + moduleName + " stopped");
                            })
                            // Failed
                            .fail(function() {
                                logger.warning("Error: Failed to stop module" + moduleName);
                            });
                    }
                }
            });
        }

        function startClockMonitor()
        {
            logger.info("Starting clock monitor");
            var time = new Date();

            setInterval(function(){

                var timeNow = new Date();
                var seconds = (timeNow.getTime() - time.getTime()) / 1000;

                //Time has just change by more than 15 minutes
                if(seconds > 900 || seconds < -900)
                {
                    logger.info("Time adjusted by more than 15 minutes, restart the media player.");

                    try{
                        stopMediaPlayer(PlaybackStartEndReasons.SYSTEM_TIME_CHANGED);
                    }catch (err){}

                    try{
                        startMediaPlayer();
                    }catch (err)
                    {
                        logger.error("Error starting media player on system time change: " + err.message);
                    }
                }

                time = timeNow;
            },30000);
        }

        function findModule(moduleName) {
            var matches = $.grep(_modules, function(module, index) {
                //logger.debug(JSON.stringify(module));
                logger.debug(module.name + " === " + moduleName);
                return module.name === moduleName;
                //return module.module;
            });
            logger.debug(JSON.stringify(matches, undefined, 2));
            if (matches.length === 1) {
                return matches[0].module;
            } else {
                return null;
            }
        }

        _messagePort.onbsmessage = function(msg) {
            for (name in msg.data) {
                
                // Is kiosk mode enabled? If not, do nothing
                var kioskSettings = UserConfigurationController.interactiveKiosk();
                if (!kioskSettings.enabled) {
                    return;
                }
        
                if (name == "type" && msg.data[name] == "TouchEvent") {
                    // Handle a touch event
                    logger.debug("Touch event detected by MessagePort");
                    handleTouchEvent(kioskSettings);
                } else if (name == "type" && msg.data[name] == "KeyboardEvent" && kioskSettings.enabled && InterruptController.isPlayingInterrupt()) { // see note below
                    // Handle a keyboard event
                    logger.debug("Keyboard event detected by MessagePort");
                    handleKeyboardEvent(kioskSettings);
                }
            }
        }

        /**************************************************************************************************************************************
        We use messagePort to listen specifically for keyboard events that occur while an interactive kiosk interrupt is playing; 
        if we don't, these events can get 'consumed' by the interactive kiosk content (websites for example) rather than bubbling up to our app
        **************************************************************************************************************************************/
        
        function handleTouchEvent(kioskSettings) {
            _lastUserInputReceived = DateTime.now(); // Reset _lastUserInputReceived in order to keep the interactive kiosk content showing if a user is touching the screen
    
            if (!InterruptController.isPlayingInterrupt()) {
                InterruptController.triggerInterrupt(kioskSettings.trigger.key, kioskSettings.trigger.type, true, true).done(function() {
                    handleInterrupt(kioskSettings);
                });
            }
        }
        
        function handleKeyboardEvent() {
            _lastUserInputReceived = DateTime.now(); // Reset _lastUserInputReceived in order to keep the interactive kiosk content showing if a user is typing
            //* Firing of the interactive kiosk interrupt & calling of 'handleInterrupt' is done in onKeyPress()  
		}
        
        function handleInterrupt(kioskSettings) {
            // Enable on-screen cursor if mouse or touchscreen is detected
            Promise.all([_pointer.isMousePresent(), _pointer.isMultiTouchPresent()]).then(function([usingCursor, usingMultiTouch]) {
                if (usingCursor || usingMultiTouch) {
                    console.log("Mouse or touchscreen detected");
                    _pointer.enableCursor(true);
                }
            });
        
            // Check for time elapsed since last user input & cancel the interactive kiosk interrupt if enough time has passed 
            var countdownTimer = setInterval(function() {
                var secondsSinceTouch = (DateTime.now() - _lastUserInputReceived) / 1000;
                if (secondsSinceTouch > kioskSettings.idleIntervalInSeconds) {
                    InterruptController.cancelInterrupt();
                    _pointer.enableCursor(false);
                    clearInterval(countdownTimer);
                }
            }, 1000);
        }        
    }
    return SLClientEngine;
});
rjs.define('system/ClientIDGenerator',['system/Guid'], function(Guid) {
	function ClientIDGenerator() {
		var self = this;
		self.generateClientID = function() {
			var dfd = $.Deferred();
			
			setTimeout(function() {
				dfd.resolve(new Guid().newGuid());
			}, 100);

			return dfd;
		};
	}

	return ClientIDGenerator;
});


rjs.define('core/SLMediaPlayerApp',["jquery", "system/DateTime", 'system/SystemInitializer', "system/io/File", "system/io/Directory", "properties/App.Properties", "properties/App.Settings", "core/SLClientEngine", "gui/views/LayoutView", "controllers/UserConfigurationController", "controllers/UserMessageController", "system/SystemInterface", "system/ClientIDGenerator",  "system/graphics/GraphicsInterface", "system/power/PowerInterface", "system/registry/RegistryInterface"], function($, DateTime, SystemInitializer, File, Directory, Properties, Settings, SLClientEngine, LayoutView, UserConfigurationController, UserMessageController, SystemInterface, ClientIDGenerator, GraphicsInterface, PowerInterface, RegistryInterface) {
    /**
     * [SLMediaPlayerApp description]
     * @param {[type]} options [description]
     */
    function SLMediaPlayerApp(options) {
        var self = this;
        var _engine = null;

        // update the default options with the user config
        $.extend(true, Settings, options);

        /**
         * [initialize description]
         * @return {[type]} [description]
         */
        self.initialize = function() {
            var device_info = new BSDeviceInfo();
            
            // Check for device model
            if (device_info.model.toUpperCase() === "LGUV5N") {
                // Update storageType before filesystem initialization
                var newStorageType = "flash";
                var newStorageDirectory = "storage/" + newStorageType;
                
                // Add a new property to the fileSystem object for parts of the code that build a path string, rather than reference a pre-built path
                Settings.application.fileSystem.storageDirectory = newStorageDirectory;
                
                // Update all paths that depend on storageDirectory
                Settings.application.fileSystem.applicationDataDirectory = newStorageDirectory + "/data";
                Settings.application.fileSystem.mediaDirectory = newStorageDirectory + "/media";
                Settings.application.fileSystem.logDirectory = newStorageDirectory + "/log";
                Settings.application.fileSystem.widgetDirectory = newStorageDirectory + "/widgets";
                Settings.application.fileSystem.dataDirectory = newStorageDirectory + "/data";
                Settings.application.fileSystem.appDirectory = newStorageDirectory + "/app";
                Settings.application.fileSystem.tempDirectory = newStorageDirectory + "/temp";
                
                // Update system files paths
                var systemFiles = Settings.application.fileSystem.systemFiles;
                for (var key in systemFiles) {
                    systemFiles[key] = systemFiles[key].replace("storage/sd", newStorageDirectory);
                }
            }
        
            // Continue with the rest of the initialization
            if (proofOfPlayLogger == null) {
                logger.debug("Proof of Play Logger not created.");
            } else {
                logger.debug("Proof of Play Logger created.");
            }

            logger.info('Initializing SL MediaPlayer App');

            // perform any setup of the hardware on first boot
            setupSystem().done(function() {

                // We need to grab deviceSeries information first, so we can run series 5-specific logic
                setupModelSpecificFeatures();

                // Series 5 - get the supported video modes available and also the current screen mode configuration. This information will then be available to reference on the Setup UI screen.

                if (Settings.application.features.deviceSeries === 5) {
                    GraphicsInterface.getSupportedVideoModes();
                    GraphicsInterface.getScreenModes().done(function() {
                        continueSetup()
                    });
                } else {
                    continueSetup();
                }

                function continueSetup() {
                // setup core UI
                setupCoreUI();

                // setup debug tracer
                setupDebugTracer();

                // initialize file system
                initializeFileSystem().done(function() {
                    logger.debug('File system Initalised');
                    // initialize application settings
                    initializeAppSettings().done(function() {

                        setupMessagePump();

                        startTheEngine();
                    });
                }).fail(function() {
                    logger.error("Directory initalization failed");
                });
                }

            }).fail(function() {
                logger.error("Error whilst initializing system");
            });
        };

        function setupModelSpecificFeatures()
        {
            var device_info = new BSDeviceInfo();

            //var is4k = document.documentElement.clientWidth >= 3840;
            var gfxmemlargeInUse = false;

            if(document.documentElement.clientWidth > 1920)
                gfxmemlargeInUse = true;
            
			switch(device_info.model.toLowerCase())
			{
				case "xt243":
                case "xt1143":
                case "cv-uhd":
                    Settings.application.features.canDisplay4kContent = true;
                    Settings.application.features.canDisplayVideoInput = true;
                    Settings.application.features.deviceSeries = 3;
                    break;
				case "xd233":
                case "xd1033": 
                    Settings.application.features.canDisplay4kContent = true;
                    Settings.application.features.deviceSeries = 3;
                    break;
				case "xt244":
                case "xt1144":
                case "cv-uhd2":
                    Settings.application.features.canDisplay4kContent = true;
                    Settings.application.features.supportsSeamlessTransitions = true;
                    Settings.application.features.canDisplayVideoInput = true;
                    Settings.application.features.deviceSeries = 4;
                    break;
				case "xd234":
                case "xd1034":
                    Settings.application.features.canDisplay4kContent = true;
                    Settings.application.features.deviceSeries = 4;
                    break;
                case "hd1024":
                case "hd224":
                case "cv-hd2":
                    Settings.application.features.canDisplay4kContent = true;
                    Settings.application.features.maxConcurrentVideos = 1;
                    Settings.application.features.deviceSeries = 4;
                    break;
				case "hd1023":
				case "hd223":
                case "ho523":
                case "cv-hd":
                    Settings.application.features.maxConcurrentVideos = 1;
                    Settings.application.features.deviceSeries = 3;
                    break;
                case "ls423":
                    Settings.application.features.maxConcurrentVideos = 1;
                    Settings.application.features.deviceSeries = 3;
                    break;
                case "ls424":
                    Settings.application.features.maxConcurrentVideos = 1;
                    Settings.application.features.deviceSeries = 4;
                    break;
                case "ls425":
                case "hs125":
                    // Does not support 4K at all
                    Settings.application.features.maxConcurrentVideos = 1;
                    Settings.application.features.deviceSeries = 5;
                    break;
                case "ls445":
                case "hs145":
                    // LS5 does not support portrait in 4k
                    if(window.screen.availWidth > window.screen.availHeight) {
                        Settings.application.features.canDisplay4kContent = true;
                    }
                    Settings.application.features.maxConcurrentVideos = 1;
                    Settings.application.features.deviceSeries = 5;
                    break;
                case "lguv5n":
                    if(window.screen.availWidth > window.screen.availHeight) {
                        Settings.application.features.canDisplay4kContent = true;
                    }
                    Settings.application.features.maxConcurrentVideos = 1;
                    Settings.application.features.deviceSeries = 5;
                    Settings.application.features.canSetScreenMode = false;
                    break;
                case "hd225":
                case "hd226":
                case "hd1025":
                case "hd1026":
                case "xd235":
                case "xd1035":
                    Settings.application.features.canDisplay4kContent = true;
                    Settings.application.features.supportsSeamlessTransitions = true;
                    Settings.application.features.deviceSeries = 5;
                    break;
                case "xd236":
                case "xd1036":
                    Settings.application.features.canDisplay4kContent = true;
                    Settings.application.features.deviceSeries = 5;                  // NOTE: This is actually Series 6 but we would need to change code in other places to handle this so for now just treat as Series 5
                    Settings.application.features.maxConcurrentVideos = 2;
                    break;
                case "xt245":
                    Settings.application.features.canDisplay4kContent = true;
                    Settings.application.features.canDisplay8kContent = true;
                    Settings.application.features.supportsSeamlessTransitions = false;
                    Settings.application.features.deviceSeries = 5;
                    break;
                case "xt1145":
                    Settings.application.features.canDisplay4kContent = true;
                    Settings.application.features.canDisplay8kContent = true;
                    Settings.application.features.canDisplayVideoInput = true;
                    Settings.application.features.supportsSeamlessTransitions = true;
                    Settings.application.features.deviceSeries = 5;
                    break;
                case "xt2145":
                    Settings.application.features.canDisplay4kContent = true;
                    Settings.application.features.canDisplay8kContent = true;
                    Settings.application.features.canDisplayVideoInput = true;
                    Settings.application.features.supportsMultipleOutputs = true;
                    Settings.application.features.maxSupportedOutputs = 2;
                    Settings.application.features.maxConcurrentVideos = 4; //requires implementing correctly
                    Settings.application.features.supportsSeamlessTransitions = false;
                    Settings.application.features.deviceSeries = 5;
                    break;
                case "xs156":
                    Settings.application.features.canDisplay4kContent = true;
                    Settings.application.features.canDisplay8kContent = false;
                    Settings.application.features.canDisplayVideoInput = false;
                    Settings.application.features.supportsSeamlessTransitions = true;
                    Settings.application.features.maxSupportedOutputs = 1;
                    Settings.application.features.maxConcurrentVideos = 2;
                    Settings.application.features.deviceSeries = 5;                     // NOTE: This is actually Series 6 but we would need to change code in other places to handle this so for now just treat as Series 5
                    break;
                case "xc2055": 
                    Settings.application.features.canDisplay4kContent = true;
                    Settings.application.features.canDisplay8kContent = true;
                    Settings.application.features.supportsMultipleOutputs = true;
                    Settings.application.features.maxSupportedOutputs = 2;
                    Settings.application.features.maxConcurrentVideos = 4; //requires implementing correctly
                    Settings.application.features.supportsSeamlessTransitions = false;
                    Settings.application.features.deviceSeries = 5;
                    break;
                case "xc4055":
                    Settings.application.features.canDisplay4kContent = true;
                    Settings.application.features.canDisplay8kContent = true;
                    Settings.application.features.supportsMultipleOutputs = true;
                    Settings.application.features.maxSupportedOutputs = 4;
                    Settings.application.features.maxConcurrentVideos = 4; //requires implementing correctly
                    Settings.application.features.supportsSeamlessTransitions = true;
                    Settings.application.features.deviceSeries = 5;
                    break;
				default:
					break;
            }
            
            //Firmware less than 8
            if(device_info.VersionCompare("8.0.0.0") < 0)
            {
                //CRC32 module is not supported
                Settings.application.features.supportsHashCRC32 = false;
            }
            
        }

        /**
         * [setupSystem description]
         * @return {[type]} [description]
         */
        function setupSystem() {

            var dfd = $.Deferred();

            // Attempt to switch Off OSD - Not critical so continue even if this fails
            SystemInterface.switchOffOSD().always(function() {

                // Call system Inializer and resolve system setup
                var systemInitializer = new SystemInitializer();
                systemInitializer.initialize().done(function() {
                    dfd.resolve();
                }).fail(function() {
                    dfd.reject();
                });

            });

            return dfd;
        }

        /**
         * [setupCoreUI description]
         * @return {[type]} [description]
         */
        function setupCoreUI() {
            // set the dimensions explicitly to the size of the window
            $('body').width(document.documentElement.clientWidth);
            $('body').height(document.documentElement.clientHeight);

            // add the canvas
            $('body').append("<a href='javascript:void(0);' id='remoteFocus'></a><canvas id='layoutBackgroundCanvas' class='viewer' style='position:absolute;'></canvas><div id='canvas' style='position: absolute;'></div>");

            $('#canvas').css('left', '0px');
            $('#canvas').css('top', '0px');
            $('#canvas').width(document.documentElement.clientWidth);
            $('#canvas').height(document.documentElement.clientHeight);

            $('#layoutBackgroundCanvas').css('left', '0px');
            $('#layoutBackgroundCanvas').css('top', '0px');
            $('#layoutBackgroundCanvas').attr('width', document.documentElement.clientWidth);
            $('#layoutBackgroundCanvas').attr('height', document.documentElement.clientHeight);

            // add the user message view
            $('#canvas').before("<div id='userMessage' style='position: absolute;top: 50%;width: 100%;height: 200px;margin-top: -50px;'><div id='userMessageText' style='position: absolute;top: 0px;width: 100%;height: 100%;color: #ffffff;font-size: 20px;line-height: 100px;font-weight: bold;text-align: center;vertical-align: middle;z-index: 1;'></div></div>");
        }

        /**
         * [setupDebugTracer description]
         * @return {[type]} [description]
         */
        function setupDebugTracer() {
            // set the logging level
            logger.level = Settings.user.loggingLevel;

            // setup the debug trace output
            if (Settings.user != null | Settings.user != undefined) {
                // enable the trace output
                if (Settings.user.showTraceOutput) {
                    $('body').append("<div id='trace' style='z-index:999;color:#ffffff;position:absolute;width:100%;height:100%;left:0px;top:0px;background-color:rgba(0,0,0,0.8); word-wrap:break-word;font-size:12px'></div>");
                    logger.enableTrace = true;
                } else {
                    logger.enableTrace = false;
                }
            }
        }

        /**
         * [initializeFileSystem description]
         * @return {[type]} [description]
         */
        function initializeFileSystem() {
            var dfd = $.Deferred();

            // create the directories
            $.when(
                Directory.createDirectory(Settings.application.fileSystem.root + "/" + Settings.application.fileSystem.appDirectory),
                Directory.createDirectory(Settings.application.fileSystem.root + "/" + Settings.application.fileSystem.mediaDirectory),
                Directory.createDirectory(Settings.application.fileSystem.root + "/" + Settings.application.fileSystem.logDirectory),
                Directory.createDirectory(Settings.application.fileSystem.root + "/" + Settings.application.fileSystem.widgetDirectory),
                Directory.createDirectory(Settings.application.fileSystem.root + "/" + Settings.application.fileSystem.dataDirectory),
                Directory.createDirectory(Settings.application.fileSystem.root + "/" + Settings.application.fileSystem.tempDirectory)
            ).done(function() {
                dfd.resolve();
            }).fail(function() {
                dfd.reject();
            });

            return dfd;
        }

        /**
         * [initializeAppSettings description]
         * @return {[type]} [description]
         */
        function initializeAppSettings() {
            var dfd = $.Deferred();

            $.when(loadClientID(), loadClientKey(), loadUserSettings()).done(function() {
                dfd.resolve();
            }).fail(function() {
                dfd.reject();
            });

            return dfd;
        }

        /**
         * [loadClientID description]
         * @return {[type]} [description]
         */
        function loadClientID() {
            var dfd = $.Deferred();

            logger.debug("Loading Client ID....");

            var device_info = new BSDeviceInfo();
            Settings.application.api.clientId = device_info.deviceUniqueId;

            dfd.resolve();

            return dfd;
        }

        /**
         * [loadClientKey description]
         * @return {[type]} [description]
         */
        function loadClientKey() {
            var dfd = $.Deferred();

            if (File.exists(Settings.application.fileSystem.systemFiles.clientKeyFilePath)) {
                // load the client Id
                File.read(Settings.application.fileSystem.systemFiles.clientKeyFilePath).done(function(data) {
                    if (data != null && data != "") {
                        var clientKeyData = JSON.parse(data);
                        logger.info('Stored Client Key is ' + clientKeyData.clientKey);
                        Settings.application.api.clientKey = clientKeyData.clientKey;
                    }

                    dfd.resolve();
                }).fail(function() {
                    dfd.reject();
                });
            } else {
                setTimeout(function() {
                    // if the file does not exist this becuase it has not yet been generated (by registering with the SL cloud)
                    // mark this method as successfull
                    dfd.resolve();
                }, 50);
            }

            return dfd;
        }

        /**
         * [generateSystemClientID description]
         * @return {[type]} [description]
         */
        function generateSystemClientID() {
            var dfd = $.Deferred();

            // generate a client id
            var idGenerator = new ClientIDGenerator();

            idGenerator.generateClientID().done(
                function(id) {
                    logger.debug("SLMediaApp: New Id Generated " + id);
                    dfd.resolve(id);
                }
            );

            return dfd;
        }

        /**
         * [loadUserSettings description]
         * @return {[type]} [description]
         */
        function loadUserSettings() {
            var dfd = $.Deferred();

            if (File.exists(Settings.application.fileSystem.systemFiles.settingsFilePath)) {
                // load the users settings
                File.read(Settings.application.fileSystem.systemFiles.settingsFilePath).done(function(data) {
                    if (data != null && data != "") {
                        Settings.user.settingsLoaded = true;
                        var userSettings = JSON.parse(data);

                        $.extend(true, Settings.user.mySettings, userSettings);

                        // Get the screen config from the user settings and apply it, if it is different
                        setScreenConfigFromUserSettings().always(function() {
                            // Have to read the registry here as the settings file is read before the client is initialised 
                            RegistryInterface.init().then(function () {
                                var ptpData = RegistryInterface.ptp;
                                if (userSettings.ptp !== undefined) {
                                    if((ptpData.enabled !== userSettings.ptp.enabled) || userSettings.ptp.enabled == true && (ptpData.domain !== userSettings.ptp.domain)) {
                                        var PtpInvalid = (parseInt(userSettings.ptp.domain) >= 0 && parseInt(userSettings.ptp.domain) <= 127) ? false : true;
                                        var newPtpData = {
                                            enabled: (userSettings.ptp.enabled == false) ? false : true,
                                            domain: (userSettings.ptp && parseInt(userSettings.ptp.domain) >= 0 && parseInt(userSettings.ptp.domain) <= 127) ? String(parseInt(userSettings.ptp.domain)) : "0" //default to 0
                                        }

                                        if(PtpInvalid == false) {
                                            // Try and apply it ..
                                            RegistryInterface.write(newPtpData).then(function () {
                                                // Update user settings to match the valid registry information
                                                UserConfigurationController.ptp(newPtpData);
                                                UserConfigurationController.save().done(function () {
                                                    if (Settings.application.features.canReboot) {
                                                        PowerInterface.reboot();
                                                    }
                                                });
                                            });
                                        } else {
                                            // Update the settings file back to the previous values
                                            UserConfigurationController.ptp(ptpData);
                                            UserConfigurationController.save();
                                        }
                                    }
                                } else {
                                    // Update user settings to match the current registry information
                                    UserConfigurationController.ptp(ptpData);
                                    UserConfigurationController.save();
                                }
                            });
                            dfd.resolve();
                        });
                        }
                }).fail(function() {
                    dfd.reject();
                });

            } else {
                setTimeout(function() {
                    // if the file does not exist this becuase it has not yet been generated (by install)
                    // mark this method as successfull
                    dfd.resolve();
                }, 50);
            }

            return dfd;
        }

        /**
         * Scales the viewport for resolutions > 1920 x 1200px
         */
        function scaleHighResolutionViewports(newConfig) {
            newConfig.forEach(function (output, i) {
                if(output.videoMode != undefined && output.videoMode != "auto") {
                    var resolutionParts = output.videoMode.split("x");
                    var resolutionWidth = parseInt(resolutionParts[0]);
                    var resolutionHeight = parseInt(resolutionParts[1]);
                    if ((resolutionWidth > 1920) || resolutionHeight > 1200) {
                        output.videoMode += ":fullres:gfxmemlarge";
                        Settings.user.mySettings.screenConfig[i].videoMode += ":fullres:gfxmemlarge";
                    }
                }
            }); 
        }

        function setScreenConfigFromUserSettings() {
            var dfd = $.Deferred();

            if(Settings.application.features.deviceSeries == 5) {
                // Get the current screen configuration from the Graphics Interface
                var currentScreenConfig = GraphicsInterface.screens;
                var newConfig = $.extend(true, [], currentScreenConfig, Settings.user.mySettings.screenConfig);
                newConfig = newConfig.filter(function(screen) { return screen.outputName !== "TypeC-1"; }); //TypeC is not currently supported therefore for existing installations using it we want the value to be ignored
                
                try {
                    scaleHighResolutionViewports(newConfig);
                    // Compare configurations (making sure a config exists in the Settings) and attempt to apply it
                    isScreenConfigurationValid(newConfig).then(function(validConfig) {
                        if (validConfig && validConfig.length > 0 && JSON.stringify(currentScreenConfig) !== JSON.stringify(validConfig) && Settings.application.features.canSetScreenMode) {
                            GraphicsInterface.setScreenModes(validConfig).then(function() {
                                PowerInterface.reboot();
                            }).catch(function(e) {
                                // If it fails - apply the previous valid configuration, so when the UserConfigurationController inits this is then saved
                                logger.error("Error applying this screen configuration - revert user settings to previously valid screen configuration");
                                Settings.user.mySettings.screenConfig = currentScreenConfig;
                                Settings.user.mySettings.isScreenConfigInvalid = true;
                                dfd.resolve();
                            });
                        } else if (newConfig && validConfig && validConfig.length > 0 && validConfig.length !== newConfig.length && Settings.application.features.canSetScreenMode) {
                            // If an invalid screen configuration (max outputs) has been added to the settings.json then this is amended in isScreenConfigurationValid()
                            // with validConfig returning the correct max amount of outputs to set. This can then be applied above (if different) and then when matching getScreenModes() we update
                            // and save the settings.json file.
                            logger.error("Incorrect screen configuration length used - therefore amending the saved screen configuration");
                            Settings.user.mySettings.screenConfig = validConfig;
                            Settings.user.mySettings.isScreenConfigInvalid = true;
                            dfd.resolve();
                        } else {
                            // If a screen configuration is not in the Settings file then once the screen setup UI is saved this will then get added
                            logger.info("Screen config does not need to change...");
                            dfd.resolve();
                        }
                    }).catch(function(e) {
                        // If it fails - apply the previous valid configuration, so when the UserConfigurationController inits this is then saved
                        logger.error("Error applying this screen configuration - revert user settings to previously valid screen configuration");
                        Settings.user.mySettings.screenConfig = currentScreenConfig;
                        Settings.user.mySettings.isScreenConfigInvalid = true;
                        dfd.resolve();
                    });
                } catch(e) {
                    // If it fails - apply the previous valid configuration, so when the UserConfigurationController inits this is then saved
                    logger.error("Error applying this screen configuration - revert user settings to previously valid screen configuration");
                    Settings.user.mySettings.screenConfig = currentScreenConfig;
                    Settings.user.mySettings.isScreenConfigInvalid = true;
                    dfd.resolve();
                };
            }
            return dfd;
        }

        function isScreenConfigurationValid(newConfig) {
            var dfd = $.Deferred();
            // Make sure that model screen configuration length matches the configuration we are trying to apply (e.g. a 1 display output device does not apply a 4 display configuration)
            // Get available screens based on BS method- as unsure on Type-C support / if it will be removed for XC?
            var maxAvailableOutputs = GraphicsInterface.screens;

            if(maxAvailableOutputs && maxAvailableOutputs.length > 0 && maxAvailableOutputs.length !== newConfig.length) {
                // Remove additional outputs - cant match by outputName incase that can be changed
                newConfig = newConfig.splice(0, maxAvailableOutputs.length);
            }

            // Make sure that we do not allow a configuration to be applied if the screenX or screenY co-ordinate is above 12000 due to an issue being caused with the client bootlooping
            newConfig.forEach(function(screen) {
                if(screen.screenX > 12000 || screen.screenY > 12000) {
                    logger.warning("Failing to apply screen configuration -- screenX is set to: " + screen.screenX + " and screenY is set to: " + screen.screenY + ". The maximum value we allow is 12000");
                    dfd.reject();
                }
            });

            dfd.resolve(newConfig);
            return dfd;
        }

        /**
         * [setUpCanvas description]
         */
        function setUpCanvas() {

            if (UserConfigurationController.isRotated() && Settings.application.features.maxSupportedOutputs == 1 && Settings.application.features.deviceSeries < 5) {
             
                switch (UserConfigurationController.rotationAngle()) {
                    case 90:
                        $('#canvas').width(document.documentElement.clientHeight);
                        $('#canvas').height(document.documentElement.clientWidth);

                        $('#canvas').css('left', (document.documentElement.clientWidth - document.documentElement.clientHeight) / 2 + 'px');
                        $('#canvas').css('top', (document.documentElement.clientHeight - document.documentElement.clientWidth) / 2 + 'px');

                        $('#canvas').addClass('rotate90');

                        //layoutView = new LayoutView(document.documentElement.clientHeight, document.documentElement.clientWidth);
                        break;
                    case 270:
                        $('#canvas').width(document.documentElement.clientHeight);
                        $('#canvas').height(document.documentElement.clientWidth);

                        $('#canvas').css('left', (document.documentElement.clientWidth - document.documentElement.clientHeight) / 2 + 'px');
                        $('#canvas').css('top', (document.documentElement.clientHeight - document.documentElement.clientWidth) / 2 + 'px');

                        $('#canvas').addClass('rotate270');

                        //layoutView = new LayoutView(document.documentElement.clientHeight, document.documentElement.clientWidth);
                        break;
                }
              
            } else {

                //layoutView = new LayoutView(document.documentElement.clientWidth, document.documentElement.clientHeight);
            }
        }

        /**
         * [setupMessagePump description]
         * @return {[type]} [description]
         */
        function setupMessagePump() {
            // setup user message pump
            UserMessageController.addEventListener(function(message) {
                logger.debug('User Message: ' + message);
                message = message.replace(/\n/g,'<br/>');
                $('#userMessageText').html(message);
            });
        }

        /**
         * [startTheEngine description]
         * @return {[type]} [description]
         */
        function startTheEngine() {
            // setup the SL client engine - this class starts all processes
            _engine = new SLClientEngine({});
            _engine.initialize();
        }

    }

    return SLMediaPlayerApp;
});
rjs.define('core/SLPlayerDataAPIInterface',["properties/App.Settings", "system/io/File", "system/DateTime"], function(Settings, File, DateTime) {
    function SLPlayerDataAPIInterface(config) {
        logger.debug('PLAYER DATA API: Building Player Data API');
        var self = this;

        // call back methods for registration and configuration
        var _configurationCallback;

        // default connection settings
        var _authenticationFailureRetryIntervalMS = 5000;

        var _defaultUploadCheckIntervalMS = 600000;
        // 60 minutes

        var _authenticationTimerId = 0;
        var _uploadTimerId = 0;

        var _popConfig = config;

        function authenticate() {
            var dfd = $.Deferred();
            logger.debug('PLAYER DATA API: auth about to be called');

            $.ajax({
                url: Settings.application.playerdataapi.url + "/authenticate",
                type: "POST",
                contentType: "application/json",
                dataType: "json",
                async: true,
                data: null,
                timeout: 60000,
                beforeSend: function(xhr, settings) {
                    xhr.setRequestHeader('X-SIGNAGELIVE-APP-ID', Settings.application.api.appId);
                    xhr.setRequestHeader('X-SIGNAGELIVE-APP-KEY', Settings.application.api.appKey);
                    xhr.setRequestHeader('X-SIGNAGELIVE-CLIENT-ID', Settings.application.api.clientId);
                    xhr.setRequestHeader('X-SIGNAGELIVE-CLIENT-KEY', Settings.application.api.clientKey);

                }
            }).done(function(data, textStatus, jqXHR) {
                logger.debug('PLAYER DATA API: authenticated! API Token: ' + data.token);
                // set the auth token
                Settings.application.api.authToken = data.token;

                var expireTime = new Date(data.expires);//We will re-authenticate 5 minutes before the token actually expires
                expireTime = new Date(expireTime - (300000));

                Settings.application.api.authTokenExpiry = expireTime;
                logger.debug("Token expires at: " + Settings.application.api.authTokenExpiry);

                dfd.resolve(true);

            }).fail(function(jqXHR, textStatus, errorThrown) {
                logger.debug('PLAYER DATA API: auth failed!');
                // log the error
                logAPIError(this.url, this.data, jqXHR.status, errorThrown, jqXHR.responseText);

                // attempt to re-athenticate in a few seconds
                //_authenticationTimerId = setTimeout(authenticate, _authenticationFailureRetryIntervalMS);

                dfd.resolve(false);
            });

            return dfd;
        }

        function setUploadTimer() {
            // calculate when the next configuration check should occur based on settings in the configuration response.
            var interval = calculateNextUploadInterval();

            // reset the configuration timer
            _uploadTimerId = setTimeout(postProofOfPlayLog, interval);
        }

        function calculateNextUploadInterval() {
            var interval = _defaultUploadCheckIntervalMS;

            if (_popConfig != null) {
                interval = _popConfig.uploadfrequency * 1000;
            }

            return interval;
        }

        function isAuthenticationRequired() {
            logger.debug('PLAYER DATA API: isAuthenticationRequired()');

            if (Settings.application.api.authTokenExpiry == null)
                return true;
            if (Settings.application.api.authTokenExpiry < DateTime.now())
                return true;
            return false;
        }

        function postProofOfPlayLog() {
            try {
                logger.debug('PLAYER DATA API: Sending Proof of Play Log');

                // does the client need to authenticate again?
                if (isAuthenticationRequired()) {
                    logger.debug('PLAYER DATA API: Auth Required');

                    // Need to authenticate asynchronously
                    authenticate()
                        .done(function(authenticated) {
                            // Client is not authenticated try again soon
                            if (!authenticated) {
                                // reset the system report timer
                                setUploadTimer();
                                return;
                            } else {
                                uploadLogs();
                            }
                        });
                } else {
                    uploadLogs();
                }


            } catch (e) {
                logger.debug('PLAYER DATA API: Proof of Play Upload Failed ' + e.message);

                setUploadTimer();
            }
        }

        function uploadLogs() {
            // Get the file to upload
            var logurl = Settings.application.fileSystem.systemFiles.proofOfPlayFilePath;
            var uploadFileUrl = Settings.application.fileSystem.systemFiles.proofOfPlayUploadFilePath;

            File.resetProofOfPlay(logurl, uploadFileUrl)
                .done(function() {
                    File.read(uploadFileUrl)
                        .done(function(popReport) {
                            logger.debug("PLAYER DATA API: Uploading Proof of Play");

                            if (popReport != null) {


                                try {
                                    $.ajax({
                                        url: Settings.application.playerdataapi.url + "/proofofplay",
                                        type: "POST",
                                        contentType: "text/plain;charset=UTF-8;",
                                        dataType: "text",
                                        async: "true",
                                        data: String(popReport),
                                        timeout: 60000,
                                        beforeSend: function(xhr, settings) {
                                            xhr.setRequestHeader('X-SIGNAGELIVE-APP-ID', Settings.application.api.appId);
                                            xhr.setRequestHeader('X-SIGNAGELIVE-AUTH-TOKEN', Settings.application.api.authToken);
                                        }
                                    }).done(function() {
                                        logger.info('PLAYER DATA API: Proof of Play Upload Done ');

                                        try {
                                            // Delete Proof of Play Upload File
                                            File.deleteLocal(uploadFileUrl);
                                            setUploadTimer();
                                        } catch (e) {
                                            logger.error("Error deleting Proof of play upload file: " + e.message);
                                            setUploadTimer();
                                        }
                                    }).fail(function(jqXHR, textStatus, errorThrown) {
                                        logAPIError(this.url, this.data, jqXHR.status, errorThrown, jqXHR.responseText);
                                        if (jqXHR.status === 401) {
                                            logger.warning("PoP Upload failed: 401 Unauthorized. Will reauthenticate and retry");
                                            setTimeout(function() {
                                                Settings.application.api.authTokenExpiry = null;
                                                postProofOfPlayLog();
                                            }, 500);
                                        } else {
                                            setUploadTimer();
                                        }
                                    });
                                } catch (e) {
                                    logger.error("Error uploading to player data api: " + e.message);
                                    setUploadTimer();
                                }
                            } else {
                                logger.debug('Proof of Play: No data to upload');
                                setUploadTimer();
                            }
                        }).fail(function() {
                            // log the error
                            logger.debug('Proof of Play: Unable to read log file');
                            setUploadTimer();
                        });
                }).fail(function() {
                    // log the error
                    logger.error('PoP: Error: Failed to roll PoP Logs');
                    setUploadTimer();
                });
        }

        function logAPIError(requestUrl, requestData, responseStatus, responseMessage, responseText) {
            var logData = {
                url: requestUrl,
                requestData: requestData,
                responseStatus: responseStatus,
                responseMessage: responseMessage,
                responseBody: responseText
            };

            logger.error("PLAYER DATA API: API Error " + JSON.stringify(logData));
        }

        self.initialize = function() {
            logger.info('PLAYER DATA API: Initializing SLPlayerDataAPIInterface, API Creds: ' + JSON.stringify(Settings.application.playerdataapi));

            if (Settings.application.api != null && Settings.application.api.appId != null && Settings.application.api.appKey != null && Settings.application.api.clientId != null && Settings.application.api.clientKey != null) {
                // start the upload process
                postProofOfPlayLog();
            }
        };
    }

    return SLPlayerDataAPIInterface;
});
rjs.define('utils/proofOfPlayLogger',["system/DateTime", "properties/App.Settings", "system/io/File", "core/SLPlayerDataAPIInterface"], function(DateTime, Settings, File, SLPlayerDataAPIInterface) {
    var proofOfPlayLogger = {
        config: null,
        traceCounter: 0,
        traceCounterMax: 50,
        totalTrace: 0,
        apiInterface: null,
        init: function(configModel, apiCreds) {
            logger.debug('Creating Proof of Play Logger, API Credentials: ' + JSON.stringify(apiCreds));

            this.config = configModel;

            if (this.config != null && this.config.uploadurl != null && this.config.enabled == true) {
                apiInterface = new SLPlayerDataAPIInterface(this.config);
                apiInterface.initialize();
                logger.debug('Proof of Play Logger created');
            } else {
                logger.debug('Proof of Play Logger configuration not found or Proof of play not enabled');
            }


        },
        updateConfig: function(configModel, apiCreds) {
            logger.debug('Updating Proof of Play Configuration');

            var configChanged = false;

            if (this.config != null) {
                if (this.config.enabled != configModel.enabled) {
                    configChanged = true;
                }

                if (this.config.uploadfrequency != configModel.uploadfrequency) {
                    configChanged = true;
                }

                if (this.config.uploadurl = !configModel.uploadurl) {
                    configChanged = true;
                }

                if (this.config.maxlogfilesizeinkb != configModel.maxlogfilesizeinkb) {
                    configChanged = true;
                }

                if (this.config.mindiskspaceinkb != configModel.mindiskspaceinkb) {
                    configChanged = true;
                }

                if (configChanged) {
                    this.config = configModel;
                    logger.debug('Proof of Play config Updated: ' + JSON.stringify(this.config));

                    if (this.config.uploadurl != null && apiCreds != null && this.config.enabled == true) {
                        Settings.application.playerdataapi.url = this.config.uploadurl;
                        logger.debug('Proof of Play - re initialising Player Data API Interface');
                        apiInterface = new SLPlayerDataAPIInterface(this.config);
                        apiInterface.initialize();
                    }
                } else {
                    logger.debug('Proof of Play config not changed: ' + JSON.stringify(this.config));
                }
            } else {
                this.config = configModel;
                logger.debug('Proof of Play config was null, now Updated: ' + JSON.stringify(this.config));
            }
        },
        debug: function(layout, mediaWindow, playlist, playlistMediaAsset, mediaAsset, start, end, playReason, endReason) {
            //logger.debug("PoP: Proof of Play Enabled: " + this.config.enabled);

            if (this.config != null && this.config.enabled) {
                // build the proof of play log entry 
                // Media Asset ID, Layout ID, Media Window ID, Playlist ID, Playlist Media Asset ID, Start Time, End Time, Play Reason, End Reason
                logString = "";

                /* All used for debugging
                 debugIDString = "PoP: ";
                debugIncludeString = "PoP: ";
				
                if (mediaAsset != null) {
                		debugIDString = "Media Asset: " + mediaAsset.externalId + ",";
                		debugIncludeString = "Media Asset: " + mediaAsset.includeInProofOfPlay + ",";
                	} else {
                		debugIDString = ",";
                		debugIncludeString = ",";
                	}
                	
                	if (layout != null) {
                		debugIDString = debugIDString + " Layout: " + layout.externalId + ",";
                		debugIncludeString = debugIncludeString + " Layout: " + layout.includeInProofOfPlay + ",";
                	} else {
                		debugIDString = debugIDString + ",";
                		debugIncludeString = debugIncludeString + ",";
                	}
                	
                	if (mediaWindow != null) {
                		debugIDString = debugIDString + " Media Window: " + mediaWindow.externalId + ",";
                		debugIncludeString = debugIncludeString + " Media Window: " + mediaWindow.includeInProofOfPlay + ",";
                	} else {
                		debugIDString = debugIDString + ",";
                		debugIncludeString = debugIncludeString + ",";
                	}
                	
                	if (playlist != null) {
                		debugIDString = debugIDString + " Playlist: " + playlist.externalId + ",";
                		debugIncludeString = debugIncludeString + " Playlist: " + playlist.includeInProofOfPlay + ",";
                	} else {
                		debugIDString = debugIDString + ",";
                		debugIncludeString = debugIncludeString + ",";
                	}
                	
                	if (playlistMediaAsset != null) {
                		debugIDString = debugIDString + " Playlist Media Asset: " + playlistMediaAsset.externalId;
                		debugIncludeString = debugIncludeString + " Playlist Media Asset: " + playlistMediaAsset.includeInProofOfPlay;
                	}
                	
                logger.debug(debugIDString);
                logger.debug(debugIncludeString);*/

                // first check if any of the objects are marked as log for proof of play
                includeInPoP = false;
                if (playlistMediaAsset != null && playlistMediaAsset.includeInProofOfPlay) {
                    includeInPoP = true;
                } else if (playlist != null && playlist.includeInProofOfPlay) {
                    includeInPoP = true;
                } else if (mediaWindow != null && mediaWindow.includeInProofOfPlay) {
                    includeInPoP = true;
                } else if (layout != null && layout.includeInProofOfPlay) {
                    includeInPoP = true;
                } else if (mediaAsset != null && mediaAsset.includeInProofOfPlay) {
                    includeInPoP = true;
                }

                //logger.debug("PoP: Include in Proof of Play: " + includeInPoP);

                // Build the string
                if (includeInPoP) {
                    if (mediaAsset != null) {
                        logString = mediaAsset.externalId + ",";
                    } else {
                        logString = ",";
                    }

                    if (layout != null) {
                        logString = logString + layout.externalId + ",";
                    } else {
                        logString = logString + ",";
                    }

                    if (mediaWindow != null) {
                        logString = logString + mediaWindow.externalId + ",";
                    } else {
                        logString = logString + ",";
                    }

                    if (playlist != null) {
                        logString = logString + playlist.externalId + ",";
                    } else {
                        logString = logString + ",";
                    }

                    if (playlistMediaAsset != null) {
                        logString = logString + playlistMediaAsset.externalId + ",";
                    } else {
                        logString = logString + ",";
                    }

                    logString = logString + start + "," + end + "," + playReason + "," + endReason;
                    var popAssetID = null;

                    //Add pop_asset_id
                    if (playlistMediaAsset != null && playlistMediaAsset.popAssetID != null && playlistMediaAsset.popAssetID != "") {
                        popAssetID = playlistMediaAsset.popAssetID;
                    } else if (mediaAsset != null && mediaAsset.popAssetID != null) {
                        popAssetID = mediaAsset.popAssetID;
                    } else {
                        popAssetID = "";
                    }
                    logString = logString + "," + popAssetID;

                    msg = "PoP - Log - " + logString;
                    //logger.debug(msg);

                    File.append(Settings.application.fileSystem.systemFiles.proofOfPlayFilePath, logString + "\r\n");

                } else {
                    logger.debug("Asset is not included in Proof of Play");
                }
            } else if (this.config == null) {
                logger.debug("Proof of Play Config is null");
            } else if (this.config.enabled == false) {
                logger.debug("Proof of Play is not enabled");
            }
        }
    };

    window.proofOfPlayLogger = proofOfPlayLogger;

    return proofOfPlayLogger;
});
rjs.require(["jquery", "utils/logger", "core/SLMediaPlayerApp", "utils/proofOfPlayLogger"], function($, logger, SLMediaPlayerApp, proofOfPlayLogger) {
    $(function() {

        window.setTimeoutStandard = setTimeout;
        window.setTimeout = function(func, delay) {
            return setTimeoutStandard(func, Math.min(delay, 2147483647));
        };

        var options = {};
        var app = new SLMediaPlayerApp(options);

        window.$ = $; // Note: not sure why this required as $ is already on the window but it is?!!

        setTimeout(function() {
            app.initialize();
        }, 100);
    });
});
rjs.define("main", function(){});

rjs.define('models/IdleState',[],function(){
	return {
		ACTIVE:"active",
		IDLE:"idle",
		LOCKED:"locked"
	}
});
rjs.define('system/sensors/IdleNotificationInterface',["models/IdleState"], function(IdleState) {
	/*
		Idle Notification Interface provides a generic interface for notifying the application
		when the idle state changes.

		This implementation is heavily influenced by the Chrome.Idle API but should provide us the flexibility
		to implement this in other platforms if and when it becomes possible to do so.
	 */
	function IdleNotificationInterface() {
		var self = this;

		self.onStateChange = {};

		/**
		 * Set the idle interval term used to determine the period of
		 * inactivity required for system to be deemed inactive
		 * @param {[type]} idleIntervalInSeconds [description]
		 */
		self.setIdleInterval = function(idleIntervalInSeconds)
		{
			var dfd = $.Deferred();

			setTimeout(function(){
				logger.warning("IdleNotificationInterface: setIdleInterval called but not implemented");
				dfd.reject();
			}, 150);
			
			return dfd;
		};

		/**
		 * Get the current Idle Status
		 * @param  {[type]} idleIntervalInSeconds [description]
		 * @return {[type]}                       [description]
		 */
		self.queryState = function(idleIntervalInSeconds)
		{
			var dfd = $.Deferred();
			
			setTimeout(function(){
				logger.warning("IdleNotificationInterface: onStateChange.queryState called but not implemented");
				dfd.reject();
			}, 150);

			return dfd;
		};

		/**
		 * Register callback that is fired when the idle status changes
		 * @param {Function} callback [description]
		 * @return Model/IdleState enum
		 */
		self.onStateChange.addListener = function(callback)
		{
			var dfd = $.Deferred();

			setTimeout(function(){
				logger.warning("IdleNotificationInterface: onStateChange.addListener called but not implemented");
				dfd.reject();
			}, 150);

			return dfd;
		};

	}
	
	// The object is a singleton as there is only a single inteface added to the application
	return new IdleNotificationInterface();
});
rjs.define('system/sensors/NFCInterface',[], function() {
	
	/**
	 * NFC Interface
	 * ---------------------------------
	 * Allows LG client to turn on/off nfc and write to the nfc tag
	 */
	function NFCInterface() {

		var self = this;

		var _enabled = false;
		
		/**
		 * [writeTag description]
		 * @param  {[type]} data [description]
		 * @return {[type]}      [description]
		 */
		self.writeTag = function(data)
		{
			var dfd = $.Deferred();

			setTimeout(function(){
				logger.warning("NFCInterface: writeTag called but not implemented");
				dfd.reject();
			}, 150);

			return dfd;
		};
		
		/**
		 * [enable description]
		 * @return {[type]} [description]
		 */
		self.enable = function()
		{
			var dfd = $.Deferred();

			setTimeout(function(){
				logger.warning("NFCInterface: enable called but not implemented");
				dfd.reject();
			}, 150);

			return dfd;	
		};

		/**
		 * [disable description]
		 * @return {[type]} [description]
		 */
		self.disable = function()
		{
			var dfd = $.Deferred();

			setTimeout(function(){
				logger.warning("NFCInterface: disable called but not implemented");
				dfd.reject();
			}, 150);

			return dfd;
		};

		/**
		 * [isEnable description]
		 * @return {[type]} [description]
		 */
		self.isEnabled = function()
		{
			var dfd = $.Deferred();

			setTimeout(function(){
				dfd.resolve(_enbled);
			}, 150);

			return dfd;	
		};

	}
	
	// The object is a singleton as there is only a single inteface added to the application
	return new NFCInterface();
});
rjs.define('models/FirmwareUpdateStatusConstants',[],function(){
	return {
		IDLE:"idle",
		DOWNLOADING:"downloading",
		READY:"ready",
		IN_PROGRESS: "in progress",
		COMPLETED: "completed",
		FAIL: "fail",
		UNKNOWN: "unknown"
	}
});
rjs.define('models/GPIOPinState',[],function(){
	return {
		UP:"Up",
		DOWN:"Down"
	}
});
rjs.define('models/GPIOPinModel',["models/GPIOPinState"], function(GPIOPinState) {

	/**
	 * A simple model for representing GPIO pins
	 * @param {int - pin number}
	 * @param {GPIOPinState - pin state}
	 */
	function GPIOPinModel(number, state)
	{
		var self = this;
		self.number = number;
		self.state = state;
	}

	return GPIOPinModel;
});
rjs.define('gui/viewmodels/DateTimeViewModel',['lib/knockout', 'system/DateTime'], function(ko, DateTime) {
	function DateTimeViewModel() {
		var _monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

		var now = DateTime.now();

		var _selectedDate = ko.observable(now.getDate());
		var _selectedMonth = ko.observable(now.getMonth()+1);
		var _selectedYear = ko.observable(now.getFullYear());
		var _selectedHour = ko.observable(now.getHours());
		var _selectedMinute = ko.observable(now.getMinutes());

		this.date = function() {
			// TODO calculate the difference between now and the last time the DT was adjusted in the UI
			// as due to the fact that the panel must reboot before the dt is saved we need to do this only once all other settings are completed.
			return new Date(_selectedYear(), _selectedMonth() - 1, _selectedDate(), _selectedHour(), _selectedMinute());
		}

		this.day = ko.computed(function() {
			return _selectedDate();
		}, this);

		this.month = ko.computed(function() {
			return _monthNames[_selectedMonth() - 1];
		}, this);

		this.year = ko.computed(function() {
			return _selectedYear();
		}, this);

		this.hour = ko.computed(function() {
			var padding = '00';
			var result = padding + _selectedHour().toString();
			return result.substr(-2, 2).toUpperCase();
		}, this);

		this.minute = ko.computed(function() {
			var padding = '00';
			var result = padding + _selectedMinute().toString();
			return result.substr(-2, 2).toUpperCase();
		}, this);

		this.addDays = function(n) {
			var newDate = _selectedDate() + n;

			var maxDateForMonth = new Date(_selectedYear(), _selectedMonth(), 0);
			if (newDate > maxDateForMonth.getDate()) {
				newDate = 1;
			}

			if (newDate == 0) {
				newDate = maxDateForMonth.getDate();
			}

			_selectedDate(newDate);
		}

		this.addMonths = function(n) {
			var newMonth = _selectedMonth() + n;

			if (newMonth == 13)
				newMonth = 1;

			if (newMonth == 0)
				newMonth = 12;

			var maxDateForMonth = new Date(_selectedYear(), newMonth, 0);
			if (_selectedDate() > maxDateForMonth.getDate()) {
				_selectedDate(maxDateForMonth.getDate());
			}

			_selectedMonth(newMonth);
		}

		this.addYears = function(n) {
			var newYear = _selectedYear() + n;

			var maxDateForMonth = new Date(newYear, _selectedMonth(), 0);
			if (_selectedDate() > maxDateForMonth.getDate()) {
				_selectedDate(maxDateForMonth.getDate());
			}

			_selectedYear(newYear);
		}

		this.addHours = function(n) {
			var newHour = _selectedHour() + n;

			if (newHour == 24)
				newHour = 0;

			if (newHour == -1)
				newHour = 23;

			_selectedHour(newHour);
		}

		this.addMinutes = function(n) {
			var newMinutes = _selectedMinute() + n;

			if (newMinutes == 60)
				newMinutes = 0;

			if (newMinutes == -1)
				newMinutes = 59;

			_selectedMinute(newMinutes);
		}
	}

	return DateTimeViewModel;
})
;
rjs.define('gui/views/ScreenControlView',[], function() {
	/**
	 * [ScreenControlView description]
	 */
	function ScreenControlView() {
		var self = this;
		self.id = 'screenControlView';

		// create html
		var viewHtml = "<div id='" + self.id + "' style='position:absolute;left:0px;top:0px;width:100%;height:100%;background-color:#000000;text-align:center'></div>";
		self.html = viewHtml;

		/**
		 * [addedToCanvas description]
		 * @return {[type]} [description]
		 */
		self.addedToCanvas = function() {
			//
		};

		/**
		 * [remove description]
		 * @return {[type]} [description]
		 */
		self.remove = function() {
			// Hide ScreenControlView
			$("#" + self.id).remove();
		}
	};

	return ScreenControlView;
});


rjs.define("bundle", function(){});
