/*
 * Copyright (C) 2009 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.
 */

/**
 * @constructor
 * @param {!WebInspector.SettingsStorage} storage
 */
WebInspector.Settings = function(storage)
{
    this._settingsStorage = storage;
    var clearLocalStorage = window.localStorage ? window.localStorage.clear.bind(window.localStorage) : undefined;
    this._localStorage = new WebInspector.SettingsStorage(window.localStorage || {}, undefined, undefined, clearLocalStorage);

    this._eventSupport = new WebInspector.Object();
    /** @type {!Map<string, !WebInspector.Setting>} */
    this._registry = new Map();
    /** @type {!Map<string, !WebInspector.Setting>} */
    this._moduleSettings = new Map();
    self.runtime.extensions("setting").forEach(this._registerModuleSetting.bind(this));
}

WebInspector.Settings.prototype = {
    /**
     * @param {!Runtime.Extension} extension
     */
    _registerModuleSetting: function(extension)
    {
        var descriptor = extension.descriptor();
        var settingName = descriptor["settingName"];
        var settingType = descriptor["settingType"];
        var defaultValue = descriptor["defaultValue"];
        var isLocal = !!descriptor["local"];
        var setting = settingType === "regex" ? this.createRegExpSetting(settingName, defaultValue, undefined, isLocal) : this.createSetting(settingName, defaultValue, isLocal);
        this._moduleSettings.set(settingName, setting);
    },

    /**
     * @param {string} settingName
     * @return {!WebInspector.Setting}
     */
    moduleSetting: function(settingName)
    {
        var setting = this._moduleSettings.get(settingName);
        if (!setting)
            throw new Error("No setting registered: " + settingName);
        return setting;
    },

    /**
     * @param {string} settingName
     * @return {!WebInspector.Setting}
     */
    settingForTest: function(settingName)
    {
        var setting = this._registry.get(settingName);
        if (!setting)
            throw new Error("No setting registered: " + settingName);
        return setting;
    },

    /**
     * @param {string} key
     * @param {*} defaultValue
     * @param {boolean=} isLocal
     * @return {!WebInspector.Setting}
     */
    createSetting: function(key, defaultValue, isLocal)
    {
        if (!this._registry.get(key))
            this._registry.set(key, new WebInspector.Setting(this, key, defaultValue, this._eventSupport, isLocal ? this._localStorage : this._settingsStorage));
        return /** @type {!WebInspector.Setting} */ (this._registry.get(key));
    },

    /**
     * @param {string} key
     * @param {*} defaultValue
     * @return {!WebInspector.Setting}
     */
    createLocalSetting: function(key, defaultValue)
    {
        return this.createSetting(key, defaultValue, true);
    },

    /**
     * @param {string} key
     * @param {string} defaultValue
     * @param {string=} regexFlags
     * @param {boolean=} isLocal
     * @return {!WebInspector.RegExpSetting}
     */
    createRegExpSetting: function(key, defaultValue, regexFlags, isLocal)
    {
        if (!this._registry.get(key))
            this._registry.set(key, new WebInspector.RegExpSetting(this, key, defaultValue, this._eventSupport, isLocal ? this._localStorage : this._settingsStorage, regexFlags));
        return /** @type {!WebInspector.RegExpSetting} */ (this._registry.get(key));
    },

    clearAll: function()
    {
        this._settingsStorage.removeAll();
        this._localStorage.removeAll();
        var versionSetting = WebInspector.settings.createSetting(WebInspector.VersionController._currentVersionName, 0);
        versionSetting.set(WebInspector.VersionController.currentVersion);
    }
}

/**
 * @constructor
 * @param {!Object} object
 * @param {function(string, string)=} setCallback
 * @param {function(string)=} removeCallback
 * @param {function(string)=} removeAllCallback
 */
WebInspector.SettingsStorage = function(object, setCallback, removeCallback, removeAllCallback)
{
    this._object = object;
    this._setCallback = setCallback || function() {};
    this._removeCallback = removeCallback || function() {};
    this._removeAllCallback = removeAllCallback || function() {};
}

