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

import * as Platform from '../../core/platform/platform.js';
import type * as PublicAPI from '../../../extension-api/ExtensionAPI'; // eslint-disable-line rulesdir/es_modules_import
import type * as HAR from '../har/har.js';

/* eslint-disable @typescript-eslint/naming-convention,@typescript-eslint/no-non-null-assertion */
export namespace PrivateAPI {
  export namespace Panels {
    export const enum SearchAction {
      CancelSearch = 'cancelSearch',
      PerformSearch = 'performSearch',
      NextSearchResult = 'nextSearchResult',
      PreviousSearchResult = 'previousSearchResult',
    }
  }

  export const enum Events {
    ButtonClicked = 'button-clicked-',
    PanelObjectSelected = 'panel-objectSelected-',
    InspectedURLChanged = 'inspected-url-changed',
    NetworkRequestFinished = 'network-request-finished',
    OpenResource = 'open-resource',
    PanelSearch = 'panel-search-',
    RecordingStarted = 'trace-recording-started-',
    RecordingStopped = 'trace-recording-stopped-',
    ResourceAdded = 'resource-added',
    ResourceContentCommitted = 'resource-content-committed',
    ViewShown = 'view-shown-',
    ViewHidden = 'view-hidden,',
    ThemeChange = 'host-theme-change',
  }

  export const enum Commands {
    AddRequestHeaders = 'addRequestHeaders',
    AddTraceProvider = 'addTraceProvider',
    ApplyStyleSheet = 'applyStyleSheet',
    CompleteTraceSession = 'completeTra.eSession',
    CreatePanel = 'createPanel',
    CreateSidebarPane = 'createSidebarPane',
    CreateToolbarButton = 'createToolbarButton',
    EvaluateOnInspectedPage = 'evaluateOnInspectedPage',
    ForwardKeyboardEvent = '_forwardKeyboardEvent',
    GetHAR = 'getHAR',
    GetPageResources = 'getPageResources',
    GetRequestContent = 'getRequestContent',
    GetResourceContent = 'getResourceContent',
    OpenResource = 'openResource',
    Reload = 'Reload',
    Subscribe = 'subscribe',
    SetOpenResourceHandler = 'setOpenResourceHandler',
    SetThemeChangeHandler = 'setThemeChangeHandler',
    SetResourceContent = 'setResourceContent',
    SetSidebarContent = 'setSidebarContent',
    SetSidebarHeight = 'setSidebarHeight',
    SetSidebarPage = 'setSidebarPage',
    ShowPanel = 'showPanel',
    Unsubscribe = 'unsubscribe',
    UpdateButton = 'updateButton',
    RegisterLanguageExtensionPlugin = 'registerLanguageExtensionPlugin',
    GetWasmLinearMemory = 'getWasmLinearMemory',
    GetWasmLocal = 'getWasmLocal',
    GetWasmGlobal = 'getWasmGlobal',
    GetWasmOp = 'getWasmOp',
    RegisterRecorderExtensionPlugin = 'registerRecorderExtensionPlugin',
  }

  export const enum LanguageExtensionPluginCommands {
    AddRawModule = 'addRawModule',
    RemoveRawModule = 'removeRawModule',
    SourceLocationToRawLocation = 'sourceLocationToRawLocation',
    RawLocationToSourceLocation = 'rawLocationToSourceLocation',
    GetScopeInfo = 'getScopeInfo',
    ListVariablesInScope = 'listVariablesInScope',
    GetTypeInfo = 'getTypeInfo',
    GetFormatter = 'getFormatter',
    GetInspectableAddress = 'getInspectableAddress',
    GetFunctionInfo = 'getFunctionInfo',
    GetInlinedFunctionRanges = 'getInlinedFunctionRanges',
    GetInlinedCalleesRanges = 'getInlinedCalleesRanges',
    GetMappedLines = 'getMappedLines',
    FormatValue = 'formatValue',
    GetProperties = 'getProperties',
    ReleaseObject = 'releaseObject',
  }

  export const enum LanguageExtensionPluginEvents {
    UnregisteredLanguageExtensionPlugin = 'unregisteredLanguageExtensionPlugin',
  }

  export const enum RecorderExtensionPluginCommands {
    Stringify = 'stringify',
    StringifyStep = 'stringifyStep',
  }

  export const enum RecorderExtensionPluginEvents {
    UnregisteredRecorderExtensionPlugin = 'unregisteredRecorderExtensionPlugin',
  }

  export interface EvaluateOptions {
    frameURL?: string;
    useContentScriptContext?: boolean;
    scriptExecutionContext?: string;
  }

  type RegisterLanguageExtensionPluginRequest = {
    command: Commands.RegisterLanguageExtensionPlugin,
    pluginName: string,
    port: MessagePort,
    supportedScriptTypes: PublicAPI.Chrome.DevTools.SupportedScriptTypes,
  };
  type RegisterRecorderExtensionPluginRequest = {
    command: Commands.RegisterRecorderExtensionPlugin,
    pluginName: string,
    mediaType: string,
    port: MessagePort,
  };
  type SubscribeRequest = {command: Commands.Subscribe, type: string};
  type UnsubscribeRequest = {command: Commands.Unsubscribe, type: string};
  type AddRequestHeadersRequest = {
    command: Commands.AddRequestHeaders,
    extensionId: string,
    headers: {[key: string]: string},
  };
  type ApplyStyleSheetRequest = {command: Commands.ApplyStyleSheet, styleSheet: string};
  type CreatePanelRequest = {command: Commands.CreatePanel, id: string, title: string, page: string};
  type ShowPanelRequest = {command: Commands.ShowPanel, id: string};
  type CreateToolbarButtonRequest = {
    command: Commands.CreateToolbarButton,
    id: string,
    icon: string,
    panel: string,
    tooltip?: string,
    disabled?: boolean,
  };
  type UpdateButtonRequest =
      {command: Commands.UpdateButton, id: string, icon?: string, tooltip?: string, disabled?: boolean};
  type CompleteTraceSessionRequest =
      {command: Commands.CompleteTraceSession, id: string, url: Platform.DevToolsPath.UrlString, timeOffset: number};
  type CreateSidebarPaneRequest = {command: Commands.CreateSidebarPane, id: string, panel: string, title: string};
  type SetSidebarHeightRequest = {command: Commands.SetSidebarHeight, id: string, height: string};
  type SetSidebarContentRequest = {
    command: Commands.SetSidebarContent,
    id: string,
    evaluateOnPage?: boolean, expression: string,
    rootTitle?: string,
    evaluateOptions?: EvaluateOptions,
  };
  type SetSidebarPageRequest = {command: Commands.SetSidebarPage, id: string, page: string};
  type OpenResourceRequest =
      {command: Commands.OpenResource, url: Platform.DevToolsPath.UrlString, lineNumber: number, columnNumber: number};
  type SetOpenResourceHandlerRequest = {command: Commands.SetOpenResourceHandler, handlerPresent: boolean};
  type SetThemeChangeHandlerRequest = {command: Commands.SetThemeChangeHandler, handlerPresent: boolean};
  type ReloadRequest = {
    command: Commands.Reload,
    options: null|{
      userAgent?: string,
      injectedScript?: string,
      ignoreCache?: boolean,
    },
  };
  type EvaluateOnInspectedPageRequest = {
    command: Commands.EvaluateOnInspectedPage,
    expression: string,
    evaluateOptions?: EvaluateOptions,
  };
  type GetRequestContentRequest = {command: Commands.GetRequestContent, id: number};
  type GetResourceContentRequest = {command: Commands.GetResourceContent, url: string};
  type SetResourceContentRequest =
      {command: Commands.SetResourceContent, url: string, content: string, commit: boolean};
  type AddTraceProviderRequest =
      {command: Commands.AddTraceProvider, id: string, categoryName: string, categoryTooltip: string};
  type ForwardKeyboardEventRequest = {
    command: Commands.ForwardKeyboardEvent,
    entries: Array<KeyboardEventInit&{eventType: string}>,
  };
  type GetHARRequest = {command: Commands.GetHAR};
  type GetPageResourcesRequest = {command: Commands.GetPageResources};
  type GetWasmLinearMemoryRequest = {
    command: Commands.GetWasmLinearMemory,
    offset: number,
    length: number,
    stopId: unknown,
  };
  type GetWasmLocalRequest = {
    command: Commands.GetWasmLocal,
    local: number,
    stopId: unknown,
  };
  type GetWasmGlobalRequest = {
    command: Commands.GetWasmGlobal,
    global: number,
    stopId: unknown,
  };
  type GetWasmOpRequest = {command: Commands.GetWasmOp, op: number, stopId: unknown};

