/*
 * Copyright (C) 2014 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

// This gets all concatenated module descriptors in the release mode.
var allDescriptors = [];
var applicationDescriptor;
var _loadedScripts = {};

// FIXME: This is a workaround to force Closure compiler provide
// the standard ES6 runtime for all modules. This should be removed
// once Closure provides standard externs for Map et al.
for (var k of []) {};

/**
 * @param {string} url
 * @return {!Promise.<string>}
 */
function loadResourcePromise(url)
{
    return new Promise(load);

    /**
     * @param {function(?)} fulfill
     * @param {function(*)} reject
     */
    function load(fulfill, reject)
    {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", url, true);
        xhr.onreadystatechange = onreadystatechange;

        /**
         * @param {Event} e
         */
        function onreadystatechange(e)
        {
            if (xhr.readyState !== 4)
                return;

            if ([0, 200, 304].indexOf(xhr.status) === -1)  // Testing harness file:/// results in 0.
                reject(new Error("While loading from url " + url + " server responded with a status of " + xhr.status));
            else
                fulfill(e.target.response);
        }
        xhr.send(null);
    }
}

/**
 * http://tools.ietf.org/html/rfc3986#section-5.2.4
 * @param {string} path
 * @return {string}
 */
function normalizePath(path)
{
    if (path.indexOf("..") === -1 && path.indexOf('.') === -1)
        return path;

    var normalizedSegments = [];
    var segments = path.split("/");
    for (var i = 0; i < segments.length; i++) {
        var segment = segments[i];
        if (segment === ".")
            continue;
        else if (segment === "..")
            normalizedSegments.pop();
        else if (segment)
            normalizedSegments.push(segment);
    }
    var normalizedPath = normalizedSegments.join("/");
    if (normalizedPath[normalizedPath.length - 1] === "/")
        return normalizedPath;
    if (path[0] === "/" && normalizedPath)
        normalizedPath = "/" + normalizedPath;
    if ((path[path.length - 1] === "/") || (segments[segments.length - 1] === ".") || (segments[segments.length - 1] === ".."))
        normalizedPath = normalizedPath + "/";

    return normalizedPath;
}

/**
 * @param {!Array.<string>} scriptNames
 * @param {string=} base
 * @return {!Promise.<undefined>}
 */
function loadScriptsPromise(scriptNames, base)
{
    /** @type {!Array.<!Promise.<string>>} */
    var promises = [];
    /** @type {!Array.<string>} */
    var urls = [];
    var sources = new Array(scriptNames.length);
    var scriptToEval = 0;
    for (var i = 0; i < scriptNames.length; ++i) {
        var scriptName = scriptNames[i];
        var sourceURL = (base || self._importScriptPathPrefix) + scriptName;
        var schemaIndex = sourceURL.indexOf("://") + 3;
        sourceURL = sourceURL.substring(0, schemaIndex) + normalizePath(sourceURL.substring(schemaIndex));
        if (_loadedScripts[sourceURL])
            continue;
        urls.push(sourceURL);
        promises.push(loadResourcePromise(sourceURL).then(scriptSourceLoaded.bind(null, i), scriptSourceLoaded.bind(null, i, undefined)));
    }
    return Promise.all(promises).then(undefined);

    /**
     * @param {number} scriptNumber
     * @param {string=} scriptSource
     */
    function scriptSourceLoaded(scriptNumber, scriptSource)
    {
        sources[scriptNumber] = scriptSource || "";
        // Eval scripts as fast as possible.
        while (typeof sources[scriptToEval] !== "undefined") {
            evaluateScript(urls[scriptToEval], sources[scriptToEval]);
            ++scriptToEval;
        }
    }

    /**
     * @param {string} sourceURL
     * @param {string=} scriptSource
     */
    function evaluateScript(sourceURL, scriptSource)
    {
        _loadedScripts[sourceURL] = true;
        if (!scriptSource) {
            // Do not reject, as this is normal in the hosted mode.
            console.error("Empty response arrived for script '" + sourceURL + "'");
            return;
        }
        self.eval(scriptSource + "\n//# sourceURL=" + sourceURL);
    }
}