WebInspector.SettingsStorage.prototype = {
    /**
     * @param {string} name
     * @param {string} value
     */
    set: function(name, value)
    {
        this._object[name] = value;
        this._setCallback(name, value);
    },

    /**
     * @param {string} name
     * @return {boolean}
     */
    has: function(name)
    {
        return name in this._object;
    },

    /**
     * @param {string} name
     * @return {string}
     */
    get: function(name)
    {
        return this._object[name];
    },

    /**
     * @param {string} name
     */
    remove: function(name)
    {
        delete this._object[name];
        this._removeCallback(name);
    },

    removeAll: function()
    {
        this._object = {};
        this._removeAllCallback();
    },

    _dumpSizes: function()
    {
        WebInspector.console.log("Ten largest settings: ");

        var sizes = { __proto__: null };
        for (var key in this._object)
            sizes[key] = this._object[key].length;
        var keys = Object.keys(sizes);

        function comparator(key1, key2)
        {
            return sizes[key2] - sizes[key1];
        }

        keys.sort(comparator);

        for (var i = 0; i < 10 && i < keys.length; ++i)
            WebInspector.console.log("Setting: '" + keys[i] + "', size: " + sizes[keys[i]]);
    }
}

/**
 * @constructor
 * @param {!WebInspector.Settings} settings
 * @param {string} name
 * @param {V} defaultValue
 * @param {!WebInspector.Object} eventSupport
 * @param {!WebInspector.SettingsStorage} storage
 * @template V
 */
WebInspector.Setting = function(settings, name, defaultValue, eventSupport, storage)
{
    this._settings = settings;
    this._name = name;
    this._defaultValue = defaultValue;
    this._eventSupport = eventSupport;
    this._storage = storage;
}

WebInspector.Setting.prototype = {
    /**
     * @param {function(!WebInspector.Event)} listener
     * @param {!Object=} thisObject
     */
    addChangeListener: function(listener, thisObject)
    {
        this._eventSupport.addEventListener(this._name, listener, thisObject);
    },

    /**
     * @param {function(!WebInspector.Event)} listener
     * @param {!Object=} thisObject
     */
    removeChangeListener: function(listener, thisObject)
    {
        this._eventSupport.removeEventListener(this._name, listener, thisObject);
    },

    get name()
    {
        return this._name;
    },

    /**
     * @return {V}
     */
    get: function()
    {
        if (typeof this._value !== "undefined")
            return this._value;

        this._value = this._defaultValue;
        if (this._storage.has(this._name)) {
            try {
                this._value = JSON.parse(this._storage.get(this._name));
            } catch (e) {
                this._storage.remove(this._name);
            }
        }
        return this._value;
    },

    /**
     * @param {V} value
     */
    set: function(value)
    {
        this._value = value;
        try {
            var settingString = JSON.stringify(value);
            try {
                this._storage.set(this._name, settingString);
            } catch (e) {
                this._printSettingsSavingError(e.message, this._name, settingString);
            }
        } catch (e) {
            WebInspector.console.error("Cannot stringify setting with name: " + this._name + ", error: " + e.message);
        }
        this._eventSupport.dispatchEventToListeners(this._name, value);
    },

    remove: function()
    {
        this._settings._registry.delete(this._name);
        this._settings._moduleSettings.delete(this._name);
        this._storage.remove(this._name);
    },

    /**
     * @param {string} message
     * @param {string} name
     * @param {string} value
     */
    _printSettingsSavingError: function(message, name, value)
    {
        var errorMessage = "Error saving setting with name: " + this._name + ", value length: " + value.length + ". Error: " + message;
        console.error(errorMessage);
        WebInspector.console.error(errorMessage);
        this._storage._dumpSizes();
    }
}

/**
 * @constructor
 * @extends {WebInspector.Setting}
 * @param {!WebInspector.Settings} settings
 * @param {string} name
 * @param {string} defaultValue
 * @param {!WebInspector.Object} eventSupport
 * @param {!WebInspector.SettingsStorage} storage
 * @param {string=} regexFlags
 */
WebInspector.RegExpSetting = function(settings, name, defaultValue, eventSupport, storage, regexFlags)
{
    WebInspector.Setting.call(this, settings, name, defaultValue ? [{ pattern: defaultValue }] : [], eventSupport, storage);
    this._regexFlags = regexFlags;
}

WebInspector.RegExpSetting.prototype = {
    /**
     * @override
     * @return {string}
     */
    get: function()
    {
        var result = [];
        var items = this.getAsArray();
        for (var i = 0; i < items.length; ++i) {
            var item = items[i];
            if (item.pattern && !item.disabled)
                result.push(item.pattern);
        }
        return result.join("|");
    },

    /**
     * @return {!Array.<{pattern: string, disabled: (boolean|undefined)}>}
     */
    getAsArray: function()
    {
        return WebInspector.Setting.prototype.get.call(this);
    },

    /**
     * @override
     * @param {string} value
     */
    set: function(value)
    {
        this.setAsArray([{ pattern: value }]);
    },

    /**
     * @param {!Array.<{pattern: string, disabled: (boolean|undefined)}>} value
     */
    setAsArray: function(value)
    {
        delete this._regex;
        WebInspector.Setting.prototype.set.call(this, value);
    },

    /**
     * @return {?RegExp}
     */
    asRegExp: function()
    {
        if (typeof this._regex !== "undefined")
            return this._regex;
        this._regex = null;
        try {
            var pattern = this.get();
            if (pattern)
                this._regex = new RegExp(pattern, this._regexFlags || "");
        } catch (e) {
        }
        return this._regex;
    },

    __proto__: WebInspector.Setting.prototype
}