  export type ServerRequests = RegisterRecorderExtensionPluginRequest|RegisterLanguageExtensionPluginRequest|
      SubscribeRequest|UnsubscribeRequest|AddRequestHeadersRequest|ApplyStyleSheetRequest|CreatePanelRequest|
      ShowPanelRequest|CreateToolbarButtonRequest|UpdateButtonRequest|CompleteTraceSessionRequest|
      CreateSidebarPaneRequest|SetSidebarHeightRequest|SetSidebarContentRequest|SetSidebarPageRequest|
      OpenResourceRequest|SetOpenResourceHandlerRequest|SetThemeChangeHandlerRequest|ReloadRequest|
      EvaluateOnInspectedPageRequest|GetRequestContentRequest|GetResourceContentRequest|SetResourceContentRequest|
      AddTraceProviderRequest|ForwardKeyboardEventRequest|GetHARRequest|GetPageResourcesRequest|
      GetWasmLinearMemoryRequest|GetWasmLocalRequest|GetWasmGlobalRequest|GetWasmOpRequest;
  export type ExtensionServerRequestMessage = PrivateAPI.ServerRequests&{requestId?: number};

  type AddRawModuleRequest = {
    method: LanguageExtensionPluginCommands.AddRawModule,
    parameters: {rawModuleId: string, symbolsURL: string|undefined, rawModule: PublicAPI.Chrome.DevTools.RawModule},
  };
  type SourceLocationToRawLocationRequest = {
    method: LanguageExtensionPluginCommands.SourceLocationToRawLocation,
    parameters: {sourceLocation: PublicAPI.Chrome.DevTools.SourceLocation},
  };
  type RawLocationToSourceLocationRequest = {
    method: LanguageExtensionPluginCommands.RawLocationToSourceLocation,
    parameters: {rawLocation: PublicAPI.Chrome.DevTools.RawLocation},
  };
  type GetScopeInfoRequest = {method: LanguageExtensionPluginCommands.GetScopeInfo, parameters: {type: string}};
  type ListVariablesInScopeRequest = {
    method: LanguageExtensionPluginCommands.ListVariablesInScope,
    parameters: {rawLocation: PublicAPI.Chrome.DevTools.RawLocation},
  };
  type RemoveRawModuleRequest = {
    method: LanguageExtensionPluginCommands.RemoveRawModule,
    parameters: {rawModuleId: string},
  };
  type GetTypeInfoRequest = {
    method: LanguageExtensionPluginCommands.GetTypeInfo,
    parameters: {expression: string, context: PublicAPI.Chrome.DevTools.RawLocation},
  };
  type GetFormatterRequest = {
    method: LanguageExtensionPluginCommands.GetFormatter,
    parameters: {
      expressionOrField: string|{
        base: PublicAPI.Chrome.DevTools.EvalBase,
        field: Array<PublicAPI.Chrome.DevTools.FieldInfo>,
      },
      context: PublicAPI.Chrome.DevTools.RawLocation,
    },
  };
  type GetInspectableAddressRequest = {
    method: LanguageExtensionPluginCommands.GetInspectableAddress,
    parameters: {
      field: {
        base: PublicAPI.Chrome.DevTools.EvalBase,
        field: Array<PublicAPI.Chrome.DevTools.FieldInfo>,
      },
    },
  };
  type GetFunctionInfoRequest = {
    method: LanguageExtensionPluginCommands.GetFunctionInfo,
    parameters: {rawLocation: PublicAPI.Chrome.DevTools.RawLocation},
  };
  type GetInlinedFunctionRangesRequest = {
    method: LanguageExtensionPluginCommands.GetInlinedFunctionRanges,
    parameters: {rawLocation: PublicAPI.Chrome.DevTools.RawLocation},
  };
  type GetInlinedCalleesRangesRequest = {
    method: LanguageExtensionPluginCommands.GetInlinedCalleesRanges,
    parameters: {rawLocation: PublicAPI.Chrome.DevTools.RawLocation},
  };
  type GetMappedLinesRequest = {
    method: LanguageExtensionPluginCommands.GetMappedLines,
    parameters: {rawModuleId: string, sourceFileURL: string},
  };
  type FormatValueRequest = {
    method: LanguageExtensionPluginCommands.FormatValue,
    parameters: {expression: string, context: PublicAPI.Chrome.DevTools.RawLocation, stopId: number},
  };
  type GetPropertiesRequest = {
    method: LanguageExtensionPluginCommands.GetProperties,
    parameters: {objectId: PublicAPI.Chrome.DevTools.RemoteObjectId},
  };
  type ReleaseObjectRequest = {
    method: LanguageExtensionPluginCommands.ReleaseObject,
    parameters: {objectId: PublicAPI.Chrome.DevTools.RemoteObjectId},
  };

  export type LanguageExtensionRequests =
      AddRawModuleRequest|SourceLocationToRawLocationRequest|RawLocationToSourceLocationRequest|GetScopeInfoRequest|
      ListVariablesInScopeRequest|RemoveRawModuleRequest|GetTypeInfoRequest|GetFormatterRequest|
      GetInspectableAddressRequest|GetFunctionInfoRequest|GetInlinedFunctionRangesRequest|
      GetInlinedCalleesRangesRequest|GetMappedLinesRequest|FormatValueRequest|GetPropertiesRequest|ReleaseObjectRequest;

  type StringifyRequest = {
    method: RecorderExtensionPluginCommands.Stringify,
    parameters: {recording: Record<string, unknown>},
  };

  type StringifyStepRequest = {
    method: RecorderExtensionPluginCommands.StringifyStep,
    parameters: {step: Record<string, unknown>},
  };

  export type RecorderExtensionRequests = StringifyRequest|StringifyStepRequest;
}

declare global {
  interface Window {
    injectedExtensionAPI:
        (extensionInfo: ExtensionDescriptor, inspectedTabId: string, themeName: string, keysToForward: number[],
         testHook:
             (extensionServer: APIImpl.ExtensionServerClient, extensionAPI: APIImpl.InspectorExtensionAPI) => unknown,
         injectedScriptId: number, targetWindow?: Window) => void;
    buildExtensionAPIInjectedScript(
        extensionInfo: ExtensionDescriptor, inspectedTabId: string, themeName: string, keysToForward: number[],
        testHook: undefined|((extensionServer: unknown, extensionAPI: unknown) => unknown)): string;
    chrome: PublicAPI.Chrome.DevTools.Chrome;
    webInspector?: APIImpl.InspectorExtensionAPI;
  }
}

export type ExtensionDescriptor = {
  startPage: string,
  name: string,
  exposeExperimentalAPIs: boolean,
  exposeWebInspectorNamespace?: boolean,
};

namespace APIImpl {
  export interface InspectorExtensionAPI {
    languageServices: PublicAPI.Chrome.DevTools.LanguageExtensions;
    recorder: PublicAPI.Chrome.DevTools.RecorderExtensions;
    timeline: Timeline;
    network: PublicAPI.Chrome.DevTools.Network;
    panels: PublicAPI.Chrome.DevTools.Panels;
    inspectedWindow: PublicAPI.Chrome.DevTools.InspectedWindow;
  }