(function() {
    var baseUrl = self.location ? self.location.origin + self.location.pathname : "";
    self._importScriptPathPrefix = baseUrl.substring(0, baseUrl.lastIndexOf("/") + 1);
})();

/**
 * @constructor
 * @param {!Array.<!Runtime.ModuleDescriptor>} descriptors
 * @param {!Array.<string>=} coreModuleNames
 */
function Runtime(descriptors, coreModuleNames)
{
    /**
     * @type {!Array.<!Runtime.Module>}
     */
    this._modules = [];
    /**
     * @type {!Object.<string, !Runtime.Module>}
     */
    this._modulesMap = {};
    /**
     * @type {!Array.<!Runtime.Extension>}
     */
    this._extensions = [];

    /**
     * @type {!Object.<string, !function(new:Object)>}
     */
    this._cachedTypeClasses = {};

    /**
     * @type {!Object.<string, !Runtime.ModuleDescriptor>}
     */
    this._descriptorsMap = {};

    for (var i = 0; i < descriptors.length; ++i)
        this._registerModule(descriptors[i]);
    if (coreModuleNames)
        this._loadAutoStartModules(coreModuleNames);
}

/**
 * @type {!Object.<string, string>}
 */
Runtime._queryParamsObject = { __proto__: null };

/**
 * @type {!Object.<string, string>}
 */
Runtime.cachedResources = { __proto__: null };

/**
 * @return {boolean}
 */
Runtime.isReleaseMode = function()
{
    return !!allDescriptors.length;
}

/**
 * @param {string} appName
 */
Runtime.startApplication = function(appName)
{
    console.timeStamp("Runtime.startApplication");

    var allDescriptorsByName = {};
    for (var i = 0; Runtime.isReleaseMode() && i < allDescriptors.length; ++i) {
        var d = allDescriptors[i];
        allDescriptorsByName[d["name"]] = d;
    }

    var applicationPromise;
    if (applicationDescriptor)
        applicationPromise = Promise.resolve(applicationDescriptor);
    else
        applicationPromise = loadResourcePromise(appName + ".json").then(JSON.parse.bind(JSON));

    applicationPromise.then(parseModuleDescriptors);

    /**
     * @param {!Array.<!Object>} configuration
     */
    function parseModuleDescriptors(configuration)
    {
        var moduleJSONPromises = [];
        var coreModuleNames = [];
        for (var i = 0; i < configuration.length; ++i) {
            var descriptor = configuration[i];
            if (descriptor["type"] === "worker")
                continue;
            var name = descriptor["name"];
            var moduleJSON = allDescriptorsByName[name];
            if (moduleJSON)
                moduleJSONPromises.push(Promise.resolve(moduleJSON));
            else
                moduleJSONPromises.push(loadResourcePromise(name + "/module.json").then(JSON.parse.bind(JSON)));
            if (descriptor["type"] === "autostart")
                coreModuleNames.push(name);
        }

        Promise.all(moduleJSONPromises).then(instantiateRuntime);
        /**
         * @param {!Array.<!Object>} moduleDescriptors
         */
        function instantiateRuntime(moduleDescriptors)
        {
            for (var i = 0; !Runtime.isReleaseMode() && i < moduleDescriptors.length; ++i) {
                moduleDescriptors[i]["name"] = configuration[i]["name"];
                moduleDescriptors[i]["condition"] = configuration[i]["condition"];
            }
            self.runtime = new Runtime(moduleDescriptors, coreModuleNames);
        }
    }
}

/**
 * @param {string} name
 * @return {?string}
 */
Runtime.queryParam = function(name)
{
    return Runtime._queryParamsObject[name] || null;
}

/**
 * @param {!Array.<string>} banned
 * @return {string}
 */
