// Copyright 2011 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 */

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 Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Buttons from '../../ui/components/buttons/buttons.js';
import {createIcon, type Icon} from '../kit/kit.js';
import * as VisualLogging from '../visual_logging/visual_logging.js';

import type {ActionDelegate as ActionDelegateInterface} from './ActionRegistration.js';
import {ActionRegistry} from './ActionRegistry.js';
import * as ARIAUtils from './ARIAUtils.js';
import type {Context} from './Context.js';
import type {ContextMenu} from './ContextMenu.js';
import {Dialog} from './Dialog.js';
import {DockController, DockState, Events as DockControllerEvents} from './DockController.js';
import {GlassPane} from './GlassPane.js';
import {Infobar, Type as InfobarType} from './Infobar.js';
import {InspectorDrawerView} from './InspectorDrawerView.js';
import {KeyboardShortcut} from './KeyboardShortcut.js';
import type {Panel} from './Panel.js';
import {type ShowMode, SplitWidget} from './SplitWidget.js';
import {type EventData, Events as TabbedPaneEvents, type TabbedPane, type TabbedPaneTabDelegate} from './TabbedPane.js';
import {Tooltip} from './Tooltip.js';
import type {TabbedViewLocation, View, ViewLocation, ViewLocationResolver} from './View.js';
import {ViewManager} from './ViewManager.js';
import {VBox, type Widget, WidgetFocusRestorer} from './Widget.js';

