// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

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

/* eslint-disable @typescript-eslint/naming-convention */
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-',
    ProfilingStarted = 'profiling-started-',
    ProfilingStopped = 'profiling-stopped-',
    ResourceAdded = 'resource-added',
    ResourceContentCommitted = 'resource-content-committed',
    ViewShown = 'view-shown-',
    ViewHidden = 'view-hidden,',
    ThemeChange = 'host-theme-change',
  }

  export const enum Commands {
    AddRequestHeaders = 'addRequestHeaders',
    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',
    AttachSourceMapToResource = 'attachSourceMapToResource',
    RegisterLanguageExtensionPlugin = 'registerLanguageExtensionPlugin',
    GetWasmLinearMemory = 'getWasmLinearMemory',
    GetWasmLocal = 'getWasmLocal',
    GetWasmGlobal = 'getWasmGlobal',
    GetWasmOp = 'getWasmOp',
    RegisterRecorderExtensionPlugin = 'registerRecorderExtensionPlugin',
    CreateRecorderView = 'createRecorderView',
    ShowRecorderView = 'showRecorderView',
    ShowNetworkPanel = 'showNetworkPanel',
    ReportResourceLoad = 'reportResourceLoad',
    SetFunctionRangesForScript = 'setFunctionRangesForScript',
  }

  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',
    Replay = 'replay',
  }

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

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

  interface RegisterLanguageExtensionPluginRequest {
    command: Commands.RegisterLanguageExtensionPlugin;
    pluginName: string;
    port: MessagePort;
    supportedScriptTypes: PublicAPI.Chrome.DevTools.SupportedScriptTypes;
  }
  export type RecordingExtensionPluginCapability = 'export'|'replay';
  interface RegisterRecorderExtensionPluginRequest {
    command: Commands.RegisterRecorderExtensionPlugin;
    pluginName: string;
    capabilities: RecordingExtensionPluginCapability[];
    port: MessagePort;
    mediaType?: string;
  }
  interface CreateRecorderViewRequest {
    command: Commands.CreateRecorderView;
    id: string;
    title: string;
    pagePath: string;
  }
  interface ShowRecorderViewRequest {
    command: Commands.ShowRecorderView;
    id: string;
  }
  interface SubscribeRequest {
    command: Commands.Subscribe;
    type: string;
  }
  interface UnsubscribeRequest {
    command: Commands.Unsubscribe;
    type: string;
  }
  interface AddRequestHeadersRequest {
    command: Commands.AddRequestHeaders;
    extensionId: string;
    headers: Record<string, string>;
  }
  interface CreatePanelRequest {
    command: Commands.CreatePanel;
    id: string;
    title: string;
    page: string;
  }
  interface ShowPanelRequest {
    command: Commands.ShowPanel;
    id: string;
  }
  interface CreateToolbarButtonRequest {
    command: Commands.CreateToolbarButton;
    id: string;
    icon: string;
    panel: string;
    tooltip?: string;
    disabled?: boolean;
  }
  interface UpdateButtonRequest {
    command: Commands.UpdateButton;
    id: string;
    icon?: string;
    tooltip?: string;
    disabled?: boolean;
  }
  interface CreateSidebarPaneRequest {
    command: Commands.CreateSidebarPane;
    id: string;
    panel: string;
    title: string;
  }
  interface SetSidebarHeightRequest {
    command: Commands.SetSidebarHeight;
    id: string;
    height: string;
  }
  interface SetSidebarContentRequest {
    command: Commands.SetSidebarContent;
    id: string;
    expression: string;
    evaluateOnPage?: boolean;
    rootTitle?: string;
    evaluateOptions?: EvaluateOptions;
  }
  interface SetSidebarPageRequest {
    command: Commands.SetSidebarPage;
    id: string;
    page: string;
  }
  interface OpenResourceRequest {
    command: Commands.OpenResource;
    url: Platform.DevToolsPath.UrlString;
    lineNumber: number;
    columnNumber: number;
  }
  interface SetOpenResourceHandlerRequest {
    command: Commands.SetOpenResourceHandler;
    handlerPresent: boolean;
    urlScheme?: string;
  }
  interface SetThemeChangeHandlerRequest {
    command: Commands.SetThemeChangeHandler;
    handlerPresent: boolean;
  }
  interface ReloadRequest {
    command: Commands.Reload;
    options: null|{
      userAgent?: string,
      injectedScript?: string,
      ignoreCache?: boolean,
    };
  }
  interface EvaluateOnInspectedPageRequest {
    command: Commands.EvaluateOnInspectedPage;
    expression: string;
    evaluateOptions?: EvaluateOptions;
  }
  interface GetRequestContentRequest {
    command: Commands.GetRequestContent;
    id: number;
  }
  interface GetResourceContentRequest {
    command: Commands.GetResourceContent;
    url: string;
  }
  interface AttachSourceMapToResourceRequest {
    command: Commands.AttachSourceMapToResource;
    contentUrl: string;
    sourceMapURL: string;
  }
  interface SetResourceContentRequest {
    command: Commands.SetResourceContent;
    url: string;
    content: string;
    commit: boolean;
  }
  interface SetFunctionRangesForScriptRequest {
    command: Commands.SetFunctionRangesForScript;
    scriptUrl: string;
    ranges: PublicAPI.Chrome.DevTools.NamedFunctionRange[];
  }
  interface ForwardKeyboardEventRequest {
    command: Commands.ForwardKeyboardEvent;
    entries: Array<KeyboardEventInit&{eventType: string}>;
  }
  interface GetHARRequest {
    command: Commands.GetHAR;
  }
  interface GetPageResourcesRequest {
    command: Commands.GetPageResources;
  }
  interface GetWasmLinearMemoryRequest {
    command: Commands.GetWasmLinearMemory;
    offset: number;
    length: number;
    stopId: unknown;
  }
  interface GetWasmLocalRequest {
    command: Commands.GetWasmLocal;
    local: number;
    stopId: unknown;
  }
  interface GetWasmGlobalRequest {
    command: Commands.GetWasmGlobal;
    global: number;
    stopId: unknown;
  }
  interface GetWasmOpRequest {
    command: Commands.GetWasmOp;
    op: number;
    stopId: unknown;
  }
  interface ShowNetworkPanelRequest {
    command: Commands.ShowNetworkPanel;
    filter: string|undefined;
  }
  interface ReportResourceLoadRequest {
    command: Commands.ReportResourceLoad;
    extensionId: string;
    resourceUrl: string;
    status: {success: boolean, errorMessage?: string, size?: number};
  }

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

  interface AddRawModuleRequest {
    method: LanguageExtensionPluginCommands.AddRawModule;
    parameters: {rawModuleId: string, symbolsURL: string|undefined, rawModule: PublicAPI.Chrome.DevTools.RawModule};
  }
  interface SourceLocationToRawLocationRequest {
    method: LanguageExtensionPluginCommands.SourceLocationToRawLocation;
    parameters: {sourceLocation: PublicAPI.Chrome.DevTools.SourceLocation};
  }
  interface RawLocationToSourceLocationRequest {
    method: LanguageExtensionPluginCommands.RawLocationToSourceLocation;
    parameters: {rawLocation: PublicAPI.Chrome.DevTools.RawLocation};
  }
  interface GetScopeInfoRequest {
    method: LanguageExtensionPluginCommands.GetScopeInfo;
    parameters: {type: string};
  }
  interface ListVariablesInScopeRequest {
    method: LanguageExtensionPluginCommands.ListVariablesInScope;
    parameters: {rawLocation: PublicAPI.Chrome.DevTools.RawLocation};
  }
  interface RemoveRawModuleRequest {
    method: LanguageExtensionPluginCommands.RemoveRawModule;
    parameters: {rawModuleId: string};
  }
  interface GetFunctionInfoRequest {
    method: LanguageExtensionPluginCommands.GetFunctionInfo;
    parameters: {rawLocation: PublicAPI.Chrome.DevTools.RawLocation};
  }
  interface GetInlinedFunctionRangesRequest {
    method: LanguageExtensionPluginCommands.GetInlinedFunctionRanges;
    parameters: {rawLocation: PublicAPI.Chrome.DevTools.RawLocation};
  }
  interface GetInlinedCalleesRangesRequest {
    method: LanguageExtensionPluginCommands.GetInlinedCalleesRanges;
    parameters: {rawLocation: PublicAPI.Chrome.DevTools.RawLocation};
  }
  interface GetMappedLinesRequest {
    method: LanguageExtensionPluginCommands.GetMappedLines;
    parameters: {rawModuleId: string, sourceFileURL: string};
  }
  interface FormatValueRequest {
    method: LanguageExtensionPluginCommands.FormatValue;
    parameters: {expression: string, context: PublicAPI.Chrome.DevTools.RawLocation, stopId: number};
  }
  interface GetPropertiesRequest {
    method: LanguageExtensionPluginCommands.GetProperties;
    parameters: {objectId: PublicAPI.Chrome.DevTools.RemoteObjectId};
  }
  interface ReleaseObjectRequest {
    method: LanguageExtensionPluginCommands.ReleaseObject;
    parameters: {objectId: PublicAPI.Chrome.DevTools.RemoteObjectId};
  }

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

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

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

  interface ReplayRequest {
    method: RecorderExtensionPluginCommands.Replay;
    parameters: {recording: Record<string, unknown>};
  }

  export type RecorderExtensionRequests = StringifyRequest|StringifyStepRequest|ReplayRequest;
}

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?: (extensionServer: unknown, extensionAPI: unknown) => unknown): string;
    chrome: PublicAPI.Chrome.DevTools.Chrome;
    webInspector?: APIImpl.InspectorExtensionAPI;
  }
}