Runtime.constructQueryParams = function(banned)
{
    var params = [];
    for (var key in Runtime._queryParamsObject) {
        if (!key || banned.indexOf(key) !== -1)
            continue;
        params.push(key + "=" + Runtime._queryParamsObject[key]);
    }
    return params.length ? "?" + params.join("&") : "";
}

/**
 * @return {!Object}
 */
Runtime._experimentsSetting = function()
{
    try {
        return /** @type {!Object} */ (JSON.parse(self.localStorage && self.localStorage["experiments"] ? self.localStorage["experiments"] : "{}"));
    } catch (e) {
        console.error("Failed to parse localStorage['experiments']");
        return {};
    }
}

/**
 * @param {!Array.<!Promise.<T, !Error>>} promises
 * @return {!Promise.<!Array.<T>>}
 * @template T
 */
Runtime._some = function(promises)
{
    var all = [];
    var wasRejected = [];
    for (var i = 0; i < promises.length; ++i) {
        // Workaround closure compiler bug.
        var handlerFunction = /** @type {function()} */ (handler.bind(promises[i], i));
        all.push(promises[i].catch(handlerFunction));
    }

    return Promise.all(all).then(filterOutFailuresResults);

    /**
     * @param {!Array.<T>} results
     * @return {!Array.<T>}
     * @template T
     */
    function filterOutFailuresResults(results)
    {
        var filtered = [];
        for (var i = 0; i < results.length; ++i) {
            if (!wasRejected[i])
                filtered.push(results[i]);
        }
        return filtered;
    }

    /**
     * @this {!Promise}
     * @param {number} index
     * @param {!Error} e
     */
    function handler(index, e)
    {
        wasRejected[index] = true;
        console.error(e.stack);
    }
}

Runtime._console = console;
Runtime._originalAssert = console.assert;
Runtime._assert = function(value, message)
{
    if (value)
        return;
    Runtime._originalAssert.call(Runtime._console, value, message + " " + new Error().stack);
}