const UIStrings = {
  /**
   * @description The aria label for the drawer minimized.
   */
  drawerMinimized: 'Drawer minimized',
  /**
   * @description The aria label for the drawer expanded.
   */
  drawerExpanded: 'Drawer expanded',
  /**
   * @description The ARIA label for the main tab bar that contains the DevTools panels
   */
  panels: 'Panels',
  /**
   * @description Title of an action that reloads the tab currently being debugged by DevTools
   */
  reloadDebuggedTab: 'Reload page',
  /**
   * @description Title of an action that reloads the DevTools
   */
  reloadDevtools: 'Reload DevTools',
  /**
   * @description Title of an action that restarts Chrome
   */
  restartChrome: 'Restart Chrome',
  /**
   * @description Confirmation dialog text for restarting Chrome
   */
  areYouSureYouWantToRestartChrome: 'Are you sure you want to restart Chrome?',
  /**
   * @description Text for context menu action to move a tab to the main tab bar
   */
  moveToMainTabBar: 'Move to main tab bar',
  /**
   * @description Text for context menu action to move a tab to the drawer
   */
  moveToDrawer: 'Move to drawer',
  /**
   * @description Text shown in a prompt to the user when DevTools is started and the
   * currently selected DevTools locale does not match Chrome's locale.
   * The placeholder is the current Chrome language.
   * @example {German} PH1
   */
  devToolsLanguageMissmatch: 'DevTools is now available in {PH1}',
  /**
   * @description An option the user can select when we notice that DevTools
   * is configured with a different locale than Chrome. This option means DevTools will
   * always try and display the DevTools UI in the same language as Chrome.
   */
  setToBrowserLanguage: 'Always match Chrome\'s language',
  /**
   * @description An option the user can select when DevTools notices that DevTools
   * is configured with a different locale than Chrome. This option means DevTools UI
   * will be switched to the language specified in the placeholder.
   * @example {German} PH1
   */
  setToSpecificLanguage: 'Switch DevTools to {PH1}',
  /**
   * @description The aria label for main toolbar
   */
  mainToolbar: 'Main toolbar',
  /**
   * @description The aria label for the drawer.
   */
  drawer: 'Tool drawer',
  /**
   * @description The aria label for the drawer shown.
   */
  drawerShown: 'Drawer shown',
  /**
   * @description The aria label for the drawer hidden.
   */
  drawerHidden: 'Drawer hidden',
  /**
   * @description Request for the user to select a local file system folder for DevTools
   * to store local overrides in.
   */
  selectOverrideFolder: 'Select a folder to store override files in',
  /**
   * @description Label for a button which opens a file picker.
   */
  selectFolder: 'Select folder',
} as const;
const str_ = i18n.i18n.registerUIStrings('ui/legacy/InspectorView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
let inspectorViewInstance: InspectorView|null = null;

const MIN_MAIN_PANEL_WIDTH = 240;
const MIN_VERTICAL_DRAWER_WIDTH = 280;
// Inspector need to have space for both main panel and the drawer + some slack for borders
const MIN_INSPECTOR_WIDTH_HORIZONTAL_DRAWER = 250;
const MIN_INSPECTOR_WIDTH_VERTICAL_DRAWER = 530;
const MIN_INSPECTOR_HEIGHT = 72;

export enum DrawerOrientation {
  VERTICAL = 'vertical',
  HORIZONTAL = 'horizontal',
  UNSET = 'unset',
}

export enum DockMode {
  BOTTOM = 'bottom',
  SIDE = 'side',  // For LEFT and RIGHT
  UNDOCKED = 'undocked',
}

export interface DrawerOrientationByDockMode {
  [DockMode.BOTTOM]: DrawerOrientation;
  [DockMode.SIDE]: DrawerOrientation;
  [DockMode.UNDOCKED]: DrawerOrientation;
}

interface ScrollState {
  element: HTMLElement;
  scrollTop: number;
  scrollLeft: number;
}

export class InspectorView extends VBox implements ViewLocationResolver {
  private readonly drawerOrientationByDockSetting: Common.Settings.Setting<DrawerOrientationByDockMode>;
  private readonly drawerSplitWidget: SplitWidget;
  readonly #drawerView: InspectorDrawerView;
  private readonly tabDelegate: InspectorViewTabDelegate;
  private readonly drawerTabbedLocation: TabbedViewLocation;
  private drawerTabbedPane: TabbedPane;
  private infoBarDiv!: HTMLDivElement|null;
  private readonly tabbedLocation: TabbedViewLocation;
  readonly tabbedPane: TabbedPane;
  private readonly keyDownBound: (event: KeyboardEvent) => void;
  private currentPanelLocked?: boolean;
  private focusRestorer?: WidgetFocusRestorer|null;
  #mainPanelAtDrawerFocus: string|null = null;
  private ownerSplitWidget?: SplitWidget;
  private reloadRequiredInfobar?: Infobar;
  #chromeRestartRequiredInfobar?: Infobar;
  #debuggedTabReloadRequiredInfobar?: Infobar;
  #selectOverrideFolderInfobar?: Infobar;
  #resizeObserver: ResizeObserver;
  #drawerShowModeBeforeDockSideChange: ShowMode|null = null;
  #drawerMinimizedBeforeDockSideChange: boolean|null = null;

  constructor() {
    super();
    GlassPane.setContainer(this.element);
    this.setMinimumSize(MIN_INSPECTOR_WIDTH_HORIZONTAL_DRAWER, MIN_INSPECTOR_HEIGHT);

    // DevTools sidebar is a vertical split of main tab bar panels and a drawer.
    this.drawerOrientationByDockSetting =
        Common.Settings.Settings.instance().createSetting('inspector.drawer-orientation-by-dock-mode', {
          [DockMode.BOTTOM]: DrawerOrientation.UNSET,
          [DockMode.SIDE]: DrawerOrientation.UNSET,
          [DockMode.UNDOCKED]: DrawerOrientation.UNSET,
        });
    const initialOrientation = this.#getOrientationForDockMode();
    const isVertical = initialOrientation === DrawerOrientation.VERTICAL;
    this.drawerSplitWidget = new SplitWidget(isVertical, true, 'inspector.drawer-split-view-state', 200, 200);
    this.drawerSplitWidget.hideSidebar();
    this.drawerSplitWidget.enableShowModeSaving();
    this.drawerSplitWidget.show(this.element);

    this.tabDelegate = new InspectorViewTabDelegate();

    // Create drawer tabbed pane.
    this.#drawerView = new InspectorDrawerView({
      splitWidget: this.drawerSplitWidget,
      revealDrawer: this.showDrawer.bind(this, {
        focus: false,
        hasTargetDrawer: true,
      }),
      isVisible: () => this.drawerSplitWidget.sidebarIsShowing() && !this.drawerSplitWidget.isSidebarMinimized(),
      drawerLabel: i18nString(UIStrings.drawer),
      onToggleMinimized: this.toggleDrawerMinimized.bind(this),
      onHide: this.closeDrawer.bind(this),
      onToggleOrientation: this.toggleDrawerOrientation.bind(this),
      onExpandFromMinimized: this.#expandDrawerFromInteraction.bind(this),
      onMinimizeFromTabInteraction: this.minimizeDrawer.bind(this),
      onTabSelected: this.tabSelected.bind(this),
      isConsoleOpenInMainAndDrawer: tabId => tabId === 'console-view' && this.tabbedPane.selectedTabId === 'console',
      tabDelegate: this.tabDelegate,
      enableOrientationToggle: Boolean(Root.Runtime.hostConfig.devToolsFlexibleLayout?.verticalDrawerEnabled),
      isVertical,
      verticalExpandedMinimumWidth: MIN_VERTICAL_DRAWER_WIDTH,
      minimumSizes: {
        inspectorWidthWhenVertical: MIN_INSPECTOR_WIDTH_VERTICAL_DRAWER,
        inspectorWidthWhenHorizontal: MIN_INSPECTOR_WIDTH_HORIZONTAL_DRAWER,
        inspectorHeight: MIN_INSPECTOR_HEIGHT,
      },
      setInspectorMinimumSize: this.setMinimumSize.bind(this),
    });
    this.drawerTabbedLocation = this.#drawerView.tabbedLocation;
    this.drawerTabbedPane = this.#drawerView.tabbedPane;

    // Create main area tabbed pane.
    this.tabbedLocation = ViewManager.instance().createTabbedLocation(
        Host.InspectorFrontendHost.InspectorFrontendHostInstance.bringToFront.bind(
            Host.InspectorFrontendHost.InspectorFrontendHostInstance),
        'panel', true, true, Root.Runtime.Runtime.queryParam('panel'));

    this.tabbedPane = this.tabbedLocation.tabbedPane();
    this.tabbedPane.setMinimumSize(MIN_MAIN_PANEL_WIDTH, 0);
    this.tabbedPane.element.classList.add('main-tabbed-pane');
    // The 'Inspect element' and 'Device mode' buttons in the tabs toolbar takes longer to load than
    // the tabs themselves, so a space equal to the buttons' total width is preemptively allocated
    // to prevent to prevent a shift in the tab layout. Note that when DevTools cannot be docked,
    // the Device mode button is not added and so the allocated space is smaller.
    const allocatedSpace = Root.Runtime.conditions.canDock() ? '69px' : '41px';
    this.tabbedPane.leftToolbar().style.minWidth = allocatedSpace;
    this.tabbedPane.addEventListener(
        TabbedPaneEvents.TabSelected,
        (event: Common.EventTarget.EventTargetEvent<EventData>) => this.tabSelected(event.data.tabId), this);
    const selectedTab = this.tabbedPane.selectedTabId;
    if (selectedTab) {
      Host.userMetrics.panelShown(selectedTab, true);
    }
    this.tabbedPane.setAccessibleName(i18nString(UIStrings.panels));
    this.tabbedPane.setTabDelegate(this.tabDelegate);

    const mainHeaderElement = this.tabbedPane.headerElement();
    ARIAUtils.markAsNavigation(mainHeaderElement);
    ARIAUtils.setLabel(mainHeaderElement, i18nString(UIStrings.mainToolbar));
    mainHeaderElement.setAttribute('jslog', `${VisualLogging.toolbar('main').track({
                                     drag: true,
                                     keydown: 'ArrowUp|ArrowLeft|ArrowDown|ArrowRight|Enter|Space',
                                   })}`);

    // Store the initial selected panel for use in launch histograms
    Host.userMetrics.setLaunchPanel(this.tabbedPane.selectedTabId);

    if (Host.InspectorFrontendHost.isUnderTest()) {
      this.tabbedPane.setAutoSelectFirstItemOnShow(false);
    }
    this.drawerSplitWidget.setMainWidget(this.tabbedPane);
    this.drawerSplitWidget.setDefaultFocusedChild(this.tabbedPane);

    this.keyDownBound = this.keyDown.bind(this);
    Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.addEventListener(
        Host.InspectorFrontendHostAPI.Events.ShowPanel, showPanel.bind(this));

    function showPanel(this: InspectorView, {data: panelName}: Common.EventTarget.EventTargetEvent<string>): void {
      void this.showPanel(panelName);
    }

    if (shouldShowLocaleInfobar()) {
      const infobar = createLocaleInfobar();
      infobar.setParentView(this);
      this.attachInfobar(infobar);
    }
    this.#resizeObserver = new ResizeObserver(this.#observedResize.bind(this));
    DockController.instance().addEventListener(
        DockControllerEvents.BEFORE_DOCK_SIDE_CHANGED, this.#rememberDrawerStateBeforeDockSideChange, this);
    DockController.instance().addEventListener(
        DockControllerEvents.DOCK_SIDE_CHANGED, this.#applyDrawerOrientationForDockSide, this);
    DockController.instance().addEventListener(
        DockControllerEvents.AFTER_DOCK_SIDE_CHANGED, this.#restoreDrawerStateAfterDockSideChange, this);

    this.#drawerView.restoreMinimizedStateFromSettings();
  }

  static instance(opts: {
    forceNew: boolean|null,
  }|undefined = {forceNew: null}): InspectorView {
    const {forceNew} = opts;
    if (!inspectorViewInstance || forceNew) {
      inspectorViewInstance = new InspectorView();
    }

    return inspectorViewInstance;
  }

  static maybeGetInspectorViewInstance(): InspectorView|null {
    return inspectorViewInstance;
  }

  static removeInstance(): void {
    inspectorViewInstance = null;
  }

  applyDrawerOrientationForDockSideForTest(): void {
  }

  #applyDrawerOrientationForDockSide(): void {
    if (!this.drawerVisible()) {
      this.applyDrawerOrientationForDockSideForTest();
      return;
    }
    const newOrientation = this.#getOrientationForDockMode();
    this.#applyDrawerOrientation(newOrientation);
    this.applyDrawerOrientationForDockSideForTest();
  }

  #getDockMode(): DockMode {
    const dockSide = DockController.instance().dockSide();
    if (dockSide === DockState.BOTTOM) {
      return DockMode.BOTTOM;
    }
    if (dockSide === DockState.UNDOCKED) {
      return DockMode.UNDOCKED;
    }

    return DockMode.SIDE;
  }

  #getOrientationForDockMode(): Omit<DrawerOrientation, DrawerOrientation.UNSET> {
    const dockMode = this.#getDockMode();
    const orientationSetting = this.drawerOrientationByDockSetting.get();

    let orientation = orientationSetting[dockMode];
    if (orientation === DrawerOrientation.UNSET) {
      // Apply defaults: horizontal for side-dock, vertical for bottom-dock.
      orientation = dockMode === DockMode.BOTTOM ? DrawerOrientation.VERTICAL : DrawerOrientation.HORIZONTAL;
    }
    return orientation;
  }

  #applyDrawerOrientation(orientation: Omit<DrawerOrientation, DrawerOrientation.UNSET>): void {
    const shouldBeVertical = orientation === DrawerOrientation.VERTICAL;
    const isVertical = this.#drawerView.isVertical();
    if (shouldBeVertical === isVertical) {
      return;
    }

    this.#drawerView.setVertical(shouldBeVertical);
  }

  #applyDrawerState(showMode: ShowMode, minimized: boolean): void {
    this.#drawerView.applyState(showMode, minimized);
  }

  #observedResize(): void {
    const rect = this.element.getBoundingClientRect();
    this.element.style.setProperty('--devtools-window-left', `${rect.left}px`);
    this.element.style.setProperty('--devtools-window-right', `${window.innerWidth - rect.right}px`);
    this.element.style.setProperty('--devtools-window-width', `${rect.width}px`);
    this.element.style.setProperty('--devtools-window-top', `${rect.top}px`);
    this.element.style.setProperty('--devtools-window-bottom', `${window.innerHeight - rect.bottom}px`);
    this.element.style.setProperty('--devtools-window-height', `${rect.height}px`);
  }

  override wasShown(): void {
    super.wasShown();
    this.#resizeObserver.observe(this.element);
    this.#observedResize();
    this.element.ownerDocument.addEventListener('keydown', this.keyDownBound, false);
    this.#applyDrawerOrientationForDockSide();
  }

  override willHide(): void {
    super.willHide();
    this.#resizeObserver.unobserve(this.element);
    this.element.ownerDocument.removeEventListener('keydown', this.keyDownBound, false);
  }

  #rememberDrawerStateBeforeDockSideChange(): void {
    this.#drawerShowModeBeforeDockSideChange = this.drawerSplitWidget.showMode();
    this.#drawerMinimizedBeforeDockSideChange = this.isDrawerMinimized();
  }

  #restoreDrawerStateAfterDockSideChange(): void {
    const showMode = this.#drawerShowModeBeforeDockSideChange;
    const minimized = this.#drawerMinimizedBeforeDockSideChange;
    this.#drawerShowModeBeforeDockSideChange = null;
    this.#drawerMinimizedBeforeDockSideChange = null;

    if (showMode === null || minimized === null) {
      return;
    }

    this.#applyDrawerState(showMode, minimized);
  }

  resolveLocation(locationName: string): ViewLocation|null {
    if (locationName === 'drawer-view') {
      return this.drawerTabbedLocation;
    }
    if (locationName === 'panel') {
      return this.tabbedLocation;
    }
    return null;
  }

  async createToolbars(): Promise<void> {
    await this.tabbedPane.leftToolbar().appendItemsAtLocation('main-toolbar-left');
    await this.tabbedPane.rightToolbar().appendItemsAtLocation('main-toolbar-right');
  }

  addPanel(view: View): void {
    this.tabbedLocation.appendView(view);
  }

  hasPanel(panelName: string): boolean {
    return this.tabbedPane.hasTab(panelName);
  }

  async panel(panelName: string): Promise<Panel> {
    const view = ViewManager.instance().view(panelName);
    if (!view) {
      throw new Error(`Expected view for panel '${panelName}'`);
    }
    return await (view.widget() as Promise<Panel>);
  }

  onSuspendStateChanged(allTargetsSuspended: boolean): void {
    this.currentPanelLocked = allTargetsSuspended;
    this.tabbedPane.setCurrentTabLocked(this.currentPanelLocked);
    this.tabbedPane.leftToolbar().setEnabled(!this.currentPanelLocked);
    this.tabbedPane.rightToolbar().setEnabled(!this.currentPanelLocked);
  }

  canSelectPanel(panelName: string): boolean {
    return !this.currentPanelLocked || this.tabbedPane.selectedTabId === panelName;
  }

  async showPanel(panelName: string): Promise<void> {
    await ViewManager.instance().showView(panelName);
  }

  setPanelWarnings(tabId: string, warnings: string[]): void {
    // Find the tabbed location where the panel lives
    const tabbedPane = this.getTabbedPaneForTabId(tabId);
    if (tabbedPane) {
      let icon: Icon|null = null;
      if (warnings.length !== 0) {
        const warning = warnings.length === 1 ? warnings[0] : '· ' + warnings.join('\n· ');
        icon = createIcon('warning-filled', 'small');
        icon.classList.add('warning');
        Tooltip.install(icon, warning);
      }
      tabbedPane.setTrailingTabIcon(tabId, icon);
    }
  }

  private getTabbedPaneForTabId(tabId: string): TabbedPane|null {
    // Tab exists in the main panel
    if (this.tabbedPane.hasTab(tabId)) {
      return this.tabbedPane;
    }

    // Tab exists in the drawer
    if (this.drawerTabbedPane.hasTab(tabId)) {
      return this.drawerTabbedPane;
    }

    // Tab is not open
    return null;
  }

  currentPanelDeprecated(): Widget|null {
    return (ViewManager.instance().materializedWidget(this.tabbedPane.selectedTabId || ''));
  }

  showDrawer({focus, hasTargetDrawer}: {focus: boolean, hasTargetDrawer: boolean}): void {
    // Both checks are needed: during an animated hide, drawerVisible()
    // (tabbedPane.isShowing()) remains true until the animation completes and
    // the widget is detached, but sidebarIsShowing() turns false synchronously.
    if (this.#drawerView.drawerVisible() && this.drawerSplitWidget.sidebarIsShowing()) {
      // Only expand a minimized drawer when the user explicitly requested
      // focus (e.g. keyboard shortcut or direct action). Programmatic calls
      // (like tab switches) should not auto-expand.
      if (focus && this.isDrawerMinimized()) {
        this.setDrawerMinimized(false);
        ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.drawerExpanded));
      }
      return;
    }
    this.#drawerView.show(hasTargetDrawer);
    if (focus) {
      this.focusRestorer = new WidgetFocusRestorer(this.drawerTabbedPane);
      this.#mainPanelAtDrawerFocus = this.tabbedPane.selectedTabId;
    } else {
      this.focusRestorer = null;
      this.#mainPanelAtDrawerFocus = null;
    }
    this.#applyDrawerOrientationForDockSide();
    ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.drawerShown));
  }

  drawerVisible(): boolean {
    return this.#drawerView.drawerVisible();
  }

  minimizeDrawer(): void {
    if (!this.#drawerView.drawerVisible()) {
      return;
    }
    this.focusRestorer = null;
    this.#mainPanelAtDrawerFocus = null;
    this.setDrawerMinimized(true);
    ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.drawerMinimized));
  }

  closeDrawer(): void {
    if (!this.#drawerView.drawerVisible()) {
      return;
    }
    // Preserve main panel scroll positions across the drawer layout change.
    const scrollState = this.#captureMainPanelScrollState();
    if (this.focusRestorer && this.#mainPanelAtDrawerFocus === this.tabbedPane.selectedTabId) {
      this.focusRestorer.restore();
    }
    this.focusRestorer = null;
    this.#mainPanelAtDrawerFocus = null;
    this.#drawerView.hide();
    this.#restoreMainPanelScrollState(scrollState);
    ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.drawerHidden));
  }

  toggleDrawerOrientation({force}: {force?: Omit<DrawerOrientation, DrawerOrientation.UNSET>} = {}): void {
    if (!this.drawerTabbedPane.isShowing()) {
      return;
    }

    const dockMode = this.#getDockMode();
    const currentSettings = this.drawerOrientationByDockSetting.get();

    let newOrientation: Omit<DrawerOrientation, DrawerOrientation.UNSET>;
    if (force) {
      newOrientation = force;
    } else {
      const currentOrientation = this.#getOrientationForDockMode();
      newOrientation =
          currentOrientation === DrawerOrientation.VERTICAL ? DrawerOrientation.HORIZONTAL : DrawerOrientation.VERTICAL;
    }

    currentSettings[dockMode] = newOrientation as DrawerOrientation;
    this.drawerOrientationByDockSetting.set(currentSettings);

    this.#applyDrawerOrientation(newOrientation);
  }

  isUserExplicitlyUpdatedDrawerOrientation(): boolean {
    const orientationSetting = this.drawerOrientationByDockSetting.get();
    const dockMode = this.#getDockMode();
    return orientationSetting[dockMode] !== DrawerOrientation.UNSET;
  }

  setDrawerMinimized(minimized: boolean): void {
    // Preserve main panel scroll positions across the drawer layout change.
    const scrollState = this.#captureMainPanelScrollState();
    this.#drawerView.setMinimized(minimized);
    this.#restoreMainPanelScrollState(scrollState);
  }

  // Showing, hiding, or minimizing the drawer causes SplitWidget to
  // manipulate CSS classes and remove inline layout properties, triggering a
  // flexbox reflow that resets scroll positions in the main panel. We capture
  // them before the operation and restore them afterwards to preserve the
  // user's scroll position.
  #captureMainPanelScrollState(): ScrollState[] {
    const selectedTabId = this.tabbedPane.selectedTabId;
    if (!selectedTabId) {
      return [];
    }

    let panel: Widget|null = null;
    try {
      panel = ViewManager.instance().materializedWidget(selectedTabId);
    } catch {
      return [];
    }

    if (!panel) {
      return [];
    }

    const rootElement = panel.element as HTMLElement;
    const scrollableElements: HTMLElement[] = [
      rootElement,
      ...rootElement.querySelectorAll<HTMLElement>('*'),
    ];

    return scrollableElements.filter(element => element.scrollTop !== 0 || element.scrollLeft !== 0)
        .map(element => ({
               element,
               scrollTop: element.scrollTop,
               scrollLeft: element.scrollLeft,
             }));
  }

  #restoreMainPanelScrollState(scrollState: ScrollState[]): void {
    if (!scrollState.length) {
      return;
    }

    this.element.window().requestAnimationFrame(() => {
      for (const {element, scrollTop, scrollLeft} of scrollState) {
        if (!element.isConnected) {
          continue;
        }
        element.scrollTop = scrollTop;
        element.scrollLeft = scrollLeft;
      }
    });
  }

  drawerSize(): number {
    return this.#drawerView.drawerSize();
  }

  setDrawerSize(size: number): void {
    this.#drawerView.setDrawerSize(size);
  }

  totalSize(): number {
    return this.#drawerView.totalSize();
  }

  isDrawerMinimized(): boolean {
    return this.#drawerView.isMinimized();
  }

  toggleDrawerMinimized(): void {
    if (!this.#drawerView.drawerVisible()) {
      // If the drawer is not visible at all, show and expand it.
      this.showDrawer({focus: true, hasTargetDrawer: false});
      return;
    }
    const minimized = this.isDrawerMinimized();
    if (minimized && this.drawerTabbedPane.selectedTabId === 'console-view' &&
        this.tabbedPane.selectedTabId === 'console') {
      return;
    }
    this.setDrawerMinimized(!minimized);
    if (!minimized) {
      ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.drawerMinimized));
    } else {
      ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.drawerExpanded));
    }
  }

  isDrawerOrientationVertical(): boolean {
    return this.#drawerView.isVertical();
  }

  #expandDrawerFromInteraction(): void {
    this.setDrawerMinimized(false);
    ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.drawerExpanded));
  }

  private keyDown(event: KeyboardEvent): void {
    if (!KeyboardShortcut.eventHasCtrlEquivalentKey(event) || event.altKey || event.shiftKey) {
      return;
    }

    // Ctrl/Cmd + 1-9 should show corresponding panel.
    const panelShortcutEnabled = Common.Settings.moduleSetting('shortcut-panel-switch').get();
    if (panelShortcutEnabled) {
      let panelIndex = -1;
      if (event.keyCode > 0x30 && event.keyCode < 0x3A) {
        panelIndex = event.keyCode - 0x31;
      } else if (
          event.keyCode > 0x60 && event.keyCode < 0x6A && event.location === KeyboardEvent.DOM_KEY_LOCATION_NUMPAD) {
        panelIndex = event.keyCode - 0x61;
      }
      if (panelIndex !== -1) {
        const panelName = this.tabbedPane.tabIds()[panelIndex];
        if (panelName) {
          if (!Dialog.hasInstance() && !this.currentPanelLocked) {
            void this.showPanel(panelName);
            void VisualLogging.logKeyDown(null, event, `panel-by-index-${panelName}`);
          }
          event.consume(true);
        }
      }
    }
  }

  override onResize(): void {
    GlassPane.containerMoved(this.element);
  }

  topResizerElement(): Element {
    return this.tabbedPane.headerElement();
  }

  toolbarItemResized(): void {
    this.tabbedPane.headerResized();
  }

  private tabSelected(tabId: string): void {
    Host.userMetrics.panelShown(tabId);
  }

  setOwnerSplit(splitWidget: SplitWidget): void {
    this.ownerSplitWidget = splitWidget;
  }

  ownerSplit(): SplitWidget|null {
    return this.ownerSplitWidget || null;
  }

  minimize(): void {
    if (this.ownerSplitWidget) {
      this.ownerSplitWidget.setSidebarMinimized(true);
    }
  }

  restore(): void {
    if (this.ownerSplitWidget) {
      this.ownerSplitWidget.setSidebarMinimized(false);
    }
  }

  displayDebuggedTabReloadRequiredWarning(message: string): void {
    if (!this.#debuggedTabReloadRequiredInfobar) {
      const infobar = new Infobar(
          InfobarType.INFO, message,
          [
            {
              text: i18nString(UIStrings.reloadDebuggedTab),
              delegate: () => {
                reloadDebuggedTab();
                this.removeDebuggedTabReloadRequiredWarning();
              },
              dismiss: false,
              buttonVariant: Buttons.Button.Variant.PRIMARY,
              jslogContext: 'main.debug-reload',
            },
          ],
          undefined, 'reload-required');
      infobar.setParentView(this);
      this.attachInfobar(infobar);
      this.#debuggedTabReloadRequiredInfobar = infobar;
      infobar.setCloseCallback(() => {
        this.#debuggedTabReloadRequiredInfobar = undefined;
      });

      SDK.TargetManager.TargetManager.instance().addModelListener(
          SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.PrimaryPageChanged,
          this.removeDebuggedTabReloadRequiredWarning, this);
    }
  }

  removeDebuggedTabReloadRequiredWarning(): void {
    if (this.#debuggedTabReloadRequiredInfobar) {
      this.#debuggedTabReloadRequiredInfobar.dispose();
      SDK.TargetManager.TargetManager.instance().removeModelListener(
          SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.PrimaryPageChanged,
          this.removeDebuggedTabReloadRequiredWarning, this);
    }
  }

  displayReloadRequiredWarning(message: string): void {
    if (!this.reloadRequiredInfobar && !this.#chromeRestartRequiredInfobar) {
      const infobar = new Infobar(
          InfobarType.INFO, message,
          [
            {
              text: i18nString(UIStrings.reloadDevtools),
              delegate: () => reloadDevTools(),
              dismiss: false,
              buttonVariant: Buttons.Button.Variant.PRIMARY,
              jslogContext: 'main.debug-reload',
            },
          ],
          undefined, 'reload-required');
      infobar.setParentView(this);
      this.attachInfobar(infobar);
      this.reloadRequiredInfobar = infobar;
      infobar.setCloseCallback(() => {
        this.reloadRequiredInfobar = undefined;
      });
    }
  }

  displayChromeRestartRequiredWarning(message: string): void {
    if (this.reloadRequiredInfobar) {
      this.reloadRequiredInfobar.dispose();
    }
    if (!this.#chromeRestartRequiredInfobar) {
      const infobar = new Infobar(
          InfobarType.INFO, message,
          [
            {
              text: i18nString(UIStrings.restartChrome),
              delegate: () => {
                if (confirm(i18nString(UIStrings.areYouSureYouWantToRestartChrome))) {
                  Host.InspectorFrontendHost.InspectorFrontendHostInstance.requestRestart();
                }
              },
              dismiss: false,
              buttonVariant: Buttons.Button.Variant.PRIMARY,
              jslogContext: 'main.chrome-restart-chrome',
            },
          ],
          undefined, 'reload-required');
      infobar.setParentView(this);
      this.attachInfobar(infobar);
      this.#chromeRestartRequiredInfobar = infobar;
      infobar.setCloseCallback(() => {
        this.#chromeRestartRequiredInfobar = undefined;
      });
    }
  }

  displaySelectOverrideFolderInfobar(callback: () => void): void {
    if (!this.#selectOverrideFolderInfobar) {
      const infobar = new Infobar(
          InfobarType.INFO, i18nString(UIStrings.selectOverrideFolder),
          [
            {
              text: i18nString(UIStrings.selectFolder),
              delegate: () => callback(),
              dismiss: true,
              buttonVariant: Buttons.Button.Variant.TONAL,
              jslogContext: 'select-folder',
            },
          ],
          undefined, 'select-override-folder');
      infobar.setParentView(this);
      this.attachInfobar(infobar);
      this.#selectOverrideFolderInfobar = infobar;
      infobar.setCloseCallback(() => {
        this.#selectOverrideFolderInfobar = undefined;
      });
    }
  }

  private createInfoBarDiv(): void {
    if (!this.infoBarDiv) {
      this.infoBarDiv = document.createElement('div');
      this.infoBarDiv.classList.add('flex-none');
      this.contentElement.insertBefore(this.infoBarDiv, this.contentElement.firstChild);
    }
  }

  private attachInfobar(infobar: Infobar): void {
    this.createInfoBarDiv();
    this.infoBarDiv?.appendChild(infobar.element);
  }
}