  export interface ExtensionServerClient {
    _callbacks: {[key: string]: (response: unknown) => unknown};
    _handlers: {[key: string]: (request: {arguments: unknown[]}) => unknown};
    _lastRequestId: number;
    _lastObjectId: number;
    _port: MessagePort;

    _onCallback(request: unknown): void;
    _onMessage(event: MessageEvent<{command: string, requestId: number, arguments: unknown[]}>): void;
    _registerCallback(callback: (response: unknown) => unknown): number;
    registerHandler(command: string, handler: (request: {arguments: unknown[]}) => unknown): void;
    unregisterHandler(command: string): void;
    hasHandler(command: string): boolean;
    sendRequest<ResponseT>(
        request: PrivateAPI.ServerRequests, callback?: ((response: ResponseT) => unknown), transfers?: unknown[]): void;
    nextObjectId(): string;
  }

  // We cannot use the stronger `unknown` type in place of `any` in the following type definition. The type is used as
  // the right-hand side of `extends` in a few places, which doesn't narrow `unknown`. Without narrowing, overload
  // resolution and meaningful type inference of arguments break, for example.
  // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  export type Callable = (...args: any) => void;

  export interface EventSink<ListenerT extends Callable> extends PublicAPI.Chrome.DevTools.EventSink<ListenerT> {
    _type: string;
    _listeners: ListenerT[];
    _customDispatch: undefined|((this: EventSink<ListenerT>, request: {arguments: unknown[]}) => unknown);

    _fire(..._vararg: Parameters<ListenerT>): void;
    _dispatch(request: {arguments: unknown[]}): void;
  }

  export interface Network extends PublicAPI.Chrome.DevTools.Network {
    addRequestHeaders(headers: {[key: string]: string}): void;
  }

  export interface Request extends PublicAPI.Chrome.DevTools.Request, HAR.Log.EntryDTO {
    _id: number;
  }

  export interface Panels extends PublicAPI.Chrome.DevTools.Panels {
    get SearchAction(): {[key: string]: string};
    applyStyleSheet(styleSheet: string): void;
    setOpenResourceHandler(callback?: (resource: PublicAPI.Chrome.DevTools.Resource, lineNumber: number) => unknown):
        void;
    setThemeChangeHandler(callback?: (themeName: string) => unknown): void;
  }

  export interface ExtensionView extends PublicAPI.Chrome.DevTools.ExtensionView {
    _id: string|null;
  }

  export interface ExtensionSidebarPane extends ExtensionView, PublicAPI.Chrome.DevTools.ExtensionSidebarPane {
    setExpression(
        expression: string, rootTitle?: string, evaluteOptions?: PrivateAPI.EvaluateOptions,
        callback?: () => unknown): void;
  }

  export interface PanelWithSidebar extends ExtensionView, PublicAPI.Chrome.DevTools.PanelWithSidebar {
    _hostPanelName: string;
  }

  export interface LanguageExtensions extends PublicAPI.Chrome.DevTools.LanguageExtensions {
    _plugins: Map<PublicAPI.Chrome.DevTools.LanguageExtensionPlugin, MessagePort>;
  }

  export interface RecorderExtensions extends PublicAPI.Chrome.DevTools.RecorderExtensions {
    _plugins: Map<PublicAPI.Chrome.DevTools.RecorderExtensionPlugin, MessagePort>;
  }

  export interface ExtensionPanel extends ExtensionView, PublicAPI.Chrome.DevTools.ExtensionPanel {
    show(): void;
  }

  export interface Button extends PublicAPI.Chrome.DevTools.Button {
    _id: string;
  }

  export interface TraceSession {
    _id: string;

    complete(url?: string, timeOffset?: number): void;
  }

  export interface TraceProvider {
    onRecordingStarted: EventSink<(session: TraceSession) => unknown>;
    onRecordingStopped: EventSink<() => unknown>;
  }

  export interface Timeline {
    addTraceProvider(categoryName: string, categoryTooltip: string): TraceProvider;
  }

  export type ResourceData = {url: string, type: string};
  export interface Resource extends PublicAPI.Chrome.DevTools.Resource {
    _type: string;
    _url: string;

    get type(): string;
  }
}