Runtime.prototype = {
    useTestBase: function()
    {
        Runtime._remoteBase = "http://localhost:8000/inspector-sources/";
    },

    /**
     * @param {!Runtime.ModuleDescriptor} descriptor
     */
    _registerModule: function(descriptor)
    {
        var module = new Runtime.Module(this, descriptor);
        this._modules.push(module);
        this._modulesMap[descriptor["name"]] = module;
    },

    /**
     * @param {string} moduleName
     * @return {!Promise.<undefined>}
     */
    loadModulePromise: function(moduleName)
    {
        return this._modulesMap[moduleName]._loadPromise();
    },

    /**
     * @param {!Array.<string>} moduleNames
     * @return {!Promise.<!Array.<*>>}
     */
    _loadAutoStartModules: function(moduleNames)
    {
        var promises = [];
        for (var i = 0; i < moduleNames.length; ++i) {
            if (Runtime.isReleaseMode())
                this._modulesMap[moduleNames[i]]._loaded = true;
            else
                promises.push(this.loadModulePromise(moduleNames[i]));
        }
        return Promise.all(promises);
    },

    /**
     * @param {!Runtime.Extension} extension
     * @param {?function(function(new:Object)):boolean} predicate
     * @return {boolean}
     */
    _checkExtensionApplicability: function(extension, predicate)
    {
        if (!predicate)
            return false;
        var contextTypes = /** @type {!Array.<string>|undefined} */ (extension.descriptor().contextTypes);
        if (!contextTypes)
            return true;
        for (var i = 0; i < contextTypes.length; ++i) {
            var contextType = this._resolve(contextTypes[i]);
            var isMatching = !!contextType && predicate(contextType);
            if (isMatching)
                return true;
        }
        return false;
    },

    /**
     * @param {!Runtime.Extension} extension
     * @param {?Object} context
     * @return {boolean}
     */
    isExtensionApplicableToContext: function(extension, context)
    {
        if (!context)
            return true;
        return this._checkExtensionApplicability(extension, isInstanceOf);

        /**
         * @param {!Function} targetType
         * @return {boolean}
         */
        function isInstanceOf(targetType)
        {
            return context instanceof targetType;
        }
    },

    /**
     * @param {!Runtime.Extension} extension
     * @param {!Set.<!Function>=} currentContextTypes
     * @return {boolean}
     */
    isExtensionApplicableToContextTypes: function(extension, currentContextTypes)
    {
        if (!extension.descriptor().contextTypes)
            return true;

        return this._checkExtensionApplicability(extension, currentContextTypes ? isContextTypeKnown : null);

        /**
         * @param {!Function} targetType
         * @return {boolean}
         */
        function isContextTypeKnown(targetType)
        {
            return currentContextTypes.has(targetType);
        }
    },

    /**
     * @param {*} type
     * @param {?Object=} context
     * @return {!Array.<!Runtime.Extension>}
     */
    extensions: function(type, context)
    {
        return this._extensions.filter(filter).sort(orderComparator);

        /**
         * @param {!Runtime.Extension} extension
         * @return {boolean}
         */
        function filter(extension)
        {
            if (extension._type !== type && extension._typeClass() !== type)
                return false;
            if (!extension.enabled())
                return false;
            return !context || extension.isApplicable(context);
        }

        /**
         * @param {!Runtime.Extension} extension1
         * @param {!Runtime.Extension} extension2
         * @return {number}
         */
        function orderComparator(extension1, extension2)
        {
            var order1 = extension1.descriptor()["order"] || 0;
            var order2 = extension2.descriptor()["order"] || 0;
            return order1 - order2;
        }
    },

    /**
     * @param {*} type
     * @param {?Object=} context
     * @return {?Runtime.Extension}
     */
    extension: function(type, context)
    {
        return this.extensions(type, context)[0] || null;
    },

    /**
     * @param {*} type
     * @param {?Object=} context
     * @return {!Promise.<!Array.<!Object>>}
     */
    instancesPromise: function(type, context)
    {
        var extensions = this.extensions(type, context);
        var promises = [];
        for (var i = 0; i < extensions.length; ++i)
            promises.push(extensions[i].instancePromise());
        return Runtime._some(promises);
    },

    /**
     * @param {*} type
     * @param {?Object=} context
     * @return {!Promise.<!Object>}
     */
    instancePromise: function(type, context)
    {
        var extension = this.extension(type, context);
        if (!extension)
            return Promise.reject(new Error("No such extension: " + type + " in given context."));
        return extension.instancePromise();
    },

    /**
     * @return {?function(new:Object)}
     */
    _resolve: function(typeName)
    {
        if (!this._cachedTypeClasses[typeName]) {
            var path = typeName.split(".");
            var object = window;
            for (var i = 0; object && (i < path.length); ++i)
                object = object[path[i]];
            if (object)
                this._cachedTypeClasses[typeName] = /** @type function(new:Object) */(object);
        }
        return this._cachedTypeClasses[typeName] || null;
    }
}

/**
 * @constructor
 */
Runtime.ModuleDescriptor = function()
{
    /**
     * @type {string}
     */
    this.name;

    /**
     * @type {!Array.<!Runtime.ExtensionDescriptor>}
     */
    this.extensions;

    /**
     * @type {!Array.<string>|undefined}
     */
    this.dependencies;

    /**
     * @type {!Array.<string>}
     */
    this.scripts;

    /**
     * @type {boolean|undefined}
     */
    this.remote;
}

/**
 * @constructor
 */
Runtime.ExtensionDescriptor = function()
{
    /**
     * @type {string}
     */
    this.type;

    /**
     * @type {string|undefined}
     */
    this.className;

    /**
     * @type {!Array.<string>|undefined}
     */
    this.contextTypes;
}

/**
 * @constructor
 * @param {!Runtime} manager
 * @param {!Runtime.ModuleDescriptor} descriptor
 */