function getDisableLocaleInfoBarSetting(): Common.Settings.Setting<boolean> {
  return Common.Settings.Settings.instance().createSetting('disable-locale-info-bar', false);
}

function shouldShowLocaleInfobar(): boolean {
  if (getDisableLocaleInfoBarSetting().get()) {
    return false;
  }

  // If the language setting is different than 'en-US', the user already
  // used the setting before, so don't show the toolbar.
  const languageSettingValue = Common.Settings.Settings.instance().moduleSetting<string>('language').get();
  if (languageSettingValue !== 'en-US') {
    return false;
  }

  // When the selected DevTools locale differs from the locale of the browser UI, we want to notify
  // users only once, that they have the opportunity to adjust DevTools locale to match Chrome's locale.
  return !i18n.DevToolsLocale.localeLanguagesMatch(navigator.language, languageSettingValue) &&
      i18n.DevToolsLocale.DevToolsLocale.instance().languageIsSupportedByDevTools(navigator.language);
}

function createLocaleInfobar(): Infobar {
  const devtoolsLocale = i18n.DevToolsLocale.DevToolsLocale.instance();
  const closestSupportedLocale = devtoolsLocale.lookupClosestDevToolsLocale(navigator.language);
  const locale = new Intl.Locale(closestSupportedLocale);
  const closestSupportedLanguageInCurrentLocale =
      new Intl.DisplayNames([devtoolsLocale.locale], {type: 'language'}).of(locale.language || 'en') || 'English';

  const languageSetting = Common.Settings.Settings.instance().moduleSetting<string>('language');
  return new Infobar(
      InfobarType.INFO, i18nString(UIStrings.devToolsLanguageMissmatch, {PH1: closestSupportedLanguageInCurrentLocale}),
      [
        {
          text: i18nString(UIStrings.setToBrowserLanguage),
          delegate: () => {
            languageSetting.set('browserLanguage');
            getDisableLocaleInfoBarSetting().set(true);
            reloadDevTools();
          },
          dismiss: true,
          jslogContext: 'set-to-browser-language',
        },
        {
          text: i18nString(UIStrings.setToSpecificLanguage, {PH1: closestSupportedLanguageInCurrentLocale}),
          delegate: () => {
            languageSetting.set(closestSupportedLocale);
            getDisableLocaleInfoBarSetting().set(true);
            reloadDevTools();
          },
          dismiss: true,
          jslogContext: 'set-to-specific-language',
        },
      ],
      getDisableLocaleInfoBarSetting(), 'language-mismatch');
}