self.injectedExtensionAPI = function(
    extensionInfo: ExtensionDescriptor, inspectedTabId: string, themeName: string, keysToForward: number[],
    testHook: (extensionServer: APIImpl.ExtensionServerClient, extensionAPI: APIImpl.InspectorExtensionAPI) => unknown,
    injectedScriptId: number, targetWindowForTest?: Window): void {
  const keysToForwardSet = new Set<number>(keysToForward);
  const chrome = window.chrome || {};

  const devtools_descriptor = Object.getOwnPropertyDescriptor(chrome, 'devtools');
  if (devtools_descriptor) {
    return;
  }
  let userAction = false;

  // Here and below, all constructors are private to API implementation.
  // For a public type Foo, if internal fields are present, these are on
  // a private FooImpl type, an instance of FooImpl is used in a closure
  // by Foo consutrctor to re-bind publicly exported members to an instance
  // of Foo.

  function EventSinkImpl<ListenerT extends APIImpl.Callable>(
      this: APIImpl.EventSink<ListenerT>, type: string,
      customDispatch?: (this: APIImpl.EventSink<ListenerT>, request: {arguments: unknown[]}) => unknown): void {
    this._type = type;
    this._listeners = [];
    this._customDispatch = customDispatch;
  }

  EventSinkImpl.prototype = {
    addListener: function<ListenerT extends APIImpl.Callable>(this: APIImpl.EventSink<ListenerT>, callback: ListenerT):
        void {
          if (typeof callback !== 'function') {
            throw 'addListener: callback is not a function';
          }
          if (this._listeners.length === 0) {
            extensionServer.sendRequest({command: PrivateAPI.Commands.Subscribe, type: this._type});
          }
          this._listeners.push(callback);
          extensionServer.registerHandler('notify-' + this._type, this._dispatch.bind(this));
        },

    removeListener: function<ListenerT extends APIImpl.Callable>(
        this: APIImpl.EventSink<ListenerT>, callback: ListenerT): void {
      const listeners = this._listeners;

      for (let i = 0; i < listeners.length; ++i) {
        if (listeners[i] === callback) {
          listeners.splice(i, 1);
          break;
        }
      }
      if (this._listeners.length === 0) {
        extensionServer.sendRequest({command: PrivateAPI.Commands.Unsubscribe, type: this._type});
      }
    },

    _fire: function<ListenerT extends APIImpl.Callable>(
        this: APIImpl.EventSink<ListenerT>, ..._vararg: Parameters<ListenerT>): void {
      const listeners = this._listeners.slice();
      for (let i = 0; i < listeners.length; ++i) {
        listeners[i].apply(null, Array.from(arguments));
      }
    },

    _dispatch: function<ListenerT extends APIImpl.Callable>(
        this: APIImpl.EventSink<ListenerT>, request: {arguments: unknown[]}): void {
      if (this._customDispatch) {
        this._customDispatch.call(this, request);
      } else {
        this._fire.apply(this, request.arguments as Parameters<ListenerT>);
      }
    },
  };

  function Constructor<NewT extends APIImpl.Callable>(ctor: NewT): new (...args: Parameters<NewT>) =>
      ThisParameterType<NewT> {
    return ctor as unknown as new (...args: Parameters<NewT>) => ThisParameterType<NewT>;
  }

  function InspectorExtensionAPI(this: APIImpl.InspectorExtensionAPI): void {
    this.inspectedWindow = new (Constructor(InspectedWindow))();
    this.panels = new (Constructor(Panels))();
    this.network = new (Constructor(Network))();
    this.timeline = new (Constructor(Timeline))();
    this.languageServices = new (Constructor(LanguageServicesAPI))();
    this.recorder = new (Constructor(RecorderServicesAPI))();
    defineDeprecatedProperty(this, 'webInspector', 'resources', 'network');
  }

  function Network(this: APIImpl.Network): void {
    function dispatchRequestEvent(
        this: APIImpl.EventSink<(request: PublicAPI.Chrome.DevTools.Request) => unknown>,
        message: {arguments: unknown[]}): void {
      const request = message.arguments[1] as APIImpl.Request & {__proto__: APIImpl.Request};

      request.__proto__ = new (Constructor(Request))(message.arguments[0] as number);
      this._fire(request);
    }

    this.onRequestFinished =
        new (Constructor(EventSink))(PrivateAPI.Events.NetworkRequestFinished, dispatchRequestEvent);
    defineDeprecatedProperty(this, 'network', 'onFinished', 'onRequestFinished');

    this.onNavigated = new (Constructor(EventSink))(PrivateAPI.Events.InspectedURLChanged);
  }

  (Network.prototype as Pick<APIImpl.Network, 'getHAR'|'addRequestHeaders'>) = {
    getHAR: function(this: PublicAPI.Chrome.DevTools.Network, callback?: (harLog: Object) => unknown): void {
      function callbackWrapper(response: unknown): void {
        const result =
            response as ({entries: Array<HAR.Log.EntryDTO&{__proto__?: APIImpl.Request, _requestId?: number}>});
        const entries = (result && result.entries) || [];
        for (let i = 0; i < entries.length; ++i) {
          entries[i].__proto__ = new (Constructor(Request))(entries[i]._requestId as number);
          delete entries[i]._requestId;
        }
        callback && callback(result as Object);
      }
      extensionServer.sendRequest({command: PrivateAPI.Commands.GetHAR}, callback && callbackWrapper);
    },

    addRequestHeaders: function(headers: {[key: string]: string}): void {
      extensionServer.sendRequest(
          {command: PrivateAPI.Commands.AddRequestHeaders, headers: headers, extensionId: window.location.hostname});
    },
  };

  function RequestImpl(this: APIImpl.Request, id: number): void {
    this._id = id;
  }

  (RequestImpl.prototype as Pick<APIImpl.Request, 'getContent'>) = {
    getContent: function(this: APIImpl.Request, callback?: (content: string, encoding: string) => unknown): void {
      function callbackWrapper(response: unknown): void {
        const {content, encoding} = response as {content: string, encoding: string};
        callback && callback(content, encoding);
      }
      extensionServer.sendRequest(
          {command: PrivateAPI.Commands.GetRequestContent, id: this._id}, callback && callbackWrapper);
    },
  };

  function Panels(this: APIImpl.Panels): void {
    const panels: {[key: string]: ElementsPanel|SourcesPanel} = {
      elements: new ElementsPanel(),
      sources: new SourcesPanel(),
    };

    function panelGetter(name: string): ElementsPanel|SourcesPanel {
      return panels[name];
    }
    for (const panel in panels) {
      Object.defineProperty(this, panel, {get: panelGetter.bind(null, panel), enumerable: true});
    }
    this.applyStyleSheet = function(styleSheet: string): void {
      extensionServer.sendRequest({command: PrivateAPI.Commands.ApplyStyleSheet, styleSheet: styleSheet});
    };
  }

  (Panels.prototype as
   Pick<APIImpl.Panels, 'create'|'setOpenResourceHandler'|'openResource'|'SearchAction'|'setThemeChangeHandler'>) = {
    create: function(
        title: string, icon: string, page: string,
        callback: (panel: PublicAPI.Chrome.DevTools.ExtensionPanel) => unknown): void {
      const id = 'extension-panel-' + extensionServer.nextObjectId();
      extensionServer.sendRequest(
          {command: PrivateAPI.Commands.CreatePanel, id, title, page},
          callback && ((): unknown => callback.call(this, new (Constructor(ExtensionPanel))(id))));
    },

    setOpenResourceHandler: function(
        callback: (resource: PublicAPI.Chrome.DevTools.Resource, lineNumber: number) => unknown): void {
      const hadHandler = extensionServer.hasHandler(PrivateAPI.Events.OpenResource);

      function callbackWrapper(message: unknown): void {
        // Allow the panel to show itself when handling the event.
        userAction = true;
        try {
          const {resource, lineNumber} = message as {resource: APIImpl.ResourceData, lineNumber: number};
          callback.call(null, new (Constructor(Resource))(resource), lineNumber);
        } finally {
          userAction = false;
        }
      }

      if (!callback) {
        extensionServer.unregisterHandler(PrivateAPI.Events.OpenResource);
      } else {
        extensionServer.registerHandler(PrivateAPI.Events.OpenResource, callbackWrapper);
      }

      // Only send command if we either removed an existing handler or added handler and had none before.
      if (hadHandler === !callback) {
        extensionServer.sendRequest(
            {command: PrivateAPI.Commands.SetOpenResourceHandler, 'handlerPresent': Boolean(callback)});
      }
    },

    setThemeChangeHandler: function(callback: (themeName: string) => unknown): void {
      const hadHandler = extensionServer.hasHandler(PrivateAPI.Events.ThemeChange);

      function callbackWrapper(message: unknown): void {
        const {themeName} = message as {themeName: string};
        chrome.devtools.panels.themeName = themeName;
        callback.call(null, themeName);
      }

      if (!callback) {
        extensionServer.unregisterHandler(PrivateAPI.Events.ThemeChange);
      } else {
        extensionServer.registerHandler(PrivateAPI.Events.ThemeChange, callbackWrapper);
      }

      // Only send command if we either removed an existing handler or added handler and had none before.
      if (hadHandler === !callback) {
        extensionServer.sendRequest(
            {command: PrivateAPI.Commands.SetThemeChangeHandler, 'handlerPresent': Boolean(callback)});
      }
    },

    openResource: function(
        url: Platform.DevToolsPath.UrlString, lineNumber: number, columnNumber?: number,
        _callback?: (response: unknown) => unknown): void {
      const callbackArg = extractCallbackArgument(arguments);
      // Handle older API:
      const columnNumberArg = typeof columnNumber === 'number' ? columnNumber : 0;
      extensionServer.sendRequest(
          {command: PrivateAPI.Commands.OpenResource, url, lineNumber, columnNumber: columnNumberArg}, callbackArg);
    },

    get SearchAction(): {[key: string]: string} {
      return {
        CancelSearch: PrivateAPI.Panels.SearchAction.CancelSearch,
        PerformSearch: PrivateAPI.Panels.SearchAction.PerformSearch,
        NextSearchResult: PrivateAPI.Panels.SearchAction.NextSearchResult,
        PreviousSearchResult: PrivateAPI.Panels.SearchAction.PreviousSearchResult,
      };
    },
  };

  function ExtensionViewImpl(this: APIImpl.ExtensionView, id: string|null): void {
    this._id = id;

    function dispatchShowEvent(
        this: APIImpl.EventSink<(window?: Window) => unknown>, message: {arguments: unknown[]}): void {
      const frameIndex = message.arguments[0];
      if (typeof frameIndex === 'number') {
        this._fire(window.parent.frames[frameIndex]);
      } else {
        this._fire();
      }
    }

    if (id) {
      this.onShown = new (Constructor(EventSink))(PrivateAPI.Events.ViewShown + id, dispatchShowEvent);

      this.onHidden = new (Constructor(EventSink))(PrivateAPI.Events.ViewHidden + id);
    }
  }

  function PanelWithSidebarImpl(this: APIImpl.PanelWithSidebar, hostPanelName: string): void {
    ExtensionViewImpl.call(this, null);
    this._hostPanelName = hostPanelName;

    this.onSelectionChanged = new (Constructor(EventSink))(PrivateAPI.Events.PanelObjectSelected + hostPanelName);
  }

  (PanelWithSidebarImpl.prototype as Pick<APIImpl.PanelWithSidebar, 'createSidebarPane'>&
   {__proto__: APIImpl.ExtensionView}) = {
    createSidebarPane: function(
        this: APIImpl.PanelWithSidebar, title: string,
        callback?: (pane: PublicAPI.Chrome.DevTools.ExtensionSidebarPane) => unknown): void {
      const id = 'extension-sidebar-' + extensionServer.nextObjectId();
      function callbackWrapper(): void {
        callback && callback(new (Constructor(ExtensionSidebarPane))(id));
      }
      extensionServer.sendRequest(
          {command: PrivateAPI.Commands.CreateSidebarPane, panel: this._hostPanelName, id, title},
          callback && callbackWrapper);
    },

    __proto__: ExtensionViewImpl.prototype,
  };

  function RecorderServicesAPIImpl(this: APIImpl.RecorderExtensions): void {
    this._plugins = new Map();
  }

  (RecorderServicesAPIImpl.prototype as
   Pick<APIImpl.RecorderExtensions, 'registerRecorderExtensionPlugin'|'unregisterRecorderExtensionPlugin'>) = {
    registerRecorderExtensionPlugin: async function(
        this: APIImpl.RecorderExtensions, plugin: PublicAPI.Chrome.DevTools.RecorderExtensionPlugin, pluginName: string,
        mediaType: string): Promise<void> {
      if (this._plugins.has(plugin)) {
        throw new Error(`Tried to register plugin '${pluginName}' twice`);
      }
      const channel = new MessageChannel();
      const port = channel.port1;
      this._plugins.set(plugin, port);
      port.onmessage = ({data}: MessageEvent<{requestId: number}&PrivateAPI.RecorderExtensionRequests>): void => {
        const {requestId} = data;
        dispatchMethodCall(data)
            .then(result => port.postMessage({requestId, result}))
            .catch(error => port.postMessage({requestId, error: {message: error.message}}));
      };

      async function dispatchMethodCall(request: PrivateAPI.RecorderExtensionRequests): Promise<unknown> {
        switch (request.method) {
          case PrivateAPI.RecorderExtensionPluginCommands.Stringify:
            return plugin.stringify(request.parameters.recording);
          case PrivateAPI.RecorderExtensionPluginCommands.StringifyStep:
            return plugin.stringifyStep(request.parameters.step);
          default:
            // @ts-expect-error
            throw new Error(`'${request.method}' is not recognized`);
        }
      }

      await new Promise<void>(resolve => {
        extensionServer.sendRequest(
            {
              command: PrivateAPI.Commands.RegisterRecorderExtensionPlugin,
              pluginName,
              mediaType,
              port: channel.port2,
            },
            () => resolve(), [channel.port2]);
      });
    },

    unregisterRecorderExtensionPlugin: async function(
        this: APIImpl.RecorderExtensions, plugin: PublicAPI.Chrome.DevTools.RecorderExtensionPlugin): Promise<void> {
      const port = this._plugins.get(plugin);
      if (!port) {
        throw new Error('Tried to unregister a plugin that was not previously registered');
      }
      this._plugins.delete(plugin);
      port.postMessage({event: PrivateAPI.RecorderExtensionPluginEvents.UnregisteredRecorderExtensionPlugin});
      port.close();
    },
  };

  function LanguageServicesAPIImpl(this: APIImpl.LanguageExtensions): void {
    this._plugins = new Map();
  }

  (LanguageServicesAPIImpl.prototype as PublicAPI.Chrome.DevTools.LanguageExtensions) = {
    registerLanguageExtensionPlugin: async function(
        this: APIImpl.LanguageExtensions, plugin: PublicAPI.Chrome.DevTools.LanguageExtensionPlugin, pluginName: string,
        supportedScriptTypes: PublicAPI.Chrome.DevTools.SupportedScriptTypes): Promise<void> {
      if (this._plugins.has(plugin)) {
        throw new Error(`Tried to register plugin '${pluginName}' twice`);
      }
      const channel = new MessageChannel();
      const port = channel.port1;
      this._plugins.set(plugin, port);
      port.onmessage = ({data}: MessageEvent<{requestId: number}&PrivateAPI.LanguageExtensionRequests>): void => {
        const {requestId} = data;
        console.time(`${requestId}: ${data.method}`);
        dispatchMethodCall(data)
            .then(result => port.postMessage({requestId, result}))
            .catch(error => port.postMessage({requestId, error: {message: error.message}}))
            .finally(() => console.timeEnd(`${requestId}: ${data.method}`));
      };

      function dispatchMethodCall(request: PrivateAPI.LanguageExtensionRequests): Promise<unknown> {
        switch (request.method) {
          case PrivateAPI.LanguageExtensionPluginCommands.AddRawModule:
            return plugin.addRawModule(
                request.parameters.rawModuleId, request.parameters.symbolsURL, request.parameters.rawModule);
          case PrivateAPI.LanguageExtensionPluginCommands.RemoveRawModule:
            return plugin.removeRawModule(request.parameters.rawModuleId);
          case PrivateAPI.LanguageExtensionPluginCommands.SourceLocationToRawLocation:
            return plugin.sourceLocationToRawLocation(request.parameters.sourceLocation);
          case PrivateAPI.LanguageExtensionPluginCommands.RawLocationToSourceLocation:
            return plugin.rawLocationToSourceLocation(request.parameters.rawLocation);
          case PrivateAPI.LanguageExtensionPluginCommands.GetScopeInfo:
            return plugin.getScopeInfo(request.parameters.type);
          case PrivateAPI.LanguageExtensionPluginCommands.ListVariablesInScope:
            return plugin.listVariablesInScope(request.parameters.rawLocation);
          case PrivateAPI.LanguageExtensionPluginCommands.GetTypeInfo:
            return plugin.getTypeInfo(request.parameters.expression, request.parameters.context);
          case PrivateAPI.LanguageExtensionPluginCommands.GetFormatter:
            return plugin.getFormatter(request.parameters.expressionOrField, request.parameters.context);
          case PrivateAPI.LanguageExtensionPluginCommands.GetInspectableAddress:
            if ('getInspectableAddress' in plugin) {
              return plugin.getInspectableAddress(request.parameters.field);
            }
            return Promise.resolve({js: ''});
          case PrivateAPI.LanguageExtensionPluginCommands.GetFunctionInfo:
            return plugin.getFunctionInfo(request.parameters.rawLocation);
          case PrivateAPI.LanguageExtensionPluginCommands.GetInlinedFunctionRanges:
            return plugin.getInlinedFunctionRanges(request.parameters.rawLocation);
          case PrivateAPI.LanguageExtensionPluginCommands.GetInlinedCalleesRanges:
            return plugin.getInlinedCalleesRanges(request.parameters.rawLocation);
          case PrivateAPI.LanguageExtensionPluginCommands.GetMappedLines:
            if ('getMappedLines' in plugin) {
              return plugin.getMappedLines(request.parameters.rawModuleId, request.parameters.sourceFileURL);
            }
            return Promise.resolve(undefined);
          case PrivateAPI.LanguageExtensionPluginCommands.FormatValue:
            if ('evaluate' in plugin && plugin.evaluate) {
              return plugin.evaluate(
                  request.parameters.expression, request.parameters.context, request.parameters.stopId);
            }
            return Promise.resolve(undefined);
          case PrivateAPI.LanguageExtensionPluginCommands.GetProperties:
            if ('getProperties' in plugin && plugin.getProperties) {
              return plugin.getProperties(request.parameters.objectId);
            }
            if (!('evaluate' in plugin &&
                  plugin.evaluate)) {  // If evalute is defined but the remote objects methods aren't, that's a bug
              return Promise.resolve(undefined);
            }
            break;
          case PrivateAPI.LanguageExtensionPluginCommands.ReleaseObject:
            if ('releaseObject' in plugin && plugin.releaseObject) {
              return plugin.releaseObject(request.parameters.objectId);
            }
            if (!('evaluate' in plugin) &&
                plugin.evaluate) {  // If evalute is defined but the remote objects methods aren't, that's a bug
              return Promise.resolve(undefined);
            }
            break;
        }
        throw new Error(`Unknown language plugin method ${request.method}`);
      }

      await new Promise<void>(resolve => {
        extensionServer.sendRequest(
            {
              command: PrivateAPI.Commands.RegisterLanguageExtensionPlugin,
              pluginName,
              port: channel.port2,
              supportedScriptTypes,
            },
            () => resolve(), [channel.port2]);
      });
    },

    unregisterLanguageExtensionPlugin: async function(
        this: APIImpl.LanguageExtensions, plugin: PublicAPI.Chrome.DevTools.LanguageExtensionPlugin): Promise<void> {
      const port = this._plugins.get(plugin);
      if (!port) {
        throw new Error('Tried to unregister a plugin that was not previously registered');
      }
      this._plugins.delete(plugin);
      port.postMessage({event: PrivateAPI.LanguageExtensionPluginEvents.UnregisteredLanguageExtensionPlugin});
      port.close();
    },

    getWasmLinearMemory: async function(
        this: APIImpl.LanguageExtensions, offset: number, length: number, stopId: number): Promise<ArrayBuffer> {
      const result = await new Promise(
          resolve => extensionServer.sendRequest(
              {command: PrivateAPI.Commands.GetWasmLinearMemory, offset, length, stopId}, resolve));
      if (Array.isArray(result)) {
        return new Uint8Array(result).buffer;
      }
      return new ArrayBuffer(0);
    },
    getWasmLocal: async function(
        this: APIImpl.LanguageExtensions, local: number, stopId: number): Promise<PublicAPI.Chrome.DevTools.WasmValue> {
      return new Promise(
          resolve => extensionServer.sendRequest({command: PrivateAPI.Commands.GetWasmLocal, local, stopId}, resolve));
    },
    getWasmGlobal: async function(this: APIImpl.LanguageExtensions, global: number, stopId: number):
        Promise<PublicAPI.Chrome.DevTools.WasmValue> {
          return new Promise(
              resolve =>
                  extensionServer.sendRequest({command: PrivateAPI.Commands.GetWasmGlobal, global, stopId}, resolve));
        },
    getWasmOp: async function(this: APIImpl.LanguageExtensions, op: number, stopId: number):
        Promise<PublicAPI.Chrome.DevTools.WasmValue> {
          return new Promise(
              resolve => extensionServer.sendRequest({command: PrivateAPI.Commands.GetWasmOp, op, stopId}, resolve));
        },
  };

  function declareInterfaceClass<ImplT extends APIImpl.Callable>(implConstructor: ImplT): (
      this: ThisParameterType<ImplT>, ...args: Parameters<ImplT>) => void {
    return function(this: ThisParameterType<ImplT>, ...args: Parameters<ImplT>): void {
      const impl = {__proto__: implConstructor.prototype};
      implConstructor.apply(impl, args);
      populateInterfaceClass(this as {[key: string]: unknown}, impl);
    };
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  function defineDeprecatedProperty(object: any, className: string, oldName: string, newName: string): void {
    let warningGiven = false;
    function getter(): unknown {
      if (!warningGiven) {
        console.warn(className + '.' + oldName + ' is deprecated. Use ' + className + '.' + newName + ' instead');
        warningGiven = true;
      }
      return object[newName];
    }
    object.__defineGetter__(oldName, getter);
  }

  function extractCallbackArgument(args: IArguments): ((...args: unknown[]) => unknown)|undefined {
    const lastArgument = args[args.length - 1];
    return typeof lastArgument === 'function' ? lastArgument as (...args: unknown[]) => unknown : undefined;
  }

  const LanguageServicesAPI = declareInterfaceClass(LanguageServicesAPIImpl);
  const RecorderServicesAPI = declareInterfaceClass(RecorderServicesAPIImpl);
  const Button = declareInterfaceClass(ButtonImpl);
  const EventSink = declareInterfaceClass(EventSinkImpl);
  const ExtensionPanel = declareInterfaceClass(ExtensionPanelImpl);
  const ExtensionSidebarPane = declareInterfaceClass(ExtensionSidebarPaneImpl);
  const PanelWithSidebarClass = declareInterfaceClass(PanelWithSidebarImpl);
  const Request = declareInterfaceClass(RequestImpl);
  const Resource = declareInterfaceClass(ResourceImpl);
  const TraceSession = declareInterfaceClass(TraceSessionImpl);

  class ElementsPanel extends (Constructor(PanelWithSidebarClass)) {
    constructor() {
      super('elements');
    }
  }

  class SourcesPanel extends (Constructor(PanelWithSidebarClass)) {
    constructor() {
      super('sources');
    }
  }

  function ExtensionPanelImpl(this: APIImpl.ExtensionPanel, id: string): void {
    ExtensionViewImpl.call(this, id);

    this.onSearch = new (Constructor(EventSink))(PrivateAPI.Events.PanelSearch + id);
  }

  (ExtensionPanelImpl.prototype as Pick<APIImpl.ExtensionPanel, 'createStatusBarButton'|'show'>&
   {__proto__: APIImpl.ExtensionView}) = {
    createStatusBarButton: function(
                               this: APIImpl.ExtensionPanel, iconPath: string, tooltipText: string, disabled: boolean):
                               PublicAPI.Chrome.DevTools.Button {
                                 const id = 'button-' + extensionServer.nextObjectId();
                                 extensionServer.sendRequest({
                                   command: PrivateAPI.Commands.CreateToolbarButton,
                                   panel: this._id as string,
                                   id: id,
                                   icon: iconPath,
                                   tooltip: tooltipText,
                                   disabled: Boolean(disabled),
                                 });

                                 return new (Constructor(Button))(id);
                               },

    show: function(this: APIImpl.ExtensionPanel): void {
      if (!userAction) {
        return;
      }

      extensionServer.sendRequest({command: PrivateAPI.Commands.ShowPanel, id: this._id as string});
    },

    __proto__: ExtensionViewImpl.prototype,
  };

  function ExtensionSidebarPaneImpl(this: APIImpl.ExtensionSidebarPane, id: string): void {
    ExtensionViewImpl.call(this, id);
  }

  (ExtensionSidebarPaneImpl.prototype as
       Pick<APIImpl.ExtensionSidebarPane, 'setHeight'|'setExpression'|'setObject'|'setPage'>&
   {__proto__: APIImpl.ExtensionView}) = {
    setHeight: function(this: APIImpl.ExtensionSidebarPane, height: string): void {
      extensionServer.sendRequest(
          {command: PrivateAPI.Commands.SetSidebarHeight, id: this._id as string, height: height});
    },

    setExpression: function(
        this: APIImpl.ExtensionSidebarPane, expression: string, rootTitle: string,
        evaluateOptions?: PrivateAPI.EvaluateOptions, _callback?: () => unknown): void {
      extensionServer.sendRequest(
          {
            command: PrivateAPI.Commands.SetSidebarContent,
            id: this._id as string,
            expression: expression,
            rootTitle: rootTitle,
            evaluateOnPage: true,
            evaluateOptions: (typeof evaluateOptions === 'object' ? evaluateOptions : {}),
          },
          extractCallbackArgument(arguments));
    },

    setObject: function(
        this: APIImpl.ExtensionSidebarPane, jsonObject: string, rootTitle?: string, callback?: () => unknown): void {
      extensionServer.sendRequest(
          {
            command: PrivateAPI.Commands.SetSidebarContent,
            id: this._id as string,
            expression: jsonObject,
            rootTitle: rootTitle,
          },
          callback);
    },

    setPage: function(this: APIImpl.ExtensionSidebarPane, page: string): void {
      extensionServer.sendRequest({command: PrivateAPI.Commands.SetSidebarPage, id: this._id as string, page: page});
    },

    __proto__: ExtensionViewImpl.prototype,
  };

  function ButtonImpl(this: APIImpl.Button, id: string): void {
    this._id = id;

    this.onClicked = new (Constructor(EventSink))(PrivateAPI.Events.ButtonClicked + id);
  }

  (ButtonImpl.prototype as Pick<APIImpl.Button, 'update'>) = {
    update: function(this: APIImpl.Button, iconPath?: string, tooltipText?: string, disabled?: boolean): void {
      extensionServer.sendRequest({
        command: PrivateAPI.Commands.UpdateButton,
        id: this._id,
        icon: iconPath,
        tooltip: tooltipText,
        disabled: Boolean(disabled),
      });
    },
  };

  function Timeline(this: APIImpl.Timeline): void {
  }

  (Timeline.prototype as Pick<APIImpl.Timeline, 'addTraceProvider'>) = {
    addTraceProvider: function(this: APIImpl.Timeline, categoryName: string, categoryTooltip: string):
                          APIImpl.TraceProvider {
                            const id = 'extension-trace-provider-' + extensionServer.nextObjectId();
                            extensionServer.sendRequest({
                              command: PrivateAPI.Commands.AddTraceProvider,
                              id: id,
                              categoryName: categoryName,
                              categoryTooltip: categoryTooltip,
                            });

                            return new (Constructor(TraceProvider))(id);
                          },
  };

  function TraceSessionImpl(this: APIImpl.TraceSession, id: string): void {
    this._id = id;
  }

  (TraceSessionImpl.prototype as Pick<APIImpl.TraceSession, 'complete'>) = {
    complete: function(this: APIImpl.TraceSession, url?: Platform.DevToolsPath.UrlString, timeOffset?: number): void {
      extensionServer.sendRequest({
        command: PrivateAPI.Commands.CompleteTraceSession,
        id: this._id,
        url: url || Platform.DevToolsPath.EmptyUrlString,
        timeOffset: timeOffset || 0,
      });
    },
  };

  function TraceProvider(this: APIImpl.TraceProvider, id: string): void {
    function dispatchRecordingStarted(
        this: APIImpl.EventSink<APIImpl.Callable>, message: {arguments: unknown[]}): void {
      const sessionId = message.arguments[0] as string;

      this._fire(new (Constructor(TraceSession))(sessionId));
    }

    this.onRecordingStarted =
        new (Constructor(EventSink))(PrivateAPI.Events.RecordingStarted + id, dispatchRecordingStarted);

    this.onRecordingStopped = new (Constructor(EventSink))(PrivateAPI.Events.RecordingStopped + id);
  }

  function InspectedWindow(this: PublicAPI.Chrome.DevTools.InspectedWindow): void {
    function dispatchResourceEvent(
        this: APIImpl.EventSink<(resource: APIImpl.Resource) => unknown>, message: {arguments: unknown[]}): void {
      this._fire(new (Constructor(Resource))(message.arguments[0] as APIImpl.ResourceData));
    }

    function dispatchResourceContentEvent(
        this: APIImpl.EventSink<(resource: APIImpl.Resource, content: string) => unknown>,
        message: {arguments: unknown[]}): void {
      this._fire(
          new (Constructor(Resource))(message.arguments[0] as APIImpl.ResourceData), message.arguments[1] as string);
    }

    this.onResourceAdded = new (Constructor(EventSink))(PrivateAPI.Events.ResourceAdded, dispatchResourceEvent);
    this.onResourceContentCommitted =
        new (Constructor(EventSink))(PrivateAPI.Events.ResourceContentCommitted, dispatchResourceContentEvent);
  }

  (InspectedWindow.prototype as Pick<PublicAPI.Chrome.DevTools.InspectedWindow, 'reload'|'eval'|'getResources'>) = {
    reload: function(optionsOrUserAgent: {
      ignoreCache?: boolean,
      injectedScript?: string,
      userAgent?: string,
    }): void {
      let options: {
        ignoreCache?: boolean,
        injectedScript?: string,
        userAgent?: string,
      }|null = null;
      if (typeof optionsOrUserAgent === 'object') {
        options = optionsOrUserAgent;
      } else if (typeof optionsOrUserAgent === 'string') {
        options = {userAgent: optionsOrUserAgent};
        console.warn(
            'Passing userAgent as string parameter to inspectedWindow.reload() is deprecated. ' +
            'Use inspectedWindow.reload({ userAgent: value}) instead.');
      }
      extensionServer.sendRequest({command: PrivateAPI.Commands.Reload, options: options});
    },

    eval: function(
              expression: string,
              evaluateOptions: {contextSecurityOrigin?: string, frameURL?: string, useContentScriptContext?: boolean}):
              Object |
        null {
          const callback = extractCallbackArgument(arguments);
          function callbackWrapper(result: unknown): void {
            const {isError, isException, value} = result as {
              isError?: boolean,
              isException?: boolean, value: unknown,
            };
            if (isError || isException) {
              callback && callback(undefined, result);
            } else {
              callback && callback(value);
            }
          }
          extensionServer.sendRequest(
              {
                command: PrivateAPI.Commands.EvaluateOnInspectedPage,
                expression: expression,
                evaluateOptions: (typeof evaluateOptions === 'object' ? evaluateOptions : undefined),
              },
              callback && callbackWrapper);
          return null;
        },

    getResources: function(callback?: (resources: PublicAPI.Chrome.DevTools.Resource[]) => unknown): void {
      function wrapResource(resourceData: APIImpl.ResourceData): APIImpl.Resource {
        return new (Constructor(Resource))(resourceData);
      }
      function callbackWrapper(resources: unknown): void {
        callback && callback((resources as APIImpl.ResourceData[]).map(wrapResource));
      }
      extensionServer.sendRequest({command: PrivateAPI.Commands.GetPageResources}, callback && callbackWrapper);
    },
  };

  function ResourceImpl(this: APIImpl.Resource, resourceData: APIImpl.ResourceData): void {
    this._url = resourceData.url;
    this._type = resourceData.type;
  }

  (ResourceImpl.prototype as Pick<APIImpl.Resource, 'url'|'type'|'getContent'|'setContent'>) = {
    get url(): string {
      return (this as APIImpl.Resource)._url;
    },

    get type(): string {
      return (this as APIImpl.Resource)._type;
    },

    getContent: function(this: APIImpl.Resource, callback?: (content: string, encoding: string) => unknown): void {
      function callbackWrapper(response: unknown): void {
        const {content, encoding} = response as {content: string, encoding: string};
        callback && callback(content, encoding);
      }

      extensionServer.sendRequest(
          {command: PrivateAPI.Commands.GetResourceContent, url: this._url}, callback && callbackWrapper);
    },

    setContent: function(
        this: APIImpl.Resource, content: string, commit: boolean, callback: (error?: Object) => unknown): void {
      extensionServer.sendRequest(
          {command: PrivateAPI.Commands.SetResourceContent, url: this._url, content: content, commit: commit},
          callback as (response: unknown) => unknown);
    },
  };

  function getTabId(): string {
    return inspectedTabId;
  }

  let keyboardEventRequestQueue: KeyboardEventInit&{eventType: string}[] = [];
  let forwardTimer: number|null = null;
  function forwardKeyboardEvent(event: KeyboardEvent): void {
    // Check if the event should be forwarded.
    // This is a workaround for crbug.com/923338.
    const focused = document.activeElement;
    if (focused) {
      const isInput = focused.nodeName === 'INPUT' || focused.nodeName === 'TEXTAREA';
      if (isInput && !(event.ctrlKey || event.altKey || event.metaKey)) {
        return;
      }
    }

    let modifiers = 0;
    if (event.shiftKey) {
      modifiers |= 1;
    }
    if (event.ctrlKey) {
      modifiers |= 2;
    }
    if (event.altKey) {
      modifiers |= 4;
    }
    if (event.metaKey) {
      modifiers |= 8;
    }
    const num = (event.keyCode & 255) | (modifiers << 8);
    // We only care about global hotkeys, not about random text
    if (!keysToForwardSet.has(num)) {
      return;
    }
    event.preventDefault();
    const requestPayload = {
      eventType: event.type,
      ctrlKey: event.ctrlKey,
      altKey: event.altKey,
      metaKey: event.metaKey,
      shiftKey: event.shiftKey,
      // @ts-expect-error keyIdentifier is a deprecated non-standard property that typescript doesn't know about.
      keyIdentifier: event.keyIdentifier,
      key: event.key,
      code: event.code,
      location: event.location,
      keyCode: event.keyCode,
    };
    keyboardEventRequestQueue.push(requestPayload);
    if (!forwardTimer) {
      forwardTimer = window.setTimeout(forwardEventQueue, 0);
    }
  }

  function forwardEventQueue(): void {
    forwardTimer = null;
    extensionServer.sendRequest(
        {command: PrivateAPI.Commands.ForwardKeyboardEvent, entries: keyboardEventRequestQueue});
    keyboardEventRequestQueue = [];
  }

  document.addEventListener('keydown', forwardKeyboardEvent, false);

  function ExtensionServerClient(this: APIImpl.ExtensionServerClient, targetWindow: Window): void {
    this._callbacks = {};
    this._handlers = {};
    this._lastRequestId = 0;
    this._lastObjectId = 0;

    this.registerHandler('callback', this._onCallback.bind(this));

    const channel = new MessageChannel();
    this._port = channel.port1;
    this._port.addEventListener('message', this._onMessage.bind(this), false);
    this._port.start();

    targetWindow.postMessage('registerExtension', '*', [channel.port2]);
  }

  (ExtensionServerClient.prototype as Pick<
       APIImpl.ExtensionServerClient,
       'sendRequest'|'hasHandler'|'registerHandler'|'unregisterHandler'|'nextObjectId'|'_registerCallback'|
       '_onCallback'|'_onMessage'>) = {
    sendRequest: function<ResponseT>(
        this: APIImpl.ExtensionServerClient, message: PrivateAPI.ServerRequests,
        callback?: (response: ResponseT) => unknown, transfers?: Transferable[]): void {
      if (typeof callback === 'function') {
        (message as PrivateAPI.ExtensionServerRequestMessage).requestId =
            this._registerCallback(callback as (response: unknown) => unknown);
      }
      // @ts-expect-error
      this._port.postMessage(message, transfers);
    },

    hasHandler: function(this: APIImpl.ExtensionServerClient, command: string): boolean {
      return Boolean(this._handlers[command]);
    },

    registerHandler: function(
        this: APIImpl.ExtensionServerClient, command: string, handler: (request: {arguments: unknown[]}) => unknown):
        void {
          this._handlers[command] = handler;
        },

    unregisterHandler: function(this: APIImpl.ExtensionServerClient, command: string): void {
      delete this._handlers[command];
    },

    nextObjectId: function(this: APIImpl.ExtensionServerClient): string {
      return injectedScriptId.toString() + '_' + ++this._lastObjectId;
    },

    _registerCallback: function(this: APIImpl.ExtensionServerClient, callback: (response: unknown) => unknown): number {
      const id = ++this._lastRequestId;
      this._callbacks[id] = callback;
      return id;
    },

    _onCallback: function(this: APIImpl.ExtensionServerClient, request: {requestId: number, result: unknown}): void {
      if (request.requestId in this._callbacks) {
        const callback = this._callbacks[request.requestId];
        delete this._callbacks[request.requestId];
        callback(request.result);
      }
    },

    _onMessage: function(
        this: APIImpl.ExtensionServerClient,
        event: MessageEvent<{command: string, requestId: number, arguments: unknown[]}>): void {
      const request = event.data;
      const handler = this._handlers[request.command];
      if (handler) {
        handler.call(this, request);
      }
    },
  };

  function populateInterfaceClass(interfaze: {[key: string]: unknown}, implementation: {[key: string]: unknown}): void {
    for (const member in implementation) {
      if (member.charAt(0) === '_') {
        continue;
      }
      let descriptor: (PropertyDescriptor|undefined)|null = null;
      // Traverse prototype chain until we find the owner.
      for (let owner = implementation; owner && !descriptor; owner = owner.__proto__ as {[key: string]: unknown}) {
        descriptor = Object.getOwnPropertyDescriptor(owner, member);
      }
      if (!descriptor) {
        continue;
      }
      if (typeof descriptor.value === 'function') {
        interfaze[member] = descriptor.value.bind(implementation);
      } else if (typeof descriptor.get === 'function') {
        // @ts-expect-error
        interfaze.__defineGetter__(member, descriptor.get.bind(implementation));
      } else {
        Object.defineProperty(interfaze, member, descriptor);
      }
    }
  }

  const extensionServer = new (Constructor(ExtensionServerClient))(targetWindowForTest || window.parent);

  const coreAPI = new (Constructor(InspectorExtensionAPI))();

  Object.defineProperty(chrome, 'devtools', {value: {}, enumerable: true});

  // Only expose tabId on chrome.devtools.inspectedWindow, not webInspector.inspectedWindow.
  // @ts-expect-error
  chrome.devtools!.inspectedWindow = {};
  Object.defineProperty(chrome.devtools!.inspectedWindow, 'tabId', {get: getTabId});
  // @ts-expect-error
  chrome.devtools!.inspectedWindow.__proto__ = coreAPI.inspectedWindow;
  chrome.devtools!.network = coreAPI.network;
  chrome.devtools!.panels = coreAPI.panels;
  chrome.devtools!.panels.themeName = themeName;
  chrome.devtools!.languageServices = coreAPI.languageServices;
  chrome.devtools!.recorder = coreAPI.recorder;

  // default to expose experimental APIs for now.
  if (extensionInfo.exposeExperimentalAPIs !== false) {
    chrome.experimental = chrome.experimental || {};
    chrome.experimental.devtools = chrome.experimental.devtools || {};

    const properties = Object.getOwnPropertyNames(coreAPI);
    for (let i = 0; i < properties.length; ++i) {
      const descriptor = Object.getOwnPropertyDescriptor(coreAPI, properties[i]);
      if (descriptor) {
        Object.defineProperty(chrome.experimental.devtools, properties[i], descriptor);
      }
    }
    chrome.experimental.devtools.inspectedWindow = chrome.devtools.inspectedWindow;
  }

  if (extensionInfo.exposeWebInspectorNamespace) {
    window.webInspector = coreAPI;
  }
  testHook(extensionServer, coreAPI);
};

self.buildExtensionAPIInjectedScript = function(
    extensionInfo: {
      startPage: string,
      name: string,
      exposeExperimentalAPIs: boolean,
    },
    inspectedTabId: string, themeName: string, keysToForward: number[],
    testHook:
        ((extensionServer: APIImpl.ExtensionServerClient, extensionAPI: APIImpl.InspectorExtensionAPI) => unknown)|
    undefined): string {
  const argumentsJSON =
      [extensionInfo, inspectedTabId || null, themeName, keysToForward].map(_ => JSON.stringify(_)).join(',');
  if (!testHook) {
    testHook = (): void => {};
  }
  return '(function(injectedScriptId){ ' +
      '(' + self.injectedExtensionAPI.toString() + ')(' + argumentsJSON + ',' + testHook + ', injectedScriptId);' +
      '})';
};
