// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable @devtools/no-imperative-dom-api */
/* eslint-disable @devtools/no-lit-render-outside-of-view */

/*
 * Copyright (C) 2006, 2007, 2008 Apple Inc.  All rights reserved.
 * Copyright (C) 2007 Matt Lilek (pewtermoose@gmail.com).
 * Copyright (C) 2009 Joseph Pecoraro
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1.  Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 * 2.  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.
 * 3.  Neither the name of Apple Computer, Inc. ("Apple") 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 APPLE AND ITS 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 APPLE OR ITS 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 Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as ProtocolClient from '../../core/protocol_client/protocol_client.js';
import * as Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Foundation from '../../foundation/foundation.js';
import * as AiAssistanceModel from '../../models/ai_assistance/ai_assistance.js';
import * as AutofillManager from '../../models/autofill_manager/autofill_manager.js';
import * as Badges from '../../models/badges/badges.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as Breakpoints from '../../models/breakpoints/breakpoints.js';
import * as CrUXManager from '../../models/crux-manager/crux-manager.js';
import * as IssuesManager from '../../models/issues_manager/issues_manager.js';
import * as LiveMetrics from '../../models/live-metrics/live-metrics.js';
import * as Logs from '../../models/logs/logs.js';
import * as Persistence from '../../models/persistence/persistence.js';
import * as ProjectSettings from '../../models/project_settings/project_settings.js';
import * as Workspace from '../../models/workspace/workspace.js';
import * as PanelCommon from '../../panels/common/common.js';
import * as Snippets from '../../panels/snippets/snippets.js';
import * as Buttons from '../../ui/components/buttons/buttons.js';
import * as Snackbar from '../../ui/components/snackbars/snackbars.js';
import * as UIHelpers from '../../ui/helpers/helpers.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js';
import {html, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';

import {ExecutionContextSelector} from './ExecutionContextSelector.js';

const UIStrings = {
  /**
   * @description Title of item in main
   */
  customizeAndControlDevtools: 'Customize and control DevTools',
  /**
   * @description Title element text content in Main
   */
  dockSide: 'Dock side',
  /**
   * @description Title element title in Main
   * @example {Ctrl+Shift+D} PH1
   */
  placementOfDevtoolsRelativeToThe: 'Placement of DevTools relative to the page. ({PH1} to restore last position)',
  /**
   * @description Text to undock the DevTools
   */
  undockIntoSeparateWindow: 'Undock into separate window',
  /**
   * @description Text to dock the DevTools to the bottom of the browser tab
   */
  dockToBottom: 'Dock to bottom',
  /**
   * @description Text to dock the DevTools to the right of the browser tab
   */
  dockToRight: 'Dock to right',
  /**
   * @description Text to dock the DevTools to the left of the browser tab
   */
  dockToLeft: 'Dock to left',
  /**
   * @description Text in Main
   */
  focusDebuggee: 'Focus page',
  /**
   * @description Text in Main
   */
  hideConsoleDrawer: 'Hide console drawer',
  /**
   * @description Text in Main
   */
  showConsoleDrawer: 'Show console drawer',
  /**
   * @description A context menu item in the Main
   */
  moreTools: 'More tools',
  /**
   * @description Text for the viewing the help options
   */
  help: 'Help',
  /**
   * @description Text describing how to navigate the dock side menu
   */
  dockSideNavigation: 'Use left and right arrow keys to navigate the options',
  /**
   * @description Notification shown to the user whenever DevTools has finished downloading a local AI model.
   */
  aiModelDownloaded: 'AI model downloaded',
  /**
   * @description A title of the menu item in the main menu leading to https://github.com/ChromeDevTools/chrome-devtools-mcp.
   */
  getDevToolsMcp: 'Get `DevTools MCP`'
} as const;
const str_ = i18n.i18n.registerUIStrings('entrypoints/main/MainImpl.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

let loadedPanelCommonModule: typeof PanelCommon|undefined;

const WINDOW_LOCAL_STORAGE: Common.Settings.SettingsBackingStore = {
  register(_setting: string): void{},
  async get(setting: string): Promise<string> {
    return window.localStorage.getItem(setting) as unknown as string;
  },
  set(setting: string, value: string): void {
    window.localStorage.setItem(setting, value);
  },
  remove(setting: string): void {
    window.localStorage.removeItem(setting);
  },
  clear: () => window.localStorage.clear(),
};

export class MainImpl {
  #readyForTestPromise = Promise.withResolvers<void>();
  #veStartPromise!: Promise<void>;
  #universe!: Foundation.Universe.Universe;

  constructor() {
    MainImpl.instanceForTest = this;
    void this.#loaded();
  }

  static time(label: string): void {
    if (Host.InspectorFrontendHost.isUnderTest()) {
      return;
    }
    console.time(label);
  }

  static timeEnd(label: string): void {
    if (Host.InspectorFrontendHost.isUnderTest()) {
      return;
    }
    console.timeEnd(label);
  }

  async #loaded(): Promise<void> {
    console.timeStamp('Main._loaded');
    Root.Runtime.Runtime.setPlatform(Host.Platform.platform());
    const [config, prefs] = await Promise.all([
      new Promise<Root.Runtime.HostConfig>(resolve => {
        Host.InspectorFrontendHost.InspectorFrontendHostInstance.getHostConfig(resolve);
      }),
      new Promise<Record<string, string>>(
          resolve => Host.InspectorFrontendHost.InspectorFrontendHostInstance.getPreferences(resolve)),
    ]);

    console.timeStamp('Main._gotPreferences');
    this.#initializeGlobalsForLayoutTests();
    Object.assign(Root.Runtime.hostConfig, config);

    const creationOptions: Foundation.Universe.CreationOptions = {
      settingsCreationOptions: {
        ...this.createSettingsStorage(prefs),
        settingRegistrations: Common.SettingRegistration.getRegisteredSettings(),
        logSettingAccess: VisualLogging.logSettingAccess,
        runSettingsMigration: !Host.InspectorFrontendHost.isUnderTest(),
      },
    };
    this.#universe = new Foundation.Universe.Universe(creationOptions);
    Root.DevToolsContext.setGlobalInstance(this.#universe.context as Root.DevToolsContext.WritableDevToolsContext);

    Root.Runtime.experiments.cleanUpStaleExperiments();

    await this.requestAndRegisterLocaleData();

    Host.userMetrics.syncSetting(Common.Settings.Settings.instance().moduleSetting<boolean>('sync-preferences').get());
    const veLogging = config.devToolsVeLogging;

    // Used by e2e to put VE Logs into "test mode".
    const veLogsTestMode = Common.Settings.Settings.instance().createSetting('veLogsTestMode', false).get();

    if (veLogging?.enabled) {
      // Note: as of https://crrev.com/c/6734500 landing, veLogging.testing is hard-coded to false.
      // But the e2e tests (test/conductor/frontend_tab.ts) use this to enable this flag for e2e tests.
      // TODO(crbug.com/432411398): remove the host config for VE logs + find a better way to set this up in e2e tests.
      if (veLogging?.testing || veLogsTestMode) {
        VisualLogging.setVeDebugLoggingEnabled(true, VisualLogging.DebugLoggingFormat.TEST);
        const options = {
          processingThrottler: new Common.Throttler.Throttler(0),
          keyboardLogThrottler: new Common.Throttler.Throttler(10),
          hoverLogThrottler: new Common.Throttler.Throttler(50),
          dragLogThrottler: new Common.Throttler.Throttler(50),
          clickLogThrottler: new Common.Throttler.Throttler(10),
          resizeLogThrottler: new Common.Throttler.Throttler(10),
        };
        this.#veStartPromise = VisualLogging.startLogging(options);
      } else {
        this.#veStartPromise = VisualLogging.startLogging();
      }
    }

    void this.#createAppUI();
  }

  #initializeGlobalsForLayoutTests(): void {
    // @ts-expect-error e2e test global
    self.Extensions ||= {};
    // @ts-expect-error e2e test global
    self.Host ||= {};
    // @ts-expect-error e2e test global
    self.Host.userMetrics ||= Host.userMetrics;
    // @ts-expect-error e2e test global
    self.Host.UserMetrics ||= Host.UserMetrics;
    // @ts-expect-error e2e test global
    self.ProtocolClient ||= {};
    // @ts-expect-error e2e test global
    self.ProtocolClient.test ||= ProtocolClient.InspectorBackend.test;
  }

  async requestAndRegisterLocaleData(): Promise<void> {
    const settingLanguage = Common.Settings.Settings.instance().moduleSetting<string>('language').get();
    const devToolsLocale = i18n.DevToolsLocale.DevToolsLocale.instance({
      create: true,
      data: {
        navigatorLanguage: navigator.language,
        settingLanguage,
        lookupClosestDevToolsLocale: i18n.i18n.lookupClosestSupportedDevToolsLocale,
      },
    });
    // Record the intended locale, regardless whether we are able to fetch it or not.
    Host.userMetrics.language(devToolsLocale.locale);

    if (devToolsLocale.locale !== 'en-US') {
      // Always load en-US locale data as a fallback. This is important, newly added
      // strings won't have a translation. If fetching en-US.json fails, something
      // is seriously wrong and the exception should bubble up.
      await i18n.i18n.fetchAndRegisterLocaleData('en-US');
    }

    try {
      await i18n.i18n.fetchAndRegisterLocaleData(devToolsLocale.locale);
    } catch (error) {
      console.warn(
          `Unable to fetch & register locale data for '${devToolsLocale.locale}', falling back to 'en-US'. Cause: `,
          error);
      // Loading the actual locale data failed, tell DevTools to use 'en-US'.
      devToolsLocale.forceFallbackLocale();
    }
  }

  createSettingsStorage(prefs: Record<string, string>): {
    syncedStorage: Common.Settings.SettingsStorage,
    globalStorage: Common.Settings.SettingsStorage,
    localStorage: Common.Settings.SettingsStorage,
  } {
    this.#initializeExperiments();
    let storagePrefix = '';
    if (Host.Platform.isCustomDevtoolsFrontend()) {
      storagePrefix = '__custom__';
    } else if (
        !Root.Runtime.Runtime.queryParam('can_dock') && Boolean(Root.Runtime.Runtime.queryParam('debugFrontend')) &&
        !Host.InspectorFrontendHost.isUnderTest()) {
      storagePrefix = '__bundled__';
    }

    let localStorage: Common.Settings.SettingsStorage;
    if (!Host.InspectorFrontendHost.isUnderTest() && window.localStorage) {
      localStorage = new Common.Settings.SettingsStorage(window.localStorage, WINDOW_LOCAL_STORAGE, storagePrefix);
    } else {
      localStorage = new Common.Settings.SettingsStorage({}, undefined, storagePrefix);
    }

    const hostUnsyncedStorage: Common.Settings.SettingsBackingStore = {
      register: (name: string) =>
          Host.InspectorFrontendHost.InspectorFrontendHostInstance.registerPreference(name, {synced: false}),
      set: Host.InspectorFrontendHost.InspectorFrontendHostInstance.setPreference,
      get: (name: string) => {
        return new Promise(resolve => {
          Host.InspectorFrontendHost.InspectorFrontendHostInstance.getPreference(name, resolve);
        });
      },
      remove: Host.InspectorFrontendHost.InspectorFrontendHostInstance.removePreference,
      clear: Host.InspectorFrontendHost.InspectorFrontendHostInstance.clearPreferences,
    };
    const hostSyncedStorage: Common.Settings.SettingsBackingStore = {
      ...hostUnsyncedStorage,
      register: (name: string) =>
          Host.InspectorFrontendHost.InspectorFrontendHostInstance.registerPreference(name, {synced: true}),
    };
    // `prefs` is retrieved via `getPreferences` host binding and contains both synced and unsynced settings.
    // As such, we use `prefs` to initialize both the synced and the global storage. This is fine as an individual
    // setting can't change storage buckets during a single DevTools session.
    const syncedStorage = new Common.Settings.SettingsStorage(prefs, hostSyncedStorage, storagePrefix);
    const globalStorage = new Common.Settings.SettingsStorage(prefs, hostUnsyncedStorage, storagePrefix);

    return {syncedStorage, globalStorage, localStorage};
  }

  #migrateValueFromLegacyToHostExperiment(
      legacyExperimentName: Root.ExperimentNames.ExperimentName, hostExperiment: Root.Runtime.HostExperiment): void {
    const value = Root.Runtime.experiments.getValueFromStorage(legacyExperimentName);
    if (value !== undefined && hostExperiment.aboutFlag) {
      // Set the host experiment to the same value as the legacy experiment.
      hostExperiment.setEnabled(value);
      // Set the chrome flag to the same value as the legacy experiment.
      Host.InspectorFrontendHost.InspectorFrontendHostInstance.setChromeFlag(hostExperiment.aboutFlag, value);
      // The legacy experiment will be cleaned up by `cleanUpStaleExperiments`.
    }
  }

  #initializeExperiments(): void {
    Root.Runtime.experiments.register(
        Root.ExperimentNames.ExperimentName.CAPTURE_NODE_CREATION_STACKS, 'Capture node creation stacks');
    Root.Runtime.experiments.register(Root.ExperimentNames.ExperimentName.LIVE_HEAP_PROFILE, 'Live heap profile');

    const enableProtocolMonitor = (Root.Runtime.hostConfig.devToolsProtocolMonitor?.enabled ?? false) ||
        Boolean(Root.Runtime.Runtime.queryParam('isChromeForTesting'));
    const protocolMonitorExperiment = Root.Runtime.experiments.registerHostExperiment({
      name: Root.ExperimentNames.ExperimentName.PROTOCOL_MONITOR,
      title: 'Protocol Monitor',
      aboutFlag: 'devtools-protocol-monitor',
      isEnabled: enableProtocolMonitor,
      requiresChromeRestart: false,
      docLink: 'https://developer.chrome.com/blog/new-in-devtools-92/#protocol-monitor' as
          Platform.DevToolsPath.UrlString,
    });
    this.#migrateValueFromLegacyToHostExperiment(
        Root.ExperimentNames.ExperimentName.PROTOCOL_MONITOR, protocolMonitorExperiment);
    Root.Runtime.experiments.register(
        Root.ExperimentNames.ExperimentName.SAMPLING_HEAP_PROFILER_TIMELINE, 'Sampling heap profiler timeline');

    // Timeline
    Root.Runtime.experiments.register(
        Root.ExperimentNames.ExperimentName.TIMELINE_INVALIDATION_TRACKING, 'Performance panel: invalidation tracking');
    Root.Runtime.experiments.register(
        Root.ExperimentNames.ExperimentName.TIMELINE_DEBUG_MODE,
        'Performance panel: debug mode (trace event details, etc)');

    // Debugging
    Root.Runtime.experiments.register(
        Root.ExperimentNames.ExperimentName.INSTRUMENTATION_BREAKPOINTS, 'Instrumentation breakpoints');
    Root.Runtime.experiments.register(
        Root.ExperimentNames.ExperimentName.USE_SOURCE_MAP_SCOPES, 'Use scope information from source maps');

    // Font Editor
    Root.Runtime.experiments.register(
        Root.ExperimentNames.ExperimentName.FONT_EDITOR, 'New font editor in the Styles tab',
        'https://developer.chrome.com/blog/new-in-devtools-89/#font');

    Root.Runtime.experiments.registerHostExperiment({
      name: Root.ExperimentNames.ExperimentName.DURABLE_MESSAGES,
      title: 'Durable Messages',
      aboutFlag: 'devtools-enable-durable-messages',
      isEnabled: Root.Runtime.hostConfig.devToolsEnableDurableMessages?.enabled ?? false,
      requiresChromeRestart: false,
    });

    Root.Runtime.experiments.registerHostExperiment({
      name: Root.ExperimentNames.ExperimentName.JPEG_XL,
      title: 'JPEG XL support',
      aboutFlag: 'enable-jxl-image-format',
      isEnabled: Root.Runtime.hostConfig.devToolsJpegXlImageFormat?.enabled ?? false,
      requiresChromeRestart: true,
    });

    Root.Runtime.experiments.enableExperimentsByDefault([
      Root.ExperimentNames.ExperimentName.USE_SOURCE_MAP_SCOPES,
    ]);

    const enabledExperiments = Root.Runtime.Runtime.queryParam('enabledExperiments');
    if (enabledExperiments) {
      Root.Runtime.experiments.setServerEnabledExperiments(enabledExperiments.split(';'));
    }

    if (Host.InspectorFrontendHost.isUnderTest()) {
      const testParam = Root.Runtime.Runtime.queryParam('test');
      if (testParam?.includes('live-line-level-heap-profile.js')) {
        Root.Runtime.experiments.enableForTest(Root.ExperimentNames.ExperimentName.LIVE_HEAP_PROFILE);
      }
    }

    for (const experiment of Root.Runtime.experiments.allConfigurableExperiments()) {
      if (experiment.isEnabled()) {
        Host.userMetrics.experimentEnabledAtLaunch(experiment.name);
      } else {
        Host.userMetrics.experimentDisabledAtLaunch(experiment.name);
      }
    }
  }

  async #createAppUI(): Promise<void> {
    MainImpl.time('Main._createAppUI');

    // Request filesystems early, we won't create connections until callback is fired. Things will happen in parallel.
    const isolatedFileSystemManager = Persistence.IsolatedFileSystemManager.IsolatedFileSystemManager.instance();
    isolatedFileSystemManager.addEventListener(
        Persistence.IsolatedFileSystemManager.Events.FileSystemError,
        event => Snackbar.Snackbar.Snackbar.show({message: event.data}));

    const defaultThemeSetting = 'systemPreferred';
    const themeSetting = Common.Settings.Settings.instance().createSetting('ui-theme', defaultThemeSetting);
    UI.UIUtils.initializeUIUtils(document);

    // Initialize theme support and apply it.
    if (!ThemeSupport.ThemeSupport.hasInstance()) {
      ThemeSupport.ThemeSupport.instance({forceNew: true, setting: themeSetting});
    }

    UI.UIUtils.addPlatformClass(document.documentElement);
    UI.UIUtils.installComponentRootStyles(document.body);

    this.#addMainEventListeners(document);

    const canDock = Boolean(Root.Runtime.Runtime.queryParam('can_dock'));
    UI.ZoomManager.ZoomManager.instance(
        {forceNew: true, win: window, frontendHost: Host.InspectorFrontendHost.InspectorFrontendHostInstance});
    UI.ContextMenu.ContextMenu.initialize();
    UI.ContextMenu.ContextMenu.installHandler(document);
    UI.ViewManager.ViewManager.instance({forceNew: true, universe: this.#universe});

    // These instances need to be created early so they don't miss any events about requests/issues/etc.
    Logs.NetworkLog.NetworkLog.instance();
    Logs.LogManager.LogManager.instance();
    IssuesManager.IssuesManager.IssuesManager.instance({
      forceNew: true,
      ensureFirst: true,
      showThirdPartyIssuesSetting: IssuesManager.Issue.getShowThirdPartyIssuesSetting(),
      hideIssueSetting: IssuesManager.IssuesManager.getHideIssueByCodeSetting(),
    });

    UI.DockController.DockController.instance({forceNew: true, canDock});
    SDK.DOMDebuggerModel.DOMDebuggerManager.instance({forceNew: true});
    const targetManager = SDK.TargetManager.TargetManager.instance();
    targetManager.addEventListener(
        SDK.TargetManager.Events.SUSPEND_STATE_CHANGED, this.#onSuspendStateChanged.bind(this));

    Workspace.FileManager.FileManager.instance({forceNew: true});

    Bindings.NetworkProject.NetworkProjectManager.instance();
    new Bindings.PresentationConsoleMessageHelper.PresentationConsoleMessageManager();
    targetManager.setScopeTarget(targetManager.primaryPageTarget());
    UI.Context.Context.instance().addFlavorChangeListener(SDK.Target.Target, ({data}) => {
      const outermostTarget = data?.outermostTarget();
      targetManager.setScopeTarget(outermostTarget);
    });
    Breakpoints.BreakpointManager.BreakpointManager.instance({
      forceNew: true,
      workspace: Workspace.Workspace.WorkspaceImpl.instance(),
      targetManager,
      debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance(),
      settings: Common.Settings.Settings.instance(),
    });
    // @ts-expect-error e2e test global
    self.Extensions.extensionServer = PanelCommon.ExtensionServer.ExtensionServer.instance({forceNew: true});

    new Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding(
        isolatedFileSystemManager, Workspace.Workspace.WorkspaceImpl.instance());
    isolatedFileSystemManager.addPlatformFileSystem(
        'snippet://' as Platform.DevToolsPath.UrlString, new Snippets.ScriptSnippetFileSystem.SnippetFileSystem());

    const persistenceImpl = Persistence.Persistence.PersistenceImpl.instance({
      forceNew: true,
      workspace: Workspace.Workspace.WorkspaceImpl.instance(),
      breakpointManager: Breakpoints.BreakpointManager.BreakpointManager.instance(),
    });
    const linkDecorator = new PanelCommon.PersistenceUtils.LinkDecorator(persistenceImpl);
    Components.Linkifier.Linkifier.setLinkDecorator(linkDecorator);
    Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance(
        {forceNew: true, workspace: Workspace.Workspace.WorkspaceImpl.instance()});

    new ExecutionContextSelector(targetManager, UI.Context.Context.instance());

    const projectSettingsModel = ProjectSettings.ProjectSettingsModel.ProjectSettingsModel.instance({
      forceNew: true,
      hostConfig: Root.Runtime.hostConfig,
      pageResourceLoader: SDK.PageResourceLoader.PageResourceLoader.instance(),
      targetManager,
    });

    const automaticFileSystemManager = Persistence.AutomaticFileSystemManager.AutomaticFileSystemManager.instance({
      forceNew: true,
      inspectorFrontendHost: Host.InspectorFrontendHost.InspectorFrontendHostInstance,
      projectSettingsModel,
    });
    Persistence.AutomaticFileSystemWorkspaceBinding.AutomaticFileSystemWorkspaceBinding.instance({
      forceNew: true,
      automaticFileSystemManager,
      isolatedFileSystemManager,
      workspace: Workspace.Workspace.WorkspaceImpl.instance(),
    });

    AutofillManager.AutofillManager.AutofillManager.instance();

    LiveMetrics.LiveMetrics.instance();
    CrUXManager.CrUXManager.instance();

    const builtInAi = AiAssistanceModel.BuiltInAi.BuiltInAi.instance();
    builtInAi.addEventListener(
        AiAssistanceModel.BuiltInAi.Events.DOWNLOADED_AND_SESSION_CREATED,
        () => Snackbar.Snackbar.Snackbar.show({message: i18nString(UIStrings.aiModelDownloaded)}));

    new PauseListener();

    const actionRegistryInstance = UI.ActionRegistry.ActionRegistry.instance({forceNew: true});
    // Required for legacy a11y layout tests
    UI.ShortcutRegistry.ShortcutRegistry.instance({forceNew: true, actionRegistry: actionRegistryInstance});
    this.#registerMessageSinkListener();

    // Initialize `GDPClient` and `UserBadges` for Google Developer Program integration
    if (Host.GdpClient.isGdpProfilesAvailable()) {
      void Host.GdpClient.GdpClient.instance().getProfile().then(getProfileResponse => {
        if (!getProfileResponse) {
          return;
        }

        const {profile, isEligible} = getProfileResponse;
        const hasProfile = Boolean(profile);
        const contextString = hasProfile ? 'has-profile' :
            isEligible                   ? 'no-profile-and-eligible' :
                                           'no-profile-and-not-eligible';
        void VisualLogging.logFunctionCall('gdp-client-initialize', contextString);
      });
      void Badges.UserBadges.instance().initialize();
      Badges.UserBadges.instance().addEventListener(Badges.Events.BADGE_TRIGGERED, async ev => {
        loadedPanelCommonModule ??= await import('../../panels/common/common.js') as typeof PanelCommon;
        const badgeNotification = new loadedPanelCommonModule.BadgeNotification();
        const {badge, reason} = ev.data;
        void badgeNotification.present(badge, reason);
      });
    }

    if (Root.Runtime.hostConfig.devToolsGeminiRebranding?.enabled) {
      await PanelCommon.GeminiRebrandPromoDialog.maybeShow();
    }

    MainImpl.timeEnd('Main._createAppUI');

    const appProvider = Common.AppProvider.getRegisteredAppProviders()[0];
    if (!appProvider) {
      throw new Error('Unable to boot DevTools, as the appprovider is missing');
    }
    await this.#showAppUI(await appProvider.loadAppProvider());
  }

  async #showAppUI(appProvider: Object): Promise<void> {
    MainImpl.time('Main._showAppUI');
    const app = (appProvider as Common.AppProvider.AppProvider).createApp();
    // It is important to kick controller lifetime after apps are instantiated.
    UI.DockController.DockController.instance().initialize();
    ThemeSupport.ThemeSupport.instance().fetchColorsAndApplyHostTheme();
    app.presentUI(document);

    if (UI.ActionRegistry.ActionRegistry.instance().hasAction('elements.toggle-element-search')) {
      const toggleSearchNodeAction =
          UI.ActionRegistry.ActionRegistry.instance().getAction('elements.toggle-element-search');
      // TODO: we should not access actions from other modules.
      Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.addEventListener(
          Host.InspectorFrontendHostAPI.Events.EnterInspectElementMode, () => {
            void toggleSearchNodeAction.execute();
          }, this);
    }
    Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.addEventListener(
        Host.InspectorFrontendHostAPI.Events.RevealSourceLine, this.#revealSourceLine, this);

    const inspectorView = UI.InspectorView.InspectorView.instance();
    Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance().addEventListener(
        Persistence.NetworkPersistenceManager.Events.LOCAL_OVERRIDES_REQUESTED, event => {
          inspectorView.displaySelectOverrideFolderInfobar(event.data);
        });
    await inspectorView.createToolbars();
    Host.InspectorFrontendHost.InspectorFrontendHostInstance.loadCompleted();

    // Initialize elements for the live announcer functionality for a11y.
    UI.ARIAUtils.LiveAnnouncer.initializeAnnouncerElements();
    UI.DockController.DockController.instance().announceDockLocation();

    // Allow UI cycles to repaint prior to creating connection.
    window.setTimeout(this.#initializeTarget.bind(this), 0);
    MainImpl.timeEnd('Main._showAppUI');
  }

  async #initializeTarget(): Promise<void> {
    MainImpl.time('Main._initializeTarget');

    // We rely on having the early initialization runnables registered in Common when an app loads its
    // modules, so that we don't have to exhaustively check the app DevTools is running as to
    // start the applicable runnables.
    for (const runnableInstanceFunction of Common.Runnable.earlyInitializationRunnables()) {
      await runnableInstanceFunction().run();
    }
    await this.#veStartPromise;
    // Used for browser tests.
    Host.InspectorFrontendHost.InspectorFrontendHostInstance.readyForTest();
    this.#readyForTestPromise.resolve();
    // Asynchronously run the extensions.
    window.setTimeout(this.#lateInitialization.bind(this), 100);
    await this.#maybeInstallVeInspectionBinding();

    MainImpl.timeEnd('Main._initializeTarget');
  }

  async #maybeInstallVeInspectionBinding(): Promise<void> {
    const primaryPageTarget = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
    const url = primaryPageTarget?.targetInfo()?.url;
    const origin = url ? Common.ParsedURL.ParsedURL.extractOrigin(url as Platform.DevToolsPath.UrlString) : undefined;

    const binding = '__devtools_ve_inspection_binding__';
    if (primaryPageTarget && await VisualLogging.isUnderInspection(origin)) {
      const runtimeModel = primaryPageTarget.model(SDK.RuntimeModel.RuntimeModel);
      await runtimeModel?.addBinding({name: binding});
      runtimeModel?.addEventListener(SDK.RuntimeModel.Events.BindingCalled, event => {
        if (event.data.name === binding) {
          if (event.data.payload === 'true' || event.data.payload === 'false') {
            VisualLogging.setVeDebuggingEnabled(event.data.payload === 'true', (query: string) => {
              VisualLogging.setVeDebuggingEnabled(false);
              void runtimeModel?.defaultExecutionContext()?.evaluate(
                  {
                    expression: `window.inspect(${JSON.stringify(query)})`,
                    includeCommandLineAPI: false,
                    silent: true,
                    returnByValue: false,
                    generatePreview: false,
                  },
                  /* userGesture */ false,
                  /* awaitPromise */ false);
            });
          } else {
            VisualLogging.setHighlightedVe(event.data.payload === 'null' ? null : event.data.payload);
          }
        }
      });
    }
  }

  async #lateInitialization(): Promise<void> {
    MainImpl.time('Main._lateInitialization');
    PanelCommon.ExtensionServer.ExtensionServer.instance().initializeExtensions();
    const promises: Array<Promise<void>> =
        Common.Runnable.lateInitializationRunnables().map(async lateInitializationLoader => {
          const runnable = await lateInitializationLoader();
          return await runnable.run();
        });
    if (Root.Runtime.experiments.isEnabled(Root.ExperimentNames.ExperimentName.LIVE_HEAP_PROFILE)) {
      const PerfUI = await import('../../ui/legacy/components/perf_ui/perf_ui.js');
      const setting = 'memory-live-heap-profile';
      if (Common.Settings.Settings.instance().moduleSetting(setting).get()) {
        promises.push(PerfUI.LiveHeapProfile.LiveHeapProfile.instance().run());
      } else {
        const changeListener = async(event: Common.EventTarget.EventTargetEvent<unknown>): Promise<void> => {
          if (!event.data) {
            return;
          }
          Common.Settings.Settings.instance().moduleSetting(setting).removeChangeListener(changeListener);
          void PerfUI.LiveHeapProfile.LiveHeapProfile.instance().run();
        };
        Common.Settings.Settings.instance().moduleSetting(setting).addChangeListener(changeListener);
      }
    }

    MainImpl.timeEnd('Main._lateInitialization');
  }

  readyForTest(): Promise<void> {
    return this.#readyForTestPromise.promise;
  }

  #registerMessageSinkListener(): void {
    Common.Console.Console.instance().addEventListener(Common.Console.Events.MESSAGE_ADDED, messageAdded);

    function messageAdded({data: message}: Common.EventTarget.EventTargetEvent<Common.Console.Message>): void {
      if (message.show) {
        Common.Console.Console.instance().show();
      }
    }
  }

  #revealSourceLine(event: Common.EventTarget.EventTargetEvent<Host.InspectorFrontendHostAPI.RevealSourceLineEvent>):
      void {
    const {url, lineNumber, columnNumber} = event.data;
    const uiSourceCode = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(url);
    if (uiSourceCode) {
      void Common.Revealer.reveal(uiSourceCode.uiLocation(lineNumber, columnNumber));
      return;
    }

    function listener(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void {
      const uiSourceCode = event.data;
      if (uiSourceCode.url() === url) {
        void Common.Revealer.reveal(uiSourceCode.uiLocation(lineNumber, columnNumber));
        Workspace.Workspace.WorkspaceImpl.instance().removeEventListener(
            Workspace.Workspace.Events.UISourceCodeAdded, listener);
      }
    }

    Workspace.Workspace.WorkspaceImpl.instance().addEventListener(
        Workspace.Workspace.Events.UISourceCodeAdded, listener);
  }

  #postDocumentKeyDown(event: Event): void {
    if (!event.handled) {
      UI.ShortcutRegistry.ShortcutRegistry.instance().handleShortcut((event as KeyboardEvent));
    }
  }

  #redispatchClipboardEvent(event: Event): void {
    const eventCopy = new CustomEvent('clipboard-' + event.type, {bubbles: true});
    // @ts-expect-error Used in ElementsTreeOutline
    eventCopy['original'] = event;
    const document = event.target && (event.target as HTMLElement).ownerDocument;
    const target = document ? UI.DOMUtilities.deepActiveElement(document) : null;
    if (target) {
      target.dispatchEvent(eventCopy);
    }
    if (eventCopy.handled) {
      event.preventDefault();
    }
  }

  #contextMenuEventFired(event: Event): void {
    if (event.handled || (event.target as HTMLElement).classList.contains('popup-glasspane')) {
      event.preventDefault();
    }
  }

  #addMainEventListeners(document: Document): void {
    document.addEventListener('keydown', this.#postDocumentKeyDown.bind(this), false);
    document.addEventListener('beforecopy', this.#redispatchClipboardEvent.bind(this), true);
    document.addEventListener('copy', this.#redispatchClipboardEvent.bind(this), false);
    document.addEventListener('cut', this.#redispatchClipboardEvent.bind(this), false);
    document.addEventListener('paste', this.#redispatchClipboardEvent.bind(this), false);
    document.addEventListener('contextmenu', this.#contextMenuEventFired.bind(this), true);
  }

  #onSuspendStateChanged(): void {
    const suspended = SDK.TargetManager.TargetManager.instance().allTargetsSuspended();
    UI.InspectorView.InspectorView.instance().onSuspendStateChanged(suspended);
  }

  static instanceForTest: MainImpl|null = null;
}

// @ts-expect-error Exported for Tests.js
globalThis.Main = globalThis.Main || {};
// @ts-expect-error Exported for Tests.js
globalThis.Main.Main = MainImpl;

export class ZoomActionDelegate implements UI.ActionRegistration.ActionDelegate {
  handleAction(_context: UI.Context.Context, actionId: string): boolean {
    if (Host.InspectorFrontendHost.InspectorFrontendHostInstance.isHostedMode()) {
      return false;
    }

    switch (actionId) {
      case 'main.zoom-in':
        Host.InspectorFrontendHost.InspectorFrontendHostInstance.zoomIn();
        return true;
      case 'main.zoom-out':
        Host.InspectorFrontendHost.InspectorFrontendHostInstance.zoomOut();
        return true;
      case 'main.zoom-reset':
        Host.InspectorFrontendHost.InspectorFrontendHostInstance.resetZoom();
        return true;
    }
    return false;
  }
}

export class SearchActionDelegate implements UI.ActionRegistration.ActionDelegate {
  handleAction(_context: UI.Context.Context, actionId: string): boolean {
    let searchableView = UI.SearchableView.SearchableView.fromElement(
        UI.DOMUtilities.deepActiveElement(document),
    );
    if (!searchableView) {
      const currentPanel = (UI.InspectorView.InspectorView.instance().currentPanelDeprecated() as UI.Panel.Panel);
      if (currentPanel?.searchableView) {
        searchableView = currentPanel.searchableView();
      }
      if (!searchableView) {
        return false;
      }
    }
    switch (actionId) {
      case 'main.search-in-panel.find':
        return searchableView.handleFindShortcut();
      case 'main.search-in-panel.cancel':
        return searchableView.handleCancelSearchShortcut();
      case 'main.search-in-panel.find-next':
        return searchableView.handleFindNextShortcut();
      case 'main.search-in-panel.find-previous':
        return searchableView.handleFindPreviousShortcut();
    }
    return false;
  }
}
let mainMenuItemInstance: MainMenuItem;

export class MainMenuItem implements UI.Toolbar.Provider {
  readonly #item: UI.Toolbar.ToolbarMenuButton;
  constructor() {
    this.#item = new UI.Toolbar.ToolbarMenuButton(
        this.#handleContextMenu.bind(this), /* isIconDropdown */ true,
        /* useSoftMenu */ true, 'main-menu', 'dots-vertical');
    this.#item.element.classList.add('main-menu');
    this.#item.setTitle(i18nString(UIStrings.customizeAndControlDevtools));
  }

  static instance(opts: {
    forceNew: boolean|null,
  } = {forceNew: null}): MainMenuItem {
    const {forceNew} = opts;
    if (!mainMenuItemInstance || forceNew) {
      mainMenuItemInstance = new MainMenuItem();
    }

    return mainMenuItemInstance;
  }

  item(): UI.Toolbar.ToolbarItem|null {
    return this.#item;
  }

  #handleContextMenu(contextMenu: UI.ContextMenu.ContextMenu): void {
    const dockController = UI.DockController.DockController.instance();
    if (dockController.canDock()) {
      const dockItemElement = document.createElement('div');
      dockItemElement.classList.add('flex-auto', 'flex-centered', 'location-menu');
      dockItemElement.setAttribute(
          'jslog', `${VisualLogging.item('dock-side').track({keydown: 'ArrowDown|ArrowLeft|ArrowRight'})}`);
      dockItemElement.tabIndex = -1;
      UI.ARIAUtils.setLabel(dockItemElement, UIStrings.dockSide + UIStrings.dockSideNavigation);
      const [toggleDockSideShortcut] =
          UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutsForAction('main.toggle-dock');

      // clang-format off
      render(html`
        <span class="dockside-title"
              title=${i18nString(UIStrings.placementOfDevtoolsRelativeToThe, {PH1: toggleDockSideShortcut.title()})}>
          ${i18nString(UIStrings.dockSide)}
        </span>
        <devtools-toolbar @mousedown=${(event: Event) => event.consume()}>
          <devtools-button class="toolbar-button"
                           jslog=${VisualLogging.toggle().track({click: true}).context('current-dock-state-undock')}
                           title=${i18nString(UIStrings.undockIntoSeparateWindow)}
                           aria-label=${i18nString(UIStrings.undockIntoSeparateWindow)}
                           .iconName=${'dock-window'}
                           .toggled=${dockController.dockSide() === UI.DockController.DockState.UNDOCKED}
                           .toggledIconName=${'dock-window'}
                           .toggleType=${Buttons.Button.ToggleType.PRIMARY}
                           .variant=${Buttons.Button.Variant.ICON_TOGGLE}
                           @click=${setDockSide.bind(null, UI.DockController.DockState.UNDOCKED)}></devtools-button>
          <devtools-button class="toolbar-button"
                           jslog=${VisualLogging.toggle().track({click: true}).context('current-dock-state-left')}
                           title=${i18nString(UIStrings.dockToLeft)}
                           aria-label=${i18nString(UIStrings.dockToLeft)}
                           .iconName=${'dock-left'}
                           .toggled=${dockController.dockSide() === UI.DockController.DockState.LEFT}
                           .toggledIconName=${'dock-left'}
                           .toggleType=${Buttons.Button.ToggleType.PRIMARY}
                           .variant=${Buttons.Button.Variant.ICON_TOGGLE}
                           @click=${setDockSide.bind(null, UI.DockController.DockState.LEFT)}></devtools-button>
          <devtools-button class="toolbar-button"
                           jslog=${VisualLogging.toggle().track({click: true}).context('current-dock-state-bottom')}
                           title=${i18nString(UIStrings.dockToBottom)}
                           aria-label=${i18nString(UIStrings.dockToBottom)}
                           .iconName=${'dock-bottom'}
                           .toggled=${dockController.dockSide() === UI.DockController.DockState.BOTTOM}
                           .toggledIconName=${'dock-bottom'}
                           .toggleType=${Buttons.Button.ToggleType.PRIMARY}
                           .variant=${Buttons.Button.Variant.ICON_TOGGLE}
                           @click=${setDockSide.bind(null, UI.DockController.DockState.BOTTOM)}></devtools-button>
          <devtools-button class="toolbar-button"
                           jslog=${VisualLogging.toggle().track({click: true}).context('current-dock-state-right')}
                           title=${i18nString(UIStrings.dockToRight)}
                           aria-label=${i18nString(UIStrings.dockToRight)}
                           .iconName=${'dock-right'}
                           .toggled=${dockController.dockSide() === UI.DockController.DockState.RIGHT}
                           .toggledIconName=${'dock-right'}
                           .toggleType=${Buttons.Button.ToggleType.PRIMARY}
                           .variant=${Buttons.Button.Variant.ICON_TOGGLE}
                           @click=${setDockSide.bind(null, UI.DockController.DockState.RIGHT)}></devtools-button>
        </devtools-toolbar>
      `, dockItemElement, {host: this});
      // clang-format on

      dockItemElement.addEventListener('keydown', event => {
        let dir = 0;
        if (event.key === 'ArrowLeft') {
          dir = -1;
        } else if (event.key === 'ArrowRight') {
          dir = 1;
        } else if (event.key === 'ArrowDown') {
          const contextMenuElement = dockItemElement.closest('.soft-context-menu');
          contextMenuElement?.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown'}));
          return;
        } else {
          return;
        }

        const buttons = Array.from(dockItemElement.querySelectorAll('devtools-button'));
        let index = buttons.findIndex(button => button.hasFocus());
        index = Platform.NumberUtilities.clamp(index + dir, 0, buttons.length - 1);
        buttons[index].focus();
        event.consume(true);
      });
      contextMenu.headerSection().appendCustomItem(dockItemElement, 'dock-side');
    }

    const button = this.#item.element;

    function setDockSide(side: UI.DockController.DockState): void {
      void dockController.once(UI.DockController.Events.AFTER_DOCK_SIDE_CHANGED).then(() => button.focus());
      dockController.setDockSide(side);
      contextMenu.discard();
    }

    contextMenu.defaultSection().appendAction('freestyler.main-menu', undefined, /* optional */ true);

    contextMenu.defaultSection().appendItem(i18nString(UIStrings.getDevToolsMcp), () => {
      UIHelpers.openInNewTab('https://github.com/ChromeDevTools/chrome-devtools-mcp');
    }, {
      additionalElement: UI.UIUtils.maybeCreateNewBadge('get-devtools-mcp'),
      jslogContext: 'get-devtools-mcp',
    });

    contextMenu.defaultSection().appendSeparator();

    if (dockController.dockSide() === UI.DockController.DockState.UNDOCKED) {
      const mainTarget = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
      if (mainTarget && mainTarget.type() === SDK.Target.Type.FRAME) {
        contextMenu.defaultSection().appendAction('inspector-main.focus-debuggee', i18nString(UIStrings.focusDebuggee));
      }
    }

    contextMenu.defaultSection().appendAction(
        'main.toggle-drawer',
        UI.InspectorView.InspectorView.instance().drawerVisible() ? i18nString(UIStrings.hideConsoleDrawer) :
                                                                    i18nString(UIStrings.showConsoleDrawer));
    contextMenu.appendItemsAtLocation('mainMenu');
    const moreTools =
        contextMenu.defaultSection().appendSubMenuItem(i18nString(UIStrings.moreTools), false, 'more-tools');
    const viewExtensions = UI.ViewManager.ViewManager.instance().getRegisteredViewExtensions();
    viewExtensions.sort((extension1, extension2) => {
      const title1 = extension1.title();
      const title2 = extension2.title();
      return title1.localeCompare(title2);
    });

    for (const viewExtension of viewExtensions) {
      const location = viewExtension.location();
      const persistence = viewExtension.persistence();
      const title = viewExtension.title();
      const id = viewExtension.viewId();

      if (id === 'issues-pane') {
        moreTools.defaultSection().appendItem(title, () => {
          Host.userMetrics.issuesPanelOpenedFrom(Host.UserMetrics.IssueOpener.HAMBURGER_MENU);
          void UI.ViewManager.ViewManager.instance().showView('issues-pane', /* userGesture */ true);
        }, {jslogContext: id});
        continue;
      }

      if (persistence !== 'closeable') {
        continue;
      }
      if (location !== 'drawer-view' && location !== 'panel') {
        continue;
      }

      moreTools.defaultSection().appendItem(title, () => {
        void UI.ViewManager.ViewManager.instance().showView(id, true, false);
      }, {isPreviewFeature: viewExtension.isPreviewFeature(), jslogContext: id});
    }

    const helpSubMenu = contextMenu.footerSection().appendSubMenuItem(i18nString(UIStrings.help), false, 'help');
    helpSubMenu.appendItemsAtLocation('mainMenuHelp');
  }
}

let settingsButtonProviderInstance: SettingsButtonProvider;

export class SettingsButtonProvider implements UI.Toolbar.Provider {
  readonly #settingsButton: UI.Toolbar.ToolbarButton;
  private constructor() {
    this.#settingsButton = UI.Toolbar.Toolbar.createActionButton('settings.show');
  }

  static instance(opts: {
    forceNew: boolean|null,
  } = {forceNew: null}): SettingsButtonProvider {
    const {forceNew} = opts;
    if (!settingsButtonProviderInstance || forceNew) {
      settingsButtonProviderInstance = new SettingsButtonProvider();
    }

    return settingsButtonProviderInstance;
  }

  item(): UI.Toolbar.ToolbarItem|null {
    return this.#settingsButton;
  }
}

export class PauseListener {
  constructor() {
    SDK.TargetManager.TargetManager.instance().addModelListener(
        SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.DebuggerPaused, this.#debuggerPaused, this);
  }

  #debuggerPaused(event: Common.EventTarget.EventTargetEvent<SDK.DebuggerModel.DebuggerModel>): void {
    SDK.TargetManager.TargetManager.instance().removeModelListener(
        SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.DebuggerPaused, this.#debuggerPaused, this);
    const debuggerModel = event.data;
    const debuggerPausedDetails = debuggerModel.debuggerPausedDetails();
    UI.Context.Context.instance().setFlavor(SDK.Target.Target, debuggerModel.target());
    void Common.Revealer.reveal(debuggerPausedDetails);
  }
}

/** Unused but mentioned at https://chromedevtools.github.io/devtools-protocol/#:~:text=use%20Main.MainImpl.-,sendOverProtocol,-()%20in%20the **/
export function sendOverProtocol(
    method: ProtocolClient.InspectorBackend.QualifiedName, params: Object|null): Promise<unknown[]|null> {
  return new Promise((resolve, reject) => {
    const sendRawMessage = ProtocolClient.InspectorBackend.test.sendRawMessage;
    if (!sendRawMessage) {
      return reject('Unable to send message to test client');
    }
    sendRawMessage(method, params, (err, ...results) => {
      if (err) {
        return reject(err);
      }
      return resolve(results);
    });
  });
}

export class ReloadActionDelegate implements UI.ActionRegistration.ActionDelegate {
  handleAction(_context: UI.Context.Context, actionId: string): boolean {
    switch (actionId) {
      case 'main.debug-reload':
        Components.Reload.reload();

        return true;
    }
    return false;
  }
}