Runtime.Module = function(manager, descriptor)
{
    this._manager = manager;
    this._descriptor = descriptor;
    this._name = descriptor.name;
    /** @type {!Object.<string, ?Object>} */
    this._instanceMap = {};
    var extensions = /** @type {?Array.<!Runtime.ExtensionDescriptor>} */ (descriptor.extensions);
    for (var i = 0; extensions && i < extensions.length; ++i)
        this._manager._extensions.push(new Runtime.Extension(this, extensions[i]));
    this._loaded = false;
}

Runtime.Module.prototype = {
    /**
     * @return {string}
     */
    name: function()
    {
        return this._name;
    },

    /**
     * @return {boolean}
     */
    enabled: function()
    {
        var activatorExperiment = this._descriptor["experiment"];
        if (activatorExperiment && !Runtime.experiments.isEnabled(activatorExperiment))
            return false;
        var condition = this._descriptor["condition"];
        if (condition && !Runtime.queryParam(condition))
            return false;
        return true;
    },

    /**
     * @param {string} name
     * @return {string}
     */
    resource: function(name)
    {
        var fullName = this._name + "/" + name;
        var content = Runtime.cachedResources[fullName];
        if (!content)
            throw new Error(fullName + " not preloaded. Check module.json");
        return content;
    },

    /**
     * @return {!Promise.<undefined>}
     */
    _loadPromise: function()
    {
        if (this._loaded)
            return Promise.resolve();

        if (!this.enabled())
            return Promise.reject(new Error("Module " + this._name + " is not enabled"));

        if (this._pendingLoadPromise)
            return this._pendingLoadPromise;

        var dependencies = this._descriptor.dependencies;
        var dependencyPromises = [];
        for (var i = 0; dependencies && i < dependencies.length; ++i)
            dependencyPromises.push(this._manager._modulesMap[dependencies[i]]._loadPromise());

        this._pendingLoadPromise = Promise.all(dependencyPromises)
            .then(this._loadResources.bind(this))
            .then(this._loadScripts.bind(this))
            .then(markAsLoaded.bind(this));

        return this._pendingLoadPromise;

        /**
         * @this {Runtime.Module}
         */
        function markAsLoaded()
        {
            delete this._pendingLoadPromise;
            this._loaded = true;
        }
    },

    /**
     * @return {!Promise.<undefined>}
     * @this {Runtime.Module}
     */
    _loadResources: function()
    {
        var resources = this._descriptor["resources"];
        if (!resources)
            return Promise.resolve();
        var promises = [];
        for (var i = 0; i < resources.length; ++i) {
            var url = this._modularizeURL(resources[i]);
            promises.push(loadResourcePromise(url).then(cacheResource.bind(this, url), cacheResource.bind(this, url, undefined)));
        }
        return Promise.all(promises).then(undefined);

        /**
         * @param {string} path
         * @param {string=} content
         */
        function cacheResource(path, content)
        {
            if (!content) {
                console.error("Failed to load resource: " + path);
                return;
            }
            var sourceURL = window.location.href;
            if (window.location.search)
                sourceURL = sourceURL.replace(window.location.search, "");
            sourceURL = sourceURL.substring(0, sourceURL.lastIndexOf("/") + 1) + path;
            Runtime.cachedResources[path] = content + "\n/*# sourceURL=" + sourceURL + " */";
        }
    },

    /**
     * @return {!Promise.<undefined>}
     */
    _loadScripts: function()
    {
        if (!this._descriptor.scripts)
            return Promise.resolve();

        if (Runtime.isReleaseMode())
            return loadScriptsPromise([this._name + "_module.js"], this._remoteBase());

        return loadScriptsPromise(this._descriptor.scripts.map(this._modularizeURL, this));
    },

    /**
     * @param {string} resourceName
     */
    _modularizeURL: function(resourceName)
    {
        return normalizePath(this._name + "/" + resourceName);
    },

    /**
     * @return {string|undefined}
     */
    _remoteBase: function()
    {
        return this._descriptor.remote && Runtime._remoteBase || undefined;
    },

    /**
     * @param {string} value
     * @return {string}
     */
    substituteURL: function(value)
    {
        var base = this._remoteBase() || "";
        return value.replace(/@url\(([^\)]*?)\)/g, convertURL.bind(this));

        function convertURL(match, url)
        {
            return base + this._modularizeURL(url);
        }
    },

    /**
     * @param {string} className
     * @param {!Runtime.Extension} extension
     * @return {?Object}
     */
    _instance: function(className, extension)
    {
        if (className in this._instanceMap)
            return this._instanceMap[className];

        var constructorFunction = window.eval(className);
        if (!(constructorFunction instanceof Function)) {
            this._instanceMap[className] = null;
            return null;
        }

        var instance = new constructorFunction(extension);
        this._instanceMap[className] = instance;
        return instance;
    }
}