export interface ExtensionDescriptor {
  startPage: string;
  name: string;
  exposeExperimentalAPIs: boolean;
  exposeWebInspectorNamespace?: boolean;
  allowFileAccess?: boolean;
}

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

  export interface ExtensionServerClient {
    _callbacks: Record<string, (response: unknown) => unknown>;
    _handlers: Record<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;
  }

  export type Callable = (...args: any[]) => void;

  export interface EventSink<ListenerT extends Callable> extends PublicAPI.Chrome.DevTools.EventSink<ListenerT> {
    _type: string;
    _listeners: ListenerT[];
    _customDispatch?: (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: Record<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(): Record<string, string>;
    setOpenResourceHandler(
        callback?: (resource: PublicAPI.Chrome.DevTools.Resource, lineNumber: number, columnNumber: 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 RecorderView extends ExtensionView, PublicAPI.Chrome.DevTools.RecorderView {}

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

  export interface ResourceData {
    url: string;
    type: string;
    buildId?: string;
  }
  export interface Resource extends PublicAPI.Chrome.DevTools.Resource {
    _type: string;
    _url: string;
    _buildId?: 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;
  let userRecorderAction = 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 new Error('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.languageServices = new (Constructor(LanguageServicesAPI))();
    this.recorder = new (Constructor(RecorderServicesAPI))();
    this.performance = new (Constructor(Performance))();
    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?.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?.(result);
      }
      extensionServer.sendRequest({command: PrivateAPI.Commands.GetHAR}, callback && callbackWrapper);
    },

    addRequestHeaders: function(headers: Record<string, string>): void {
      extensionServer.sendRequest(
          {command: PrivateAPI.Commands.AddRequestHeaders, 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?.(content, encoding);
      }
      extensionServer.sendRequest(
          {command: PrivateAPI.Commands.GetRequestContent, id: this._id}, callback && callbackWrapper);
    },
  };

  function Panels(this: APIImpl.Panels): void {
    const panels: Record<string, ElementsPanel|SourcesPanel|PublicAPI.Chrome.DevTools.NetworkPanel> = {
      elements: new ElementsPanel(),
      sources: new SourcesPanel(),
      network: new (Constructor(NetworkPanel))(),
    };

    function panelGetter(name: string): ElementsPanel|SourcesPanel|PublicAPI.Chrome.DevTools.NetworkPanel {
      return panels[name];
    }
    for (const panel in panels) {
      Object.defineProperty(this, panel, {get: panelGetter.bind(null, panel), enumerable: true});
    }
  }

  (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 && (() => callback.call(this, new (Constructor(ExtensionPanel))(id))));
    },

    setOpenResourceHandler: function(
        callback: (resource: PublicAPI.Chrome.DevTools.Resource, lineNumber: number, columnNumber: number) => unknown,
        urlScheme?: string): 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, columnNumber} =
              message as {resource: APIImpl.ResourceData, lineNumber: number, columnNumber: number};
          callback.call(null, new (Constructor(Resource))(resource), lineNumber, columnNumber);
        } 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), urlScheme});
      }
    },

    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(): Record<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?.(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();
  }

  async function registerRecorderExtensionPluginImpl(
      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 await (plugin as PublicAPI.Chrome.DevTools.RecorderExtensionExportPlugin)
              .stringify(request.parameters.recording);
        case PrivateAPI.RecorderExtensionPluginCommands.StringifyStep:
          return await (plugin as PublicAPI.Chrome.DevTools.RecorderExtensionExportPlugin)
              .stringifyStep(request.parameters.step);
        case PrivateAPI.RecorderExtensionPluginCommands.Replay:
          try {
            userAction = true;
            userRecorderAction = true;
            return (plugin as PublicAPI.Chrome.DevTools.RecorderExtensionReplayPlugin)
                .replay(request.parameters.recording);
          } finally {
            userAction = false;
            userRecorderAction = false;
          }
        default:
          // @ts-expect-error
          throw new Error(`'${request.method}' is not recognized`);
      }
    }

    const capabilities: PrivateAPI.RecordingExtensionPluginCapability[] = [];

    if ('stringify' in plugin && 'stringifyStep' in plugin) {
      capabilities.push('export');
    }

    if ('replay' in plugin) {
      capabilities.push('replay');
    }

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

  (RecorderServicesAPIImpl.prototype as Pick<
       APIImpl.RecorderExtensions,
       'registerRecorderExtensionPlugin'|'unregisterRecorderExtensionPlugin'|'createView'>) = {
    registerRecorderExtensionPlugin: registerRecorderExtensionPluginImpl,
    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();
    },
    createView: async function(this: APIImpl.RecorderExtensions, title: string, pagePath: string):
        Promise<PublicAPI.Chrome.DevTools.RecorderView> {
          const id = 'recorder-extension-view-' + extensionServer.nextObjectId();
          await new Promise(resolve => {
            extensionServer.sendRequest(
                {command: PrivateAPI.Commands.CreateRecorderView, id, title, pagePath}, resolve);
          });
          return new (Constructor(RecorderView))(id);
        },
  };

  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.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);
            }
            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 await 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 await 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 await new Promise(
              resolve => extensionServer.sendRequest({command: PrivateAPI.Commands.GetWasmOp, op, stopId}, resolve));
        },

    reportResourceLoad: function(resourceUrl: string, status: {success: boolean, errorMessage?: string, size?: number}):
        Promise<void> {
          return new Promise(
              resolve => extensionServer.sendRequest(
                  {
                    command: PrivateAPI.Commands.ReportResourceLoad,
                    extensionId: window.location.origin,
                    resourceUrl,
                    status,
                  },
                  resolve));
        },

  };

  function NetworkPanelImpl(this: PublicAPI.Chrome.DevTools.NetworkPanel): void {
  }

  (NetworkPanelImpl.prototype as Pick<PublicAPI.Chrome.DevTools.NetworkPanel, 'show'>) = {
    show: function(options?: {filter: string}): Promise<void> {
      return new Promise<void>(
          resolve => extensionServer.sendRequest(
              {command: PrivateAPI.Commands.ShowNetworkPanel, filter: options?.filter}, () => resolve()));
    },
  };

  function PerformanceImpl(this: PublicAPI.Chrome.DevTools.Performance): void {
    function dispatchProfilingStartedEvent(this: APIImpl.EventSink<() => unknown>): void {
      this._fire();
    }

    function dispatchProfilingStoppedEvent(this: APIImpl.EventSink<() => unknown>): void {
      this._fire();
    }

    this.onProfilingStarted =
        new (Constructor(EventSink))(PrivateAPI.Events.ProfilingStarted, dispatchProfilingStartedEvent);
    this.onProfilingStopped =
        new (Constructor(EventSink))(PrivateAPI.Events.ProfilingStopped, dispatchProfilingStoppedEvent);
  }

  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 Record<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 Performance = declareInterfaceClass(PerformanceImpl);
  const Button = declareInterfaceClass(ButtonImpl);
  const EventSink = declareInterfaceClass(EventSinkImpl);
  const ExtensionPanel = declareInterfaceClass(ExtensionPanelImpl);
  const RecorderView = declareInterfaceClass(RecorderViewImpl);
  const ExtensionSidebarPane = declareInterfaceClass(ExtensionSidebarPaneImpl);
  const PanelWithSidebarClass = declareInterfaceClass(PanelWithSidebarImpl);
  const Request = declareInterfaceClass(RequestImpl);
  const Resource = declareInterfaceClass(ResourceImpl);
  const NetworkPanel = declareInterfaceClass(NetworkPanelImpl);

  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,
                                   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 RecorderViewImpl(this: APIImpl.RecorderView, id: string): void {
    ExtensionViewImpl.call(this, id);
  }

  (RecorderViewImpl.prototype as Pick<APIImpl.RecorderView, 'show'>& {__proto__: APIImpl.ExtensionView}) = {
    show: function(this: APIImpl.RecorderView): void {
      if (!userAction || !userRecorderAction) {
        return;
      }

      extensionServer.sendRequest({command: PrivateAPI.Commands.ShowRecorderView, 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});
    },

    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,
            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,
          },
          callback);
    },

    setPage: function(this: APIImpl.ExtensionSidebarPane, page: string): void {
      extensionServer.sendRequest({command: PrivateAPI.Commands.SetSidebarPage, id: this._id as string, 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 InspectedWindow(this: PublicAPI.Chrome.DevTools.InspectedWindow): void {
    function dispatchResourceEvent(
        this: APIImpl.EventSink<(resource: APIImpl.Resource) => unknown>, message: {arguments: unknown[]}): void {
      const resourceData = message.arguments[0] as APIImpl.ResourceData;
      this._fire(new (Constructor(Resource))(resourceData));
    }

    function dispatchResourceContentEvent(
        this: APIImpl.EventSink<(resource: APIImpl.Resource, content: string) => unknown>,
        message: {arguments: unknown[]}): void {
      const resourceData = message.arguments[0] as APIImpl.ResourceData;
      this._fire(new (Constructor(Resource))(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});
    },

    eval: function(
              expression: string,
              evaluateOptions: {scriptExecutionContext?: string, frameURL?: string, useContentScriptContext?: boolean}):
              Object |
        null {
          const callback = extractCallbackArgument(arguments);
          function callbackWrapper(result: unknown): void {
            const {isError, isException, value} = result as {
              value: unknown,
              isError?: boolean,
              isException?: boolean,
            };
            if (isError || isException) {
              callback?.(undefined, result);
            } else {
              callback?.(value);
            }
          }
          extensionServer.sendRequest(
              {
                command: PrivateAPI.Commands.EvaluateOnInspectedPage,
                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?.((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;
    this._buildId = resourceData.buildId;
  }

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

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

    get buildId(): (string | undefined) {
      return (this as APIImpl.Resource)._buildId;
    },

    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?.(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, commit},
          callback as (response: unknown) => unknown);
    },

    setFunctionRangesForScript: function(
        this: APIImpl.Resource, ranges: PublicAPI.Chrome.DevTools.NamedFunctionRange[]): Promise<void> {
      return new Promise(
          (resolve, reject) => extensionServer.sendRequest(
              {
                command: PrivateAPI.Commands.SetFunctionRangesForScript,
                scriptUrl: this._url,
                ranges,
              },
              (response: unknown) => {
                const result = response as {
                  code: string,
                  description: string,
                  details: unknown[],
                  isError?: boolean,
                };
                if (result.isError) {
                  reject(result);
                } else {
                  resolve();
                }
              }));
    },

    attachSourceMapURL: function(this: APIImpl.Resource, sourceMapURL: string): Promise<void> {
      return new Promise(
          (resolve, reject) => extensionServer.sendRequest(
              {command: PrivateAPI.Commands.AttachSourceMapToResource, contentUrl: this._url, sourceMapURL},
              (response: unknown) => {
                const result = response as {
                  code: string,
                  description: string,
                  details: unknown[],
                  isError?: boolean,
                };
                if (result.isError) {
                  reject(new Error(result.description));
                } else {
                  resolve();
                }
              }));
    },
  };

  function getTabId(): string {
    return inspectedTabId;
  }

  let keyboardEventRequestQueue: KeyboardEventInit&Array<{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' || (focused as HTMLElement).isContentEditable;
      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: Record<string, unknown>, implementation: Record<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 Record<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;
  chrome.devtools.performance = coreAPI.performance;

  // 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);' +
      '})';
};