function reloadDevTools(): void {
  if (DockController.instance().canDock() && DockController.instance().dockSide() === DockState.UNDOCKED) {
    Host.InspectorFrontendHost.InspectorFrontendHostInstance.setIsDocked(true, function() {});
  }
  Host.InspectorFrontendHost.InspectorFrontendHostInstance.reattach(() => window.location.reload());
}

function reloadDebuggedTab(): void {
  void ActionRegistry.instance().getAction('inspector-main.reload').execute();
}

export class ActionDelegate implements ActionDelegateInterface {
  handleAction(_context: Context, actionId: string): boolean {
    switch (actionId) {
      case 'main.toggle-drawer':
        if (InspectorView.instance().drawerVisible()) {
          InspectorView.instance().closeDrawer();
        } else {
          InspectorView.instance().showDrawer({
            focus: true,
            hasTargetDrawer: false,
          });
        }
        return true;
      case 'main.toggle-drawer-orientation':
        InspectorView.instance().toggleDrawerOrientation();
        return true;
      case 'main.next-tab':
        InspectorView.instance().tabbedPane.selectNextTab();
        InspectorView.instance().tabbedPane.focus();
        return true;
      case 'main.previous-tab':
        InspectorView.instance().tabbedPane.selectPrevTab();
        InspectorView.instance().tabbedPane.focus();
        return true;
    }
    return false;
  }
}