/**
 * @constructor
 * @param {!Runtime.Module} module
 * @param {!Runtime.ExtensionDescriptor} descriptor
 */
Runtime.Extension = function(module, descriptor)
{
    this._module = module;
    this._descriptor = descriptor;

    this._type = descriptor.type;
    this._hasTypeClass = this._type.charAt(0) === "@";

    /**
     * @type {?string}
     */
    this._className = descriptor.className || null;
}

Runtime.Extension.prototype = {
    /**
     * @return {!Object}
     */
    descriptor: function()
    {
        return this._descriptor;
    },

    /**
     * @return {!Runtime.Module}
     */
    module: function()
    {
        return this._module;
    },

    /**
     * @return {boolean}
     */
    enabled: function()
    {
        var activatorExperiment = this.descriptor()["experiment"];
        if (activatorExperiment && activatorExperiment.startsWith("!") && Runtime.experiments.isEnabled(activatorExperiment.substring(1)))
            return false;
        if (activatorExperiment && !activatorExperiment.startsWith("!") && !Runtime.experiments.isEnabled(activatorExperiment))
            return false;
        var condition = this.descriptor()["condition"];
        if (condition && !Runtime.queryParam(condition))
            return false;
        return this._module.enabled();
    },

    /**
     * @return {?function(new:Object)}
     */
    _typeClass: function()
    {
        if (!this._hasTypeClass)
            return null;
        return this._module._manager._resolve(this._type.substring(1));
    },

    /**
     * @param {?Object} context
     * @return {boolean}
     */
    isApplicable: function(context)
    {
        return this._module._manager.isExtensionApplicableToContext(this, context);
    },

    /**
     * @return {!Promise.<!Object>}
     */
    instancePromise: function()
    {
        if (!this._className)
            return Promise.reject(new Error("No class name in extension"));
        var className = this._className;
        if (this._instance)
            return Promise.resolve(this._instance);

        return this._module._loadPromise().then(constructInstance.bind(this));

        /**
         * @return {!Object}
         * @this {Runtime.Extension}
         */
        function constructInstance()
        {
            var result = this._module._instance(className, this);
            if (!result)
                return Promise.reject("Could not instantiate: " + className);
            return result;
        }
    },

    /**
     * @param {string} platform
     * @return {string}
     */
    title: function(platform)
    {
        // FIXME: should be WebInspector.UIString() but runtime is not l10n aware yet.
        return this._descriptor["title-" + platform] || this._descriptor["title"];
    }
}

/**
 * @constructor
 */
Runtime.ExperimentsSupport = function()
{
    this._supportEnabled = Runtime.queryParam("experiments") !== null;
    this._experiments = [];
    this._experimentNames = {};
    this._enabledTransiently = {};
}

