// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable indent */
(function(window) {

  // DevToolsAPI ----------------------------------------------------------------

  /**
   * @unrestricted
   */
  const DevToolsAPIImpl = class {
    constructor() {
      /**
       * @type {number}
       */
      this._lastCallId = 0;

      /**
       * @type {!Object.<number, function(?Object)>}
       */
      this._callbacks = {};

      /**
       * @type {!Array.<!ExtensionDescriptor>}
       */
      this._pendingExtensionDescriptors = [];

      /**
       * @type {?function(!ExtensionDescriptor)}
       */
      this._addExtensionCallback = null;
    }

    /**
     * @param {number} id
     * @param {?Object} arg
     */
    embedderMessageAck(id, arg) {
      const callback = this._callbacks[id];
      delete this._callbacks[id];
      if (callback) {
        callback(arg);
      }
    }

    /**
     * @param {string} method
     * @param {!Array.<*>} args
     * @param {?function(?Object)} callback
     */
    sendMessageToEmbedder(method, args, callback) {
      const callId = ++this._lastCallId;
      if (callback) {
        this._callbacks[callId] = callback;
      }
      const message = {'id': callId, 'method': method};
      if (args.length) {
        message.params = args;
      }
      DevToolsHost.sendMessageToEmbedder(JSON.stringify(message));
    }

    /**
     * @param {string} method
     * @param {!Array<*>} args
     */
    _dispatchOnInspectorFrontendAPI(method, args) {
      const inspectorFrontendAPI = /** @type {!Object<string, function()>} */ (window['InspectorFrontendAPI']);
      inspectorFrontendAPI[method].apply(inspectorFrontendAPI, args);
    }

    // API methods below this line --------------------------------------------

    /**
     * @param {!Array.<!ExtensionDescriptor>} extensions
     */
    addExtensions(extensions) {
      // Support for legacy front-ends (<M41).
      if (window['WebInspector'] && window['WebInspector']['addExtensions']) {
        window['WebInspector']['addExtensions'](extensions);
      } else {
        // The addExtensions command is sent as the onload event happens for
        // DevTools front-end. We should buffer this command until the frontend
        // is ready for it.
        if (this._addExtensionCallback) {
          extensions.forEach(this._addExtensionCallback);
        } else {
          this._pendingExtensionDescriptors.pushAll(extensions);
        }
      }
    }

    /**
     * @param {string} url
     */
    appendedToURL(url) {
      this._dispatchOnInspectorFrontendAPI('appendedToURL', [url]);
    }

    /**
     * @param {string} url
     */
    canceledSaveURL(url) {
      this._dispatchOnInspectorFrontendAPI('canceledSaveURL', [url]);
    }

    contextMenuCleared() {
      this._dispatchOnInspectorFrontendAPI('contextMenuCleared', []);
    }

    /**
     * @param {string} id
     */
    contextMenuItemSelected(id) {
      this._dispatchOnInspectorFrontendAPI('contextMenuItemSelected', [id]);
    }

    /**
     * @param {number} count
     */
    deviceCountUpdated(count) {
      this._dispatchOnInspectorFrontendAPI('deviceCountUpdated', [count]);
    }

    /**
     * @param {!Adb.Config} config
     */
    devicesDiscoveryConfigChanged(config) {
      this._dispatchOnInspectorFrontendAPI('devicesDiscoveryConfigChanged', [config]);
    }

    /**
     * @param {!Adb.PortForwardingStatus} status
     */
    devicesPortForwardingStatusChanged(status) {
      this._dispatchOnInspectorFrontendAPI('devicesPortForwardingStatusChanged', [status]);
    }

    /**
     * @param {!Array.<!Adb.Device>} devices
     */
    devicesUpdated(devices) {
      this._dispatchOnInspectorFrontendAPI('devicesUpdated', [devices]);
    }

    /**
     * @param {string} message
     */
    dispatchMessage(message) {
      this._dispatchOnInspectorFrontendAPI('dispatchMessage', [message]);
    }

    /**
     * @param {string} messageChunk
     * @param {number} messageSize
     */
    dispatchMessageChunk(messageChunk, messageSize) {
      this._dispatchOnInspectorFrontendAPI('dispatchMessageChunk', [messageChunk, messageSize]);
    }

    enterInspectElementMode() {
      this._dispatchOnInspectorFrontendAPI('enterInspectElementMode', []);
    }

    /**
     * @param {!{r: number, g: number, b: number, a: number}} color
     */
    eyeDropperPickedColor(color) {
      this._dispatchOnInspectorFrontendAPI('eyeDropperPickedColor', [color]);
    }

    /**
     * @param {!Array.<!{fileSystemName: string, rootURL: string, fileSystemPath: string}>} fileSystems
     */
    fileSystemsLoaded(fileSystems) {
      this._dispatchOnInspectorFrontendAPI('fileSystemsLoaded', [fileSystems]);
    }

    /**
     * @param {string} fileSystemPath
     */
    fileSystemRemoved(fileSystemPath) {
      this._dispatchOnInspectorFrontendAPI('fileSystemRemoved', [fileSystemPath]);
    }

    /**
     * @param {?string} error
     * @param {?{type: string, fileSystemName: string, rootURL: string, fileSystemPath: string}} fileSystem
     */
    fileSystemAdded(error, fileSystem) {
      this._dispatchOnInspectorFrontendAPI('fileSystemAdded', [error, fileSystem]);
    }

    /**
     * @param {!Array<string>} changedPaths
     * @param {!Array<string>} addedPaths
     * @param {!Array<string>} removedPaths
     */
    fileSystemFilesChangedAddedRemoved(changedPaths, addedPaths, removedPaths) {
      // Support for legacy front-ends (<M58)
      if (window['InspectorFrontendAPI'] && window['InspectorFrontendAPI']['fileSystemFilesChanged']) {
        this._dispatchOnInspectorFrontendAPI(
            'fileSystemFilesChanged', [changedPaths.concat(addedPaths).concat(removedPaths)]);
      } else {
        this._dispatchOnInspectorFrontendAPI(
            'fileSystemFilesChangedAddedRemoved', [changedPaths, addedPaths, removedPaths]);
      }
    }

    /**
     * @param {number} requestId
     * @param {string} fileSystemPath
     * @param {number} totalWork
     */
    indexingTotalWorkCalculated(requestId, fileSystemPath, totalWork) {
      this._dispatchOnInspectorFrontendAPI('indexingTotalWorkCalculated', [requestId, fileSystemPath, totalWork]);
    }

    /**
     * @param {number} requestId
     * @param {string} fileSystemPath
     * @param {number} worked
     */
    indexingWorked(requestId, fileSystemPath, worked) {
      this._dispatchOnInspectorFrontendAPI('indexingWorked', [requestId, fileSystemPath, worked]);
    }

    /**
     * @param {number} requestId
     * @param {string} fileSystemPath
     */
    indexingDone(requestId, fileSystemPath) {
      this._dispatchOnInspectorFrontendAPI('indexingDone', [requestId, fileSystemPath]);
    }

    /**
     * @param {{type: string, key: string, code: string, keyCode: number, modifiers: number}} event
     */
    keyEventUnhandled(event) {
      event.keyIdentifier = keyCodeToKeyIdentifier(event.keyCode);
      this._dispatchOnInspectorFrontendAPI('keyEventUnhandled', [event]);
    }

    /**
     * @param {function(!ExtensionDescriptor)} callback
     */
    setAddExtensionCallback(callback) {
      this._addExtensionCallback = callback;
      if (this._pendingExtensionDescriptors.length) {
        this._pendingExtensionDescriptors.forEach(this._addExtensionCallback);
        this._pendingExtensionDescriptors = [];
      }
    }

    reattachMainTarget() {
      this._dispatchOnInspectorFrontendAPI('reattachMainTarget', []);
    }

    /**
     * @param {boolean} hard
     */
    reloadInspectedPage(hard) {
      this._dispatchOnInspectorFrontendAPI('reloadInspectedPage', [hard]);
    }

    /**
     * @param {string} url
     * @param {number} lineNumber
     * @param {number} columnNumber
     */
    revealSourceLine(url, lineNumber, columnNumber) {
      this._dispatchOnInspectorFrontendAPI('revealSourceLine', [url, lineNumber, columnNumber]);
    }

    /**
     * @param {string} url
     * @param {string=} fileSystemPath
     */
    savedURL(url, fileSystemPath) {
      this._dispatchOnInspectorFrontendAPI('savedURL', [url, fileSystemPath]);
    }

    /**
     * @param {number} requestId
     * @param {string} fileSystemPath
     * @param {!Array.<string>} files
     */
    searchCompleted(requestId, fileSystemPath, files) {
      this._dispatchOnInspectorFrontendAPI('searchCompleted', [requestId, fileSystemPath, files]);
    }

    /**
     * @param {string} tabId
     */
    setInspectedTabId(tabId) {
      this._inspectedTabIdValue = tabId;

      // Support for legacy front-ends (<M41).
      if (window['WebInspector'] && window['WebInspector']['setInspectedTabId']) {
        window['WebInspector']['setInspectedTabId'](tabId);
      } else {
        this._dispatchOnInspectorFrontendAPI('setInspectedTabId', [tabId]);
      }
    }

    /**
     * @return {string|undefined}
     */
    getInspectedTabId() {
      return this._inspectedTabIdValue;
    }

    /**
     * @param {boolean} useSoftMenu
     */
    setUseSoftMenu(useSoftMenu) {
      this._dispatchOnInspectorFrontendAPI('setUseSoftMenu', [useSoftMenu]);
    }

    /**
     * @param {string} panelName
     */
    showPanel(panelName) {
      this._dispatchOnInspectorFrontendAPI('showPanel', [panelName]);
    }

    /**
     * @param {number} id
     * @param {string} chunk
     * @param {boolean} encoded
     */
    streamWrite(id, chunk, encoded) {
      this._dispatchOnInspectorFrontendAPI('streamWrite', [id, encoded ? this._decodeBase64(chunk) : chunk]);
    }

    /**
     * @param {string} chunk
     * @return {string}
     */
    _decodeBase64(chunk) {
      const request = new XMLHttpRequest();
      request.open('GET', 'data:text/plain;base64,' + chunk, false);
      request.send(null);
      if (request.status === 200) {
        return request.responseText;
      } else {
        console.error('Error while decoding chunk in streamWrite');
        return '';
      }
    }
  };

  const DevToolsAPI = new DevToolsAPIImpl();
  window.DevToolsAPI = DevToolsAPI;

  // InspectorFrontendHostImpl --------------------------------------------------

  /**
   * @implements {InspectorFrontendHostAPI}
   * @unrestricted
   */
  const InspectorFrontendHostImpl = class {
    /**
     * @return {string}
     */
    getSelectionBackgroundColor() {
      return '#6e86ff';
    }

    /**
     * @return {string}
     */
    getSelectionForegroundColor() {
      return '#ffffff';
    }

    /**
     * @return {string}
     */
    getInactiveSelectionBackgroundColor() {
      return '#c9c8c8';
    }

    /**
     * @return {string}
     */
    getInactiveSelectionForegroundColor() {
      return '#323232';
    }

    /**
     * @override
     * @return {string}
     */
    platform() {
      return DevToolsHost.platform();
    }

    /**
     * @override
     */
    loadCompleted() {
      DevToolsAPI.sendMessageToEmbedder('loadCompleted', [], null);
      // Support for legacy (<57) frontends.
      if (window.Runtime && window.Runtime.queryParam) {
        const panelToOpen = window.Runtime.queryParam('panel');
        if (panelToOpen) {
          window.DevToolsAPI.showPanel(panelToOpen);
        }
      }
    }

    /**
     * @override
     */
    bringToFront() {
      DevToolsAPI.sendMessageToEmbedder('bringToFront', [], null);
    }

    /**
     * @override
     */
    closeWindow() {
      DevToolsAPI.sendMessageToEmbedder('closeWindow', [], null);
    }

    /**
     * @override
     * @param {boolean} isDocked
     * @param {function()} callback
     */
    setIsDocked(isDocked, callback) {
      DevToolsAPI.sendMessageToEmbedder('setIsDocked', [isDocked], callback);
    }

    /**
     * Requests inspected page to be placed atop of the inspector frontend with specified bounds.
     * @override
     * @param {{x: number, y: number, width: number, height: number}} bounds
     */
    setInspectedPageBounds(bounds) {
      DevToolsAPI.sendMessageToEmbedder('setInspectedPageBounds', [bounds], null);
    }

    /**
     * @override
     */
    inspectElementCompleted() {
      DevToolsAPI.sendMessageToEmbedder('inspectElementCompleted', [], null);
    }

    /**
     * @override
     * @param {string} url
     * @param {string} headers
     * @param {number} streamId
     * @param {function(!InspectorFrontendHostAPI.LoadNetworkResourceResult)} callback
     */
    loadNetworkResource(url, headers, streamId, callback) {
      DevToolsAPI.sendMessageToEmbedder(
          'loadNetworkResource', [url, headers, streamId], /** @type {function(?Object)} */ (callback));
    }

    /**
     * @override
     * @param {function(!Object<string, string>)} callback
     */
    getPreferences(callback) {
      DevToolsAPI.sendMessageToEmbedder('getPreferences', [], /** @type {function(?Object)} */ (callback));
    }

    /**
     * @override
     * @param {string} name
     * @param {string} value
     */
    setPreference(name, value) {
      DevToolsAPI.sendMessageToEmbedder('setPreference', [name, value], null);
    }

    /**
     * @override
     * @param {string} name
     */
    removePreference(name) {
      DevToolsAPI.sendMessageToEmbedder('removePreference', [name], null);
    }

    /**
     * @override
     */
    clearPreferences() {
      DevToolsAPI.sendMessageToEmbedder('clearPreferences', [], null);
    }

    /**
     * @override
     * @param {string} origin
     * @param {string} script
     */
    setInjectedScriptForOrigin(origin, script) {
      DevToolsAPI.sendMessageToEmbedder('registerExtensionsAPI', [origin, script], null);
    }

    /**
     * @override
     * @param {string} url
     */
    inspectedURLChanged(url) {
      DevToolsAPI.sendMessageToEmbedder('inspectedURLChanged', [url], null);
    }

    /**
     * @override
     * @param {string} text
     */
    copyText(text) {
      DevToolsHost.copyText(text);
    }

    /**
     * @override
     * @param {string} url
     */
    openInNewTab(url) {
      DevToolsAPI.sendMessageToEmbedder('openInNewTab', [url], null);
    }

    /**
     * @override
     * @param {string} fileSystemPath
     */
    showItemInFolder(fileSystemPath) {
      DevToolsAPI.sendMessageToEmbedder('showItemInFolder', [fileSystemPath], null);
    }

    /**
     * @override
     * @param {string} url
     * @param {string} content
     * @param {boolean} forceSaveAs
     */
    save(url, content, forceSaveAs) {
      DevToolsAPI.sendMessageToEmbedder('save', [url, content, forceSaveAs], null);
    }

    /**
     * @override
     * @param {string} url
     * @param {string} content
     */
    append(url, content) {
      DevToolsAPI.sendMessageToEmbedder('append', [url, content], null);
    }

    /**
     * @override
     * @param {string} url
     */
    close(url) {
    }

    /**
     * @override
     * @param {string} message
     */
    sendMessageToBackend(message) {
      DevToolsAPI.sendMessageToEmbedder('dispatchProtocolMessage', [message], null);
    }

    /**
     * @override
     * @param {string} actionName
     * @param {number} actionCode
     * @param {number} bucketSize
     */
    recordEnumeratedHistogram(actionName, actionCode, bucketSize) {
      // Support for M49 frontend.
      if (actionName === 'DevTools.DrawerShown') {
        return;
      }
      DevToolsAPI.sendMessageToEmbedder('recordEnumeratedHistogram', [actionName, actionCode, bucketSize], null);
    }

    /**
     * @override
     * @param {string} histogramName
     * @param {number} duration
     */
    recordPerformanceHistogram(histogramName, duration) {
      DevToolsAPI.sendMessageToEmbedder('recordPerformanceHistogram', [histogramName, duration], null);
    }

    /**
     * @override
     * @param {string} umaName
     */
    recordUserMetricsAction(umaName) {
      DevToolsAPI.sendMessageToEmbedder('recordUserMetricsAction', [umaName], null);
    }

    /**
     * @override
     */
    requestFileSystems() {
      DevToolsAPI.sendMessageToEmbedder('requestFileSystems', [], null);
    }

    /**
     * @override
     * @param {string=} type
     */
    addFileSystem(type) {
      DevToolsAPI.sendMessageToEmbedder('addFileSystem', [type || ''], null);
    }

    /**
     * @override
     * @param {string} fileSystemPath
     */
    removeFileSystem(fileSystemPath) {
      DevToolsAPI.sendMessageToEmbedder('removeFileSystem', [fileSystemPath], null);
    }

    /**
     * @override
     * @param {string} fileSystemId
     * @param {string} registeredName
     * @return {?DOMFileSystem}
     */
    isolatedFileSystem(fileSystemId, registeredName) {
      return DevToolsHost.isolatedFileSystem(fileSystemId, registeredName);
    }

    /**
     * @override
     * @param {!FileSystem} fileSystem
     */
    upgradeDraggedFileSystemPermissions(fileSystem) {
      DevToolsHost.upgradeDraggedFileSystemPermissions(fileSystem);
    }

    /**
     * @override
     * @param {number} requestId
     * @param {string} fileSystemPath
     * @param {string} excludedFolders
     */
    indexPath(requestId, fileSystemPath, excludedFolders) {
      // |excludedFolders| added in M67. For backward compatibility,
      // pass empty array.
      excludedFolders = excludedFolders || '[]';
      DevToolsAPI.sendMessageToEmbedder('indexPath', [requestId, fileSystemPath, excludedFolders], null);
    }

    /**
     * @override
     * @param {number} requestId
     */
    stopIndexing(requestId) {
      DevToolsAPI.sendMessageToEmbedder('stopIndexing', [requestId], null);
    }

    /**
     * @override
     * @param {number} requestId
     * @param {string} fileSystemPath
     * @param {string} query
     */
    searchInPath(requestId, fileSystemPath, query) {
      DevToolsAPI.sendMessageToEmbedder('searchInPath', [requestId, fileSystemPath, query], null);
    }

    /**
     * @override
     * @return {number}
     */
    zoomFactor() {
      return DevToolsHost.zoomFactor();
    }

    /**
     * @override
     */
    zoomIn() {
      DevToolsAPI.sendMessageToEmbedder('zoomIn', [], null);
    }

    /**
     * @override
     */
    zoomOut() {
      DevToolsAPI.sendMessageToEmbedder('zoomOut', [], null);
    }

    /**
     * @override
     */
    resetZoom() {
      DevToolsAPI.sendMessageToEmbedder('resetZoom', [], null);
    }

    /**
     * @override
     * @param {string} shortcuts
     */
    setWhitelistedShortcuts(shortcuts) {
      DevToolsAPI.sendMessageToEmbedder('setWhitelistedShortcuts', [shortcuts], null);
    }

    /**
     * @override
     * @param {boolean} active
     */
    setEyeDropperActive(active) {
      DevToolsAPI.sendMessageToEmbedder('setEyeDropperActive', [active], null);
    }

    /**
     * @override
     * @param {!Array<string>} certChain
     */
    showCertificateViewer(certChain) {
      DevToolsAPI.sendMessageToEmbedder('showCertificateViewer', [JSON.stringify(certChain)], null);
    }

    /**
     * Only needed to run Lighthouse on old devtools.
     * @override
     * @param {function()} callback
     */
    reattach(callback) {
      DevToolsAPI.sendMessageToEmbedder('reattach', [], callback);
    }

    /**
     * @override
     */
    readyForTest() {
      DevToolsAPI.sendMessageToEmbedder('readyForTest', [], null);
    }

    /**
     * @override
     */
    connectionReady() {
      DevToolsAPI.sendMessageToEmbedder('connectionReady', [], null);
    }

    /**
     * @override
     * @param {boolean} value
     */
    setOpenNewWindowForPopups(value) {
      DevToolsAPI.sendMessageToEmbedder('setOpenNewWindowForPopups', [value], null);
    }

    /**
     * @override
     * @param {!Adb.Config} config
     */
    setDevicesDiscoveryConfig(config) {
      DevToolsAPI.sendMessageToEmbedder(
          'setDevicesDiscoveryConfig',
          [
            config.discoverUsbDevices, config.portForwardingEnabled, JSON.stringify(config.portForwardingConfig),
            config.networkDiscoveryEnabled, JSON.stringify(config.networkDiscoveryConfig)
          ],
          null);
    }

    /**
     * @override
     * @param {boolean} enabled
     */
    setDevicesUpdatesEnabled(enabled) {
      DevToolsAPI.sendMessageToEmbedder('setDevicesUpdatesEnabled', [enabled], null);
    }

    /**
     * @override
     * @param {string} pageId
     * @param {string} action
     */
    performActionOnRemotePage(pageId, action) {
      DevToolsAPI.sendMessageToEmbedder('performActionOnRemotePage', [pageId, action], null);
    }

    /**
     * @override
     * @param {string} browserId
     * @param {string} url
     */
    openRemotePage(browserId, url) {
      DevToolsAPI.sendMessageToEmbedder('openRemotePage', [browserId, url], null);
    }

    /**
     * @override
     */
    openNodeFrontend() {
      DevToolsAPI.sendMessageToEmbedder('openNodeFrontend', [], null);
    }

    /**
     * @override
     * @param {number} x
     * @param {number} y
     * @param {!Array.<!InspectorFrontendHostAPI.ContextMenuDescriptor>} items
     * @param {!Document} document
     */
    showContextMenuAtPoint(x, y, items, document) {
      DevToolsHost.showContextMenuAtPoint(x, y, items, document);
    }

    /**
     * @override
     * @return {boolean}
     */
    isHostedMode() {
      return DevToolsHost.isHostedMode();
    }

    /**
     * @override
     * @param {function(!ExtensionDescriptor)} callback
     */
    setAddExtensionCallback(callback) {
      DevToolsAPI.setAddExtensionCallback(callback);
    }

    // Backward-compatible methods below this line --------------------------------------------

    /**
     * Support for legacy front-ends (<M65).
     * @return {boolean}
     */
    isUnderTest() {
      return false;
    }

    /**
     * Support for legacy front-ends (<M50).
     * @param {string} message
     */
    sendFrontendAPINotification(message) {
    }

    /**
     * Support for legacy front-ends (<M41).
     * @return {string}
     */
    port() {
      return 'unknown';
    }

    /**
     * Support for legacy front-ends (<M38).
     * @param {number} zoomFactor
     */
    setZoomFactor(zoomFactor) {
    }

    /**
     * Support for legacy front-ends (<M34).
     */
    sendMessageToEmbedder() {
    }

    /**
     * Support for legacy front-ends (<M34).
     * @param {string} dockSide
     */
    requestSetDockSide(dockSide) {
      DevToolsAPI.sendMessageToEmbedder('setIsDocked', [dockSide !== 'undocked'], null);
    }

    /**
     * Support for legacy front-ends (<M34).
     * @return {boolean}
     */
    supportsFileSystems() {
      return true;
    }

    /**
     * Support for legacy front-ends (<M44).
     * @param {number} actionCode
     */
    recordActionTaken(actionCode) {
      this.recordEnumeratedHistogram('DevTools.ActionTaken', actionCode, 100);
    }

    /**
     * Support for legacy front-ends (<M44).
     * @param {number} panelCode
     */
    recordPanelShown(panelCode) {
      this.recordEnumeratedHistogram('DevTools.PanelShown', panelCode, 20);
    }
  };

  window.InspectorFrontendHost = new InspectorFrontendHostImpl();

  // DevToolsApp ---------------------------------------------------------------

  function installObjectObserve() {
    /** @type {!Array<string>} */
    const properties = [
      'advancedSearchConfig',
      'auditsPanelSplitViewState',
      'auditsSidebarWidth',
      'blockedURLs',
      'breakpoints',
      'cacheDisabled',
      'colorFormat',
      'consoleHistory',
      'consoleTimestampsEnabled',
      'cpuProfilerView',
      'cssSourceMapsEnabled',
      'currentDockState',
      'customColorPalette',
      'customDevicePresets',
      'customEmulatedDeviceList',
      'customFormatters',
      'customUserAgent',
      'databaseTableViewVisibleColumns',
      'dataGrid-cookiesTable',
      'dataGrid-DOMStorageItemsView',
      'debuggerSidebarHidden',
      'disableDataSaverInfobar',
      'disablePausedStateOverlay',
      'domBreakpoints',
      'domWordWrap',
      'elementsPanelSplitViewState',
      'elementsSidebarWidth',
      'emulation.deviceHeight',
      'emulation.deviceModeValue',
      'emulation.deviceOrientationOverride',
      'emulation.deviceScale',
      'emulation.deviceScaleFactor',
      'emulation.deviceUA',
      'emulation.deviceWidth',
      'emulation.geolocationOverride',
      'emulation.showDeviceMode',
      'emulation.showRulers',
      'enableAsyncStackTraces',
      'eventListenerBreakpoints',
      'fileMappingEntries',
      'fileSystemMapping',
      'FileSystemViewSidebarWidth',
      'fileSystemViewSplitViewState',
      'filterBar-consoleView',
      'filterBar-networkPanel',
      'filterBar-promisePane',
      'filterBar-timelinePanel',
      'frameViewerHideChromeWindow',
      'heapSnapshotRetainersViewSize',
      'heapSnapshotSplitViewState',
      'hideCollectedPromises',
      'hideNetworkMessages',
      'highlightNodeOnHoverInOverlay',
      'highResolutionCpuProfiling',
      'inlineVariableValues',
      'Inspector.drawerSplitView',
      'Inspector.drawerSplitViewState',
      'InspectorView.panelOrder',
      'InspectorView.screencastSplitView',
      'InspectorView.screencastSplitViewState',
      'InspectorView.splitView',
      'InspectorView.splitViewState',
      'javaScriptDisabled',
      'jsSourceMapsEnabled',
      'lastActivePanel',
      'lastDockState',
      'lastSelectedSourcesSidebarPaneTab',
      'lastSnippetEvaluationIndex',
      'layerDetailsSplitView',
      'layerDetailsSplitViewState',
      'layersPanelSplitViewState',
      'layersShowInternalLayers',
      'layersSidebarWidth',
      'messageLevelFilters',
      'messageURLFilters',
      'monitoringXHREnabled',
      'navigatorGroupByFolder',
      'navigatorHidden',
      'networkColorCodeResourceTypes',
      'networkConditions',
      'networkConditionsCustomProfiles',
      'networkHideDataURL',
      'networkLogColumnsVisibility',
      'networkLogLargeRows',
      'networkLogShowOverview',
      'networkPanelSplitViewState',
      'networkRecordFilmStripSetting',
      'networkResourceTypeFilters',
      'networkShowPrimaryLoadWaterfall',
      'networkSidebarWidth',
      'openLinkHandler',
      'pauseOnCaughtException',
      'pauseOnExceptionEnabled',
      'preserveConsoleLog',
      'prettyPrintInfobarDisabled',
      'previouslyViewedFiles',
      'profilesPanelSplitViewState',
      'profilesSidebarWidth',
      'promiseStatusFilters',
      'recordAllocationStacks',
      'requestHeaderFilterSetting',
      'request-info-formData-category-expanded',
      'request-info-general-category-expanded',
      'request-info-queryString-category-expanded',
      'request-info-requestHeaders-category-expanded',
      'request-info-requestPayload-category-expanded',
      'request-info-responseHeaders-category-expanded',
      'resources',
      'resourcesLastSelectedItem',
      'resourcesPanelSplitViewState',
      'resourcesSidebarWidth',
      'resourceViewTab',
      'savedURLs',
      'screencastEnabled',
      'scriptsPanelNavigatorSidebarWidth',
      'searchInContentScripts',
      'selectedAuditCategories',
      'selectedColorPalette',
      'selectedProfileType',
      'shortcutPanelSwitch',
      'showAdvancedHeapSnapshotProperties',
      'showEventListenersForAncestors',
      'showFrameowkrListeners',
      'showHeaSnapshotObjectsHiddenProperties',
      'showInheritedComputedStyleProperties',
      'showMediaQueryInspector',
      'showNativeFunctionsInJSProfile',
      'showUAShadowDOM',
      'showWhitespacesInEditor',
      'sidebarPosition',
      'skipContentScripts',
      'skipStackFramesPattern',
      'sourceMapInfobarDisabled',
      'sourcesPanelDebuggerSidebarSplitViewState',
      'sourcesPanelNavigatorSplitViewState',
      'sourcesPanelSplitSidebarRatio',
      'sourcesPanelSplitViewState',
      'sourcesSidebarWidth',
      'standardEmulatedDeviceList',
      'StylesPaneSplitRatio',
      'stylesPaneSplitViewState',
      'textEditorAutocompletion',
      'textEditorAutoDetectIndent',
      'textEditorBracketMatching',
      'textEditorIndent',
      'textEditorTabMovesFocus',
      'timelineCaptureFilmStrip',
      'timelineCaptureLayersAndPictures',
      'timelineCaptureMemory',
      'timelineCaptureNetwork',
      'timeline-details',
      'timelineEnableJSSampling',
      'timelineOverviewMode',
      'timelinePanelDetailsSplitViewState',
      'timelinePanelRecorsSplitViewState',
      'timelinePanelTimelineStackSplitViewState',
      'timelinePerspective',
      'timeline-split',
      'timelineTreeGroupBy',
      'timeline-view',
      'timelineViewMode',
      'uiTheme',
      'watchExpressions',
      'WebInspector.Drawer.lastSelectedView',
      'WebInspector.Drawer.showOnLoad',
      'workspaceExcludedFolders',
      'workspaceFolderExcludePattern',
      'workspaceInfobarDisabled',
      'workspaceMappingInfobarDisabled',
      'xhrBreakpoints'
    ];

    /**
     * @this {!{_storage: Object, _name: string}}
     */
    function settingRemove() {
      this._storage[this._name] = undefined;
    }

    /**
     * @param {!Object} object
     * @param {function(!Array<!{name: string}>)} observer
     */
    function objectObserve(object, observer) {
      if (window['WebInspector']) {
        const settingPrototype = /** @type {!Object} */ (window['WebInspector']['Setting']['prototype']);
        if (typeof settingPrototype['remove'] === 'function') {
          settingPrototype['remove'] = settingRemove;
        }
      }
      /** @type {!Set<string>} */
      const changedProperties = new Set();
      let scheduled = false;

      function scheduleObserver() {
        if (scheduled) {
          return;
        }
        scheduled = true;
        setImmediate(callObserver);
      }

      function callObserver() {
        scheduled = false;
        const changes = /** @type {!Array<!{name: string}>} */ ([]);
        changedProperties.forEach(function(name) {
          changes.push({name: name});
        });
        changedProperties.clear();
        observer.call(null, changes);
      }

      /** @type {!Map<string, *>} */
      const storage = new Map();

      /**
       * @param {string} property
       */
      function defineProperty(property) {
        if (property in object) {
          storage.set(property, object[property]);
          delete object[property];
        }

        Object.defineProperty(object, property, {
          /**
           * @return {*}
           */
          get: function() {
            return storage.get(property);
          },

          /**
           * @param {*} value
           */
          set: function(value) {
            storage.set(property, value);
            changedProperties.add(property);
            scheduleObserver();
          }
        });
      }

      for (let i = 0; i < properties.length; ++i) {
        defineProperty(properties[i]);
      }
    }

    window.Object.observe = objectObserve;
  }

  /** @type {!Map<number, string>} */
  const staticKeyIdentifiers = new Map([
    [0x12, 'Alt'],
    [0x11, 'Control'],
    [0x10, 'Shift'],
    [0x14, 'CapsLock'],
    [0x5b, 'Win'],
    [0x5c, 'Win'],
    [0x0c, 'Clear'],
    [0x28, 'Down'],
    [0x23, 'End'],
    [0x0a, 'Enter'],
    [0x0d, 'Enter'],
    [0x2b, 'Execute'],
    [0x70, 'F1'],
    [0x71, 'F2'],
    [0x72, 'F3'],
    [0x73, 'F4'],
    [0x74, 'F5'],
    [0x75, 'F6'],
    [0x76, 'F7'],
    [0x77, 'F8'],
    [0x78, 'F9'],
    [0x79, 'F10'],
    [0x7a, 'F11'],
    [0x7b, 'F12'],
    [0x7c, 'F13'],
    [0x7d, 'F14'],
    [0x7e, 'F15'],
    [0x7f, 'F16'],
    [0x80, 'F17'],
    [0x81, 'F18'],
    [0x82, 'F19'],
    [0x83, 'F20'],
    [0x84, 'F21'],
    [0x85, 'F22'],
    [0x86, 'F23'],
    [0x87, 'F24'],
    [0x2f, 'Help'],
    [0x24, 'Home'],
    [0x2d, 'Insert'],
    [0x25, 'Left'],
    [0x22, 'PageDown'],
    [0x21, 'PageUp'],
    [0x13, 'Pause'],
    [0x2c, 'PrintScreen'],
    [0x27, 'Right'],
    [0x91, 'Scroll'],
    [0x29, 'Select'],
    [0x26, 'Up'],
    [0x2e, 'U+007F'],  // Standard says that DEL becomes U+007F.
    [0xb0, 'MediaNextTrack'],
    [0xb1, 'MediaPreviousTrack'],
    [0xb2, 'MediaStop'],
    [0xb3, 'MediaPlayPause'],
    [0xad, 'VolumeMute'],
    [0xae, 'VolumeDown'],
    [0xaf, 'VolumeUp'],
  ]);

  /**
   * @param {number} keyCode
   * @return {string}
   */
  function keyCodeToKeyIdentifier(keyCode) {
    let result = staticKeyIdentifiers.get(keyCode);
    if (result !== undefined) {
      return result;
    }
    result = 'U+';
    const hexString = keyCode.toString(16).toUpperCase();
    for (let i = hexString.length; i < 4; ++i) {
      result += '0';
    }
    result += hexString;
    return result;
  }

  function installBackwardsCompatibility() {
    const majorVersion = getRemoteMajorVersion();
    if (!majorVersion) {
      return;
    }

    /** @type {!Array<string>} */
    const styleRules = [];
    // Shadow DOM V0 polyfill
    if (majorVersion <= 73 && !Element.prototype.createShadowRoot) {
      Element.prototype.createShadowRoot = function() {
        try {
          return this.attachShadow({mode: 'open'});
        } catch (e) {
          // some elements we use to add shadow roots can no
          // longer have shadow roots.
          const fakeShadowHost = document.createElement('span');
          this.appendChild(fakeShadowHost);
          fakeShadowHost.className = 'fake-shadow-host';
          return fakeShadowHost.createShadowRoot();
        }
      };

      const origAdd = DOMTokenList.prototype.add;
      DOMTokenList.prototype.add = function(...tokens) {
        if (tokens[0].startsWith('insertion-point') || tokens[0].startsWith('tabbed-pane-header')) {
          this._myElement.slot = '.' + tokens[0];
        }
        return origAdd.apply(this, tokens);
      };

      const origCreateElement = Document.prototype.createElement;
      Document.prototype.createElement = function(tagName, ...rest) {
        if (tagName === 'content') {
          tagName = 'slot';
        }
        const element = origCreateElement.call(this, tagName, ...rest);
        element.classList._myElement = element;
        return element;
      };

      Object.defineProperty(HTMLSlotElement.prototype, 'select', {
        async set(selector) {
          this.name = selector;
        }
      });

      function overrideCreateElementWithClass() {
        window.removeEventListener('DOMContentLoaded', overrideCreateElementWithClass);

        const origCreateElementWithClass = Document.prototype.createElementWithClass;
        Document.prototype.createElementWithClass = function(tagName, className, ...rest) {
          if (tagName !== 'button' || (className !== 'soft-dropdown' && className !== 'dropdown-button')) {
            return origCreateElementWithClass.call(this, tagName, className, ...rest);
          }
          const element = origCreateElementWithClass.call(this, 'div', className, ...rest);
          element.tabIndex = 0;
          element.role = 'button';
          return element;
        };
      }

      // Document.prototype.createElementWithClass is a DevTools method, so we
      // need to wait for DOMContentLoaded in order to override it.
      if (window.document.head &&
          (window.document.readyState === 'complete' || window.document.readyState === 'interactive')) {
        overrideCreateElementWithClass();
      } else {
        window.addEventListener('DOMContentLoaded', overrideCreateElementWithClass);
      }
    }

    // Custom Elements V0 polyfill
    if (majorVersion <= 73 && !Document.prototype.hasOwnProperty('registerElement')) {
      const fakeRegistry = new Map();
      Document.prototype.registerElement = function(typeExtension, options) {
        const {prototype, extends: localName} = options;
        const document = this;
        const callback = function() {
          const element = document.createElement(localName || typeExtension);
          const skip = new Set(['constructor', '__proto__']);
          for (const key of Object.keys(Object.getOwnPropertyDescriptors(prototype.__proto__ || {}))) {
            if (skip.has(key)) {
              continue;
            }
            element[key] = prototype[key];
          }
          element.setAttribute('is', typeExtension);
          if (element['createdCallback']) {
            element['createdCallback']();
          }
          return element;
        };
        fakeRegistry.set(typeExtension, callback);
        return callback;
      };

      const origCreateElement = Document.prototype.createElement;
      Document.prototype.createElement = function(tagName, fakeCustomElementType) {
        const fakeConstructor = fakeRegistry.get(fakeCustomElementType);
        if (fakeConstructor) {
          return fakeConstructor();
        }
        return origCreateElement.call(this, tagName, fakeCustomElementType);
      };

      // DevTools front-ends mistakenly assume that
      //   classList.toggle('a', undefined) works as
      //   classList.toggle('a', false) rather than as
      //   classList.toggle('a');
      const originalDOMTokenListToggle = DOMTokenList.prototype.toggle;
      DOMTokenList.prototype.toggle = function(token, force) {
        if (arguments.length === 1) {
          force = !this.contains(token);
        }
        return originalDOMTokenListToggle.call(this, token, !!force);
      };
    }

    if (majorVersion <= 66) {
      /** @type {(!function(number, number):Element|undefined)} */
      ShadowRoot.prototype.__originalShadowRootElementFromPoint;

      if (!ShadowRoot.prototype.__originalShadowRootElementFromPoint) {
        ShadowRoot.prototype.__originalShadowRootElementFromPoint = ShadowRoot.prototype.elementFromPoint;
        /**
         *  @param {number} x
         *  @param {number} y
         *  @return {Element}
         */
        ShadowRoot.prototype.elementFromPoint = function(x, y) {
          const originalResult = ShadowRoot.prototype.__originalShadowRootElementFromPoint.apply(this, arguments);
          if (this.host && originalResult === this.host) {
            return null;
          }
          return originalResult;
        };
      }
    }

    if (majorVersion <= 53) {
      Object.defineProperty(window.KeyboardEvent.prototype, 'keyIdentifier', {
        /**
         * @return {string}
         * @this {KeyboardEvent}
         */
        get: function() {
          return keyCodeToKeyIdentifier(this.keyCode);
        }
      });
    }

    if (majorVersion <= 50) {
      installObjectObserve();
    }

    if (majorVersion <= 45) {
      /**
       * @param {string} property
       * @return {!CSSValue|null}
       * @this {CSSStyleDeclaration}
       */
      function getValue(property) {
        // Note that |property| comes from another context, so we can't use === here.
        // eslint-disable-next-line eqeqeq
        if (property == 'padding-left') {
          return /** @type {!CSSValue} */ ({
            /**
             * @return {number}
             * @this {!{__paddingLeft: number}}
             */
            getFloatValue: function() {
              return this.__paddingLeft;
            },
            __paddingLeft: parseFloat(this.paddingLeft)
          });
        }
        throw new Error('getPropertyCSSValue is undefined');
      }

      window.CSSStyleDeclaration.prototype.getPropertyCSSValue = getValue;

      function CSSPrimitiveValue() {
      }
      CSSPrimitiveValue.CSS_PX = 5;
      window.CSSPrimitiveValue = CSSPrimitiveValue;
    }

    if (majorVersion <= 45) {
      styleRules.push('* { min-width: 0; min-height: 0; }');
    }

    if (majorVersion <= 51) {
      // Support for quirky border-image behavior (<M51), see:
      // https://bugs.chromium.org/p/chromium/issues/detail?id=559258
      styleRules.push('.cm-breakpoint .CodeMirror-linenumber { border-style: solid !important; }');
      styleRules.push(
          '.cm-breakpoint.cm-breakpoint-conditional .CodeMirror-linenumber { border-style: solid !important; }');
    }
    if (majorVersion <= 71) {
      styleRules.push(
          '.coverage-toolbar-container, .animation-timeline-toolbar-container, .computed-properties { flex-basis: auto; }');
    }

    if (majorVersion <= 50) {
      Event.prototype.deepPath = undefined;
    }

    if (majorVersion <= 54) {
      window.FileError = /** @type {!function (new: FileError) : ?} */ ({
        NOT_FOUND_ERR: DOMException.NOT_FOUND_ERR,
        ABORT_ERR: DOMException.ABORT_ERR,
        INVALID_MODIFICATION_ERR: DOMException.INVALID_MODIFICATION_ERR,
        NOT_READABLE_ERR: 0  // No matching DOMException, so code will be 0.
      });
    }

    installExtraStyleRules(styleRules);
  }

  /**
   * @return {?number}
   */
  function getRemoteMajorVersion() {
    try {
      const remoteVersion = new URLSearchParams(window.location.search).get('remoteVersion');
      if (!remoteVersion) {
        return null;
      }
      const majorVersion = parseInt(remoteVersion.split('.')[0], 10);
      return majorVersion;
    } catch (e) {
      return null;
    }
  }

  /**
   * @param {!Array<string>} styleRules
   */
  function installExtraStyleRules(styleRules) {
    if (!styleRules.length) {
      return;
    }
    const styleText = styleRules.join('\n');
    document.head.appendChild(createStyleElement(styleText));

    const origCreateShadowRoot = HTMLElement.prototype.createShadowRoot;
    HTMLElement.prototype.createShadowRoot = function(...args) {
      const shadowRoot = origCreateShadowRoot.call(this, ...args);
      shadowRoot.appendChild(createStyleElement(styleText));
      return shadowRoot;
    };
  }

  /**
   * @param {string} styleText
   * @return {!Element}
   */
  function createStyleElement(styleText) {
    const style = document.createElement('style');
    style.textContent = styleText;
    return style;
  }

  installBackwardsCompatibility();

})(window);