export class InspectorViewTabDelegate implements TabbedPaneTabDelegate {
  closeTabs(tabbedPane: TabbedPane, ids: string[]): void {
    tabbedPane.closeTabs(ids, true);
  }

  moveToDrawer(tabId: string): void {
    Host.userMetrics.actionTaken(Host.UserMetrics.Action.TabMovedToDrawer);
    ViewManager.instance().moveView(tabId, 'drawer-view');
  }

  moveToMainTabBar(tabId: string): void {
    Host.userMetrics.actionTaken(Host.UserMetrics.Action.TabMovedToMainPanel);
    ViewManager.instance().moveView(tabId, 'panel');
  }

  onContextMenu(tabId: string, contextMenu: ContextMenu): void {
    // Special case for console, we don't show the movable context panel for this two tabs
    if (tabId === 'console' || tabId === 'console-view') {
      return;
    }

    const locationName = ViewManager.instance().locationNameForViewId(tabId);
    if (locationName === 'drawer-view') {
      contextMenu.defaultSection().appendItem(
          i18nString(UIStrings.moveToMainTabBar), this.moveToMainTabBar.bind(this, tabId),
          {jslogContext: 'move-to-top'});
    } else {
      contextMenu.defaultSection().appendItem(
          i18nString(UIStrings.moveToDrawer), this.moveToDrawer.bind(this, tabId), {jslogContext: 'move-to-bottom'});
    }
  }
}