Runtime.ExperimentsSupport.prototype = {
    /**
     * @return {!Array.<!Runtime.Experiment>}
     */
    allConfigurableExperiments: function()
    {
        var result = [];
        for (var i = 0; i < this._experiments.length; i++) {
            var experiment = this._experiments[i];
            if (!this._enabledTransiently[experiment.name])
                result.push(experiment);
        }
        return result;
    },

    /**
     * @return {boolean}
     */
    supportEnabled: function()
    {
        return this._supportEnabled;
    },

    /**
     * @param {!Object} value
     */
    _setExperimentsSetting: function(value)
    {
        if (!self.localStorage)
            return;
        self.localStorage["experiments"] = JSON.stringify(value);
    },

    /**
     * @param {string} experimentName
     * @param {string} experimentTitle
     * @param {boolean=} hidden
     */
    register: function(experimentName, experimentTitle, hidden)
    {
        Runtime._assert(!this._experimentNames[experimentName], "Duplicate registration of experiment " + experimentName);
        this._experimentNames[experimentName] = true;
        this._experiments.push(new Runtime.Experiment(this, experimentName, experimentTitle, !!hidden));
    },

    /**
     * @param {string} experimentName
     * @return {boolean}
     */
    isEnabled: function(experimentName)
    {
        this._checkExperiment(experimentName);

        if (this._enabledTransiently[experimentName])
            return true;
        if (!this.supportEnabled())
            return false;

        return !!Runtime._experimentsSetting()[experimentName];
    },

    /**
     * @param {string} experimentName
     * @param {boolean} enabled
     */
    setEnabled: function(experimentName, enabled)
    {
        this._checkExperiment(experimentName);
        var experimentsSetting = Runtime._experimentsSetting();
        experimentsSetting[experimentName] = enabled;
        this._setExperimentsSetting(experimentsSetting);
    },

    /**
     * @param {!Array.<string>} experimentNames
     */
    setDefaultExperiments: function(experimentNames)
    {
        for (var i = 0; i < experimentNames.length; ++i) {
            this._checkExperiment(experimentNames[i]);
            this._enabledTransiently[experimentNames[i]] = true;
        }
    },

    /**
     * @param {string} experimentName
     */
    enableForTest: function(experimentName)
    {
        this._checkExperiment(experimentName);
        this._enabledTransiently[experimentName] = true;
    },

    clearForTest: function()
    {
        this._experiments = [];
        this._experimentNames = {};
        this._enabledTransiently = {};
    },

    cleanUpStaleExperiments: function()
    {
        var experimentsSetting = Runtime._experimentsSetting();
        var cleanedUpExperimentSetting = {};
        for (var i = 0; i < this._experiments.length; ++i) {
            var experimentName = this._experiments[i].name;
            if (experimentsSetting[experimentName])
                cleanedUpExperimentSetting[experimentName] = true;
        }
        this._setExperimentsSetting(cleanedUpExperimentSetting);
    },

    /**
     * @param {string} experimentName
     */
    _checkExperiment: function(experimentName)
    {
        Runtime._assert(this._experimentNames[experimentName], "Unknown experiment " + experimentName);
    }
}

/**
 * @constructor
 * @param {!Runtime.ExperimentsSupport} experiments
 * @param {string} name
 * @param {string} title
 * @param {boolean} hidden
 */
Runtime.Experiment = function(experiments, name, title, hidden)
{
    this.name = name;
    this.title = title;
    this.hidden = hidden;
    this._experiments = experiments;
}

Runtime.Experiment.prototype = {
    /**
     * @return {boolean}
     */
    isEnabled: function()
    {
        return this._experiments.isEnabled(this.name);
    },

    /**
     * @param {boolean} enabled
     */
    setEnabled: function(enabled)
    {
        this._experiments.setEnabled(this.name, enabled);
    }
}

{(function parseQueryParameters()
{
    var queryParams = location.search;
    if (!queryParams)
        return;
    var params = queryParams.substring(1).split("&");
    for (var i = 0; i < params.length; ++i) {
        var pair = params[i].split("=");
        var name = pair.shift();
        Runtime._queryParamsObject[name] = pair.join("=");
    }
})();}


// This must be constructed after the query parameters have been parsed.
Runtime.experiments = new Runtime.ExperimentsSupport();

/**
 * @type {?string}
 */
Runtime._remoteBase = Runtime.queryParam("remoteBase");

/** @type {!Runtime} */
var runtime;