/**
 * @constructor
 */
WebInspector.VersionController = function()
{
}

WebInspector.VersionController._currentVersionName = "inspectorVersion";
WebInspector.VersionController.currentVersion = 18;

WebInspector.VersionController.prototype = {
    updateVersion: function()
    {
        var localStorageVersion = window.localStorage ? window.localStorage[WebInspector.VersionController._currentVersionName] : 0;
        var versionSetting = WebInspector.settings.createSetting(WebInspector.VersionController._currentVersionName, 0);
        var currentVersion = WebInspector.VersionController.currentVersion;
        // While localStorage version exists, treat it as the main one. It'll be erased once migrated to prefs.
        var oldVersion = parseInt(localStorageVersion || "0", 10) || versionSetting.get();
        if (oldVersion === 0) {
            // First run, no need to do anything.
            versionSetting.set(currentVersion);
            return;
        }
        var methodsToRun = this._methodsToRunToUpdateVersion(oldVersion, currentVersion);
        for (var i = 0; i < methodsToRun.length; ++i)
            this[methodsToRun[i]].call(this);
        versionSetting.set(currentVersion);
    },

    /**
     * @param {number} oldVersion
     * @param {number} currentVersion
     */
    _methodsToRunToUpdateVersion: function(oldVersion, currentVersion)
    {
        var result = [];
        for (var i = oldVersion; i < currentVersion; ++i)
            result.push("_updateVersionFrom" + i + "To" + (i + 1));
        return result;
    },

    _updateVersionFrom0To1: function()
    {
        this._clearBreakpointsWhenTooMany(WebInspector.settings.createLocalSetting("breakpoints", []), 500000);
    },

    _updateVersionFrom1To2: function()
    {
        WebInspector.settings.createSetting("previouslyViewedFiles", []).set([]);
    },

    _updateVersionFrom2To3: function()
    {
        WebInspector.settings.createSetting("fileSystemMapping", {}).set({});
        WebInspector.settings.createSetting("fileMappingEntries", []).remove();
    },

    _updateVersionFrom3To4: function()
    {
        var advancedMode = WebInspector.settings.createSetting("showHeaSnapshotObjectsHiddenProperties", false);
        WebInspector.moduleSetting("showAdvancedHeapSnapshotProperties").set(advancedMode.get());
        advancedMode.remove();
    },

    _updateVersionFrom4To5: function()
    {
        var settingNames = {
            "FileSystemViewSidebarWidth": "fileSystemViewSplitViewState",
            "elementsSidebarWidth": "elementsPanelSplitViewState",
            "StylesPaneSplitRatio": "stylesPaneSplitViewState",
            "heapSnapshotRetainersViewSize": "heapSnapshotSplitViewState",
            "InspectorView.splitView": "InspectorView.splitViewState",
            "InspectorView.screencastSplitView": "InspectorView.screencastSplitViewState",
            "Inspector.drawerSplitView": "Inspector.drawerSplitViewState",
            "layerDetailsSplitView": "layerDetailsSplitViewState",
            "networkSidebarWidth": "networkPanelSplitViewState",
            "sourcesSidebarWidth": "sourcesPanelSplitViewState",
            "scriptsPanelNavigatorSidebarWidth": "sourcesPanelNavigatorSplitViewState",
            "sourcesPanelSplitSidebarRatio": "sourcesPanelDebuggerSidebarSplitViewState",
            "timeline-details": "timelinePanelDetailsSplitViewState",
            "timeline-split": "timelinePanelRecorsSplitViewState",
            "timeline-view": "timelinePanelTimelineStackSplitViewState",
            "auditsSidebarWidth": "auditsPanelSplitViewState",
            "layersSidebarWidth": "layersPanelSplitViewState",
            "profilesSidebarWidth": "profilesPanelSplitViewState",
            "resourcesSidebarWidth": "resourcesPanelSplitViewState"
        };
        var empty = {};
        for (var oldName in settingNames) {
            var newName = settingNames[oldName];
            var oldNameH = oldName + "H";

            var newValue = null;
            var oldSetting = WebInspector.settings.createSetting(oldName, empty);
            if (oldSetting.get() !== empty) {
                newValue = newValue || {};
                newValue.vertical = {};
                newValue.vertical.size = oldSetting.get();
                oldSetting.remove();
            }
            var oldSettingH = WebInspector.settings.createSetting(oldNameH, empty);
            if (oldSettingH.get() !== empty) {
                newValue = newValue || {};
                newValue.horizontal = {};
                newValue.horizontal.size = oldSettingH.get();
                oldSettingH.remove();
            }
            if (newValue)
                WebInspector.settings.createSetting(newName, {}).set(newValue);
        }
    },

    _updateVersionFrom5To6: function()
    {
        var settingNames = {
            "debuggerSidebarHidden": "sourcesPanelSplitViewState",
            "navigatorHidden": "sourcesPanelNavigatorSplitViewState",
            "WebInspector.Drawer.showOnLoad": "Inspector.drawerSplitViewState"
        };

        for (var oldName in settingNames) {
            var oldSetting = WebInspector.settings.createSetting(oldName, null);
            if (oldSetting.get() === null) {
                oldSetting.remove();
                continue;
            }

            var newName = settingNames[oldName];
            var invert = oldName === "WebInspector.Drawer.showOnLoad";
            var hidden = oldSetting.get() !== invert;
            oldSetting.remove();
            var showMode = hidden ? "OnlyMain" : "Both";

            var newSetting = WebInspector.settings.createSetting(newName, {});
            var newValue = newSetting.get() || {};
            newValue.vertical = newValue.vertical || {};
            newValue.vertical.showMode = showMode;
            newValue.horizontal = newValue.horizontal || {};
            newValue.horizontal.showMode = showMode;
            newSetting.set(newValue);
        }
    },

    _updateVersionFrom6To7: function()
    {
        var settingNames = {
            "sourcesPanelNavigatorSplitViewState": "sourcesPanelNavigatorSplitViewState",
            "elementsPanelSplitViewState": "elementsPanelSplitViewState",
            "stylesPaneSplitViewState": "stylesPaneSplitViewState",
            "sourcesPanelDebuggerSidebarSplitViewState": "sourcesPanelDebuggerSidebarSplitViewState"
        };

        var empty = {};
        for (var name in settingNames) {
            var setting = WebInspector.settings.createSetting(name, empty);
            var value = setting.get();
            if (value === empty)
                continue;
            // Zero out saved percentage sizes, and they will be restored to defaults.
            if (value.vertical && value.vertical.size && value.vertical.size < 1)
                value.vertical.size = 0;
            if (value.horizontal && value.horizontal.size && value.horizontal.size < 1)
                value.horizontal.size = 0;
            setting.set(value);
        }
    },

    _updateVersionFrom7To8: function()
    {
    },

    _updateVersionFrom8To9: function()
    {
        var settingNames = [
            "skipStackFramesPattern",
            "workspaceFolderExcludePattern"
        ];

        for (var i = 0; i < settingNames.length; ++i) {
            var setting = WebInspector.settings.createSetting(settingNames[i], "");
            var value = setting.get();
            if (!value)
                return;
            if (typeof value === "string")
                value = [value];
            for (var j = 0; j < value.length; ++j) {
                if (typeof value[j] === "string")
                    value[j] = { pattern: value[j] };
            }
            setting.set(value);
        }
    },

    _updateVersionFrom9To10: function()
    {
        // This one is localStorage specific, which is fine.
        if (!window.localStorage)
            return;
        for (var key in window.localStorage) {
            if (key.startsWith("revision-history"))
                window.localStorage.removeItem(key);
        }
    },

    _updateVersionFrom10To11: function()
    {
        var oldSettingName = "customDevicePresets";
        var newSettingName = "customEmulatedDeviceList";
        var oldSetting = WebInspector.settings.createSetting(oldSettingName, undefined);
        var list = oldSetting.get();
        if (!Array.isArray(list))
            return;
        var newList = [];
        for (var i = 0; i < list.length; ++i) {
            var value = list[i];
            var device = {};
            device["title"] = value["title"];
            device["type"] = "unknown";
            device["user-agent"] = value["userAgent"];
            device["capabilities"] = [];
            if (value["touch"])
                device["capabilities"].push("touch");
            if (value["mobile"])
                device["capabilities"].push("mobile");
            device["screen"] = {};
            device["screen"]["vertical"] = {width: value["width"], height: value["height"]};
            device["screen"]["horizontal"] = {width: value["height"], height: value["width"]};
            device["screen"]["device-pixel-ratio"] = value["deviceScaleFactor"];
            device["modes"] = [];
            device["show-by-default"] = true;
            device["show"] = "Default";
            newList.push(device);
        }
        if (newList.length)
            WebInspector.settings.createSetting(newSettingName, []).set(newList);
        oldSetting.remove();
    },

    _updateVersionFrom11To12: function()
    {
        this._migrateSettingsFromLocalStorage();
    },

    _updateVersionFrom12To13: function()
    {
        this._migrateSettingsFromLocalStorage();
        WebInspector.settings.createSetting("timelineOverviewMode", "").remove();
    },

    _updateVersionFrom13To14: function()
    {
        var defaultValue = { "throughput": -1, "latency": 0 };
        WebInspector.settings.createSetting("networkConditions", defaultValue).set(defaultValue);
    },

    _updateVersionFrom14To15: function()
    {
        var setting = WebInspector.settings.createLocalSetting("workspaceExcludedFolders", {});
        var oldValue = setting.get();
        var newValue = {};
        for (var fileSystemPath in oldValue) {
            newValue[fileSystemPath] = [];
            for (var entry of oldValue[fileSystemPath])
                newValue[fileSystemPath].push(entry.path);
        }
        setting.set(newValue);
    },

    _updateVersionFrom15To16: function()
    {
        var setting = WebInspector.settings.createSetting("InspectorView.panelOrder", {});
        var tabOrders = setting.get();
        for (var key of Object.keys(tabOrders))
            tabOrders[key] = (tabOrders[key] + 1) * 10;
        setting.set(tabOrders);
    },

    _updateVersionFrom16To17: function()
    {
        var setting = WebInspector.settings.createSetting("networkConditionsCustomProfiles", []);
        var oldValue = setting.get();
        var newValue = [];
        if (Array.isArray(oldValue)) {
            for (var preset of oldValue) {
                if (typeof preset.title === "string" && typeof preset.value === "object" && typeof preset.value.throughput === "number" && typeof preset.value.latency === "number")
                    newValue.push({title: preset.title, value: {download: preset.value.throughput, upload: preset.value.throughput, latency: preset.value.latency}});
            }
        }
        setting.set(newValue);
    },

    _updateVersionFrom17To18: function()
    {
        var setting = WebInspector.settings.createLocalSetting("workspaceExcludedFolders", {});
        var oldValue = setting.get();
        var newValue = {};
        for (var oldKey in oldValue) {
            var newKey = oldKey.replace(/\\/g, "/");
            if (!newKey.startsWith("file://")) {
                if (newKey.startsWith("/"))
                    newKey = "file://" + newKey;
                else
                    newKey = "file:///" + newKey;
            }
            newValue[newKey] = oldValue[oldKey];
        }
        setting.set(newValue);
    },

    _migrateSettingsFromLocalStorage: function()
    {
        // This step migrates all the settings except for the ones below into the browser profile.
        var localSettings = [ "advancedSearchConfig", "breakpoints", "consoleHistory", "domBreakpoints", "eventListenerBreakpoints",
                              "fileSystemMapping", "lastSelectedSourcesSidebarPaneTab", "previouslyViewedFiles",
                              "savedURLs", "watchExpressions", "workspaceExcludedFolders", "xhrBreakpoints" ].keySet();
        if (!window.localStorage)
            return;

        for (var key in window.localStorage) {
            if (key in localSettings)
                continue;
            var value = window.localStorage[key];
            window.localStorage.removeItem(key);
            WebInspector.settings._settingsStorage[key] = value;
        }
    },

    /**
     * @param {!WebInspector.Setting} breakpointsSetting
     * @param {number} maxBreakpointsCount
     */
    _clearBreakpointsWhenTooMany: function(breakpointsSetting, maxBreakpointsCount)
    {
        // If there are too many breakpoints in a storage, it is likely due to a recent bug that caused
        // periodical breakpoints duplication leading to inspector slowness.
        if (breakpointsSetting.get().length > maxBreakpointsCount)
            breakpointsSetting.set([]);
    }
}

/**
 * @type {!WebInspector.Settings}
 */
WebInspector.settings;

/**
 * @param {string} settingName
 * @return {!WebInspector.Setting}
 */
WebInspector.moduleSetting = function(settingName)
{
    return WebInspector.settings.moduleSetting(settingName);
}

/**
 * @param {string} settingName
 * @return {!WebInspector.Setting}
 */
WebInspector.settingForTest = function(settingName)
{
    return WebInspector.settings.settingForTest(settingName);
}
