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

/*
 * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
 * 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 '../../ui/legacy/legacy.js';

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 Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Protocol from '../../generated/protocol.js';
import * as AiCodeCompletion from '../../models/ai_code_completion/ai_code_completion.js';
import * as AiCodeGeneration from '../../models/ai_code_generation/ai_code_generation.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as IssuesManager from '../../models/issues_manager/issues_manager.js';
import * as Logs from '../../models/logs/logs.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as CodeHighlighter from '../../ui/components/code_highlighter/code_highlighter.js';
import * as Highlighting from '../../ui/components/highlighting/highlighting.js';
import * as IssueCounter from '../../ui/components/issue_counter/issue_counter.js';
import type * as TextEditor from '../../ui/components/text_editor/text_editor.js';
import {createIcon} from '../../ui/kit/kit.js';
// eslint-disable-next-line @devtools/es-modules-import
import objectValueStyles from '../../ui/legacy/components/object_ui/objectValue.css.js';
import * as SettingsUI from '../../ui/legacy/components/settings_ui/settings_ui.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import {AiCodeCompletionSummaryToolbar} from '../common/common.js';

import {ConsoleContextSelector} from './ConsoleContextSelector.js';
import {ConsoleFilter, FilterType, type LevelsMask} from './ConsoleFilter.js';
import {ConsolePinPane} from './ConsolePinPane.js';
import {ConsolePrompt, Events as ConsolePromptEvents} from './ConsolePrompt.js';
import {ConsoleSidebar, Events} from './ConsoleSidebar.js';
import consoleViewStyles from './consoleView.css.js';
import {
  ConsoleCommand,
  ConsoleCommandResult,
  ConsoleGroupViewMessage,
  ConsoleTableMessageView,
  ConsoleViewMessage,
  getMessageForElement,
} from './ConsoleViewMessage.js';
import {ConsoleViewport, type ConsoleViewportElement, type ConsoleViewportProvider} from './ConsoleViewport.js';

const UIStrings = {
  /**
   * @description Label for button which links to Issues tab, specifying how many issues there are.
   */
  issuesWithColon: '{n, plural, =0 {No Issues} =1 {# Issue:} other {# Issues:}}',
  /**
   * @description Text for the tooltip of the issue counter toolbar item
   */
  issueToolbarTooltipGeneral: 'Some problems no longer generate console messages, but are surfaced in the issues tab.',
  /**
   * @description Text for the tooltip of the issue counter toolbar item. The placeholder indicates how many issues
   * there are in the Issues tab broken down by kind.
   * @example {1 page error, 2 breaking changes} issueEnumeration
   */
  issueToolbarClickToView: 'Click to view {issueEnumeration}',
  /**
   * @description Text for the tooltip of the issue counter toolbar item. The placeholder indicates how many issues
   * there are in the Issues tab broken down by kind.
   */
  issueToolbarClickToGoToTheIssuesTab: 'Click to go to the issues tab',
  /**
   * @description Text in Console View of the Console panel
   */
  findStringInLogs: 'Find string in logs',
  /**
   * @description Tooltip text that appears when hovering over the largeicon settings gear in show settings pane setting in console view of the console panel
   */
  consoleSettings: 'Console settings',
  /**
   * @description Title of a setting under the Console category that can be invoked through the Command Menu
   */
  groupSimilarMessagesInConsole: 'Group similar messages',
  /**
   * @description Title of a setting under the Console category that can be invoked through the Command Menu
   */
  showCorsErrorsInConsole: 'CORS errors in console',
  /**
   * @description Tooltip for the the console sidebar toggle in the Console panel. Command to
   * open/show the sidebar.
   */
  showConsoleSidebar: 'Show console sidebar',
  /**
   * @description Tooltip for the the console sidebar toggle in the Console panel. Command to
   * open/show the sidebar.
   */
  hideConsoleSidebar: 'Hide console sidebar',
  /**
   * @description Screen reader announcement when the sidebar is shown in the Console panel.
   */
  consoleSidebarShown: 'Console sidebar shown',
  /**
   * @description Screen reader announcement when the sidebar is hidden in the Console panel.
   */
  consoleSidebarHidden: 'Console sidebar hidden',
  /**
   * @description Tooltip text that appears on the setting to preserve log when hovering over the item
   */
  doNotClearLogOnPageReload: 'Do not clear log on page reload / navigation',
  /**
   * @description Text to preserve the log after refreshing
   */
  preserveLog: 'Preserve log',
  /**
   * @description Text in Console View of the Console panel
   */
  networkMessages: 'Network messages',
  /**
   * @description Tooltip text that appears on the setting when hovering over it in Console View of the Console panel
   */
  onlyShowMessagesFromTheCurrentContext:
      'Only show messages from the current context (`top`, `iframe`, `worker`, extension)',
  /**
   * @description Alternative title text of a setting in Console View of the Console panel
   */
  selectedContextOnly: 'Selected context only',
  /**
   * @description Description of a setting that controls whether XMLHttpRequests are logged in the console.
   */
  logXMLHttpRequests: 'Log XMLHttpRequests',
  /**
   * @description Tooltip text that appears on the setting when hovering over it in Console View of the Console panel
   */
  eagerlyEvaluateTextInThePrompt: 'Eagerly evaluate text in the prompt',
  /**
   * @description Description of a setting that controls whether text typed in the console should be autocompleted from commands executed in the local console history.
   */
  autocompleteFromHistory: 'Autocomplete from history',
  /**
   * @description Description of a setting that controls whether user activation is triggered by evaluation'.
   */
  treatEvaluationAsUserActivation: 'Treat evaluation as user activation',
  /**
   * @description Text in Console View of the Console panel, indicating that a number of console
   * messages have been hidden.
   */
  sHidden: '{n, plural, =1 {# hidden} other {# hidden}}',
  /**
   * @description Alert message for screen readers when the console is cleared
   */
  consoleCleared: 'Console cleared',
  /**
   * @description Text in Console View of the Console panel
   * @example {index.js} PH1
   */
  hideMessagesFromS: 'Hide messages from {PH1}',
  /**
   * @description Text to save content as a specific file type
   */
  saveAs: 'Save as…',
  /**
   * @description Text to copy Console log to clipboard
   */
  copyConsole: 'Copy console',
  /**
   * @description A context menu item in the Console View of the Console panel
   */
  copyVisibleStyledSelection: 'Copy visible styled selection',
  /**
   * @description Text to replay an XHR request
   */
  replayXhr: 'Replay XHR',
  /**
   * @description Text to indicate DevTools is writing to a file
   */
  writingFile: 'Writing file…',
  /**
   * @description Text to indicate the searching is in progress
   */
  searching: 'Searching…',
  /**
   * @description Text in Console View of the Console panel
   */
  egEventdCdnUrlacom: 'e.g. `/event\d/ -cdn url:a.com`',
  /**
   * @description Sdk console message message level verbose of level Labels in Console View of the Console panel
   */
  verbose: 'Verbose',
  /**
   * @description Sdk console message message level info of level Labels in Console View of the Console panel
   */
  info: 'Info',
  /**
   * @description Sdk console message message level warning of level Labels in Console View of the Console panel
   */
  warnings: 'Warnings',
  /**
   * @description Text for errors
   */
  errors: 'Errors',
  /**
   * @description Tooltip text of the info icon shown next to the filter drop down
   *              in the Console panels main toolbar when the sidebar is active.
   */
  overriddenByFilterSidebar: 'Log levels are controlled by the console sidebar.',
  /**
   * @description Text in Console View of the Console panel
   */
  customLevels: 'Custom levels',
  /**
   * @description Text in Console View of the Console panel
   * @example {Warnings} PH1
   */
  sOnly: '{PH1} only',
  /**
   * @description Text in Console View of the Console panel
   */
  allLevels: 'All levels',
  /**
   * @description Text in Console View of the Console panel
   */
  defaultLevels: 'Default levels',
  /**
   * @description Text in Console View of the Console panel
   */
  hideAll: 'Hide all',
  /**
   * @description Title of level menu button in console view of the console panel
   * @example {All levels} PH1
   */
  logLevelS: 'Log level: {PH1}',
  /**
   * @description A context menu item in the Console View of the Console panel
   */
  default: 'Default',
  /**
   * @description Text summary to indicate total number of messages in console for accessibility/screen readers.
   * @example {5} PH1
   */
  filteredMessagesInConsole: '{PH1} messages in console',

} as const;
const str_ = i18n.i18n.registerUIStrings('panels/console/ConsoleView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
let consoleViewInstance: ConsoleView|null;

const MIN_HISTORY_LENGTH_FOR_DISABLING_SELF_XSS_WARNING = 5;
const DISCLAIMER_TOOLTIP_ID = 'console-ai-code-completion-disclaimer-tooltip';
const SPINNER_TOOLTIP_ID = 'console-ai-code-completion-spinner-tooltip';
const CITATIONS_TOOLTIP_ID = 'console-ai-code-completion-citations-tooltip';

export class ConsoleView extends UI.Widget.VBox implements
    UI.SearchableView.Searchable, ConsoleViewportProvider,
    SDK.TargetManager.SDKModelObserver<SDK.ConsoleModel.ConsoleModel> {
  readonly #searchableView: UI.SearchableView.SearchableView;
  private readonly sidebar: ConsoleSidebar;
  private isSidebarOpen: boolean;
  private filter: ConsoleViewFilter;
  private readonly consoleToolbarContainer: HTMLElement;
  private readonly splitWidget: UI.SplitWidget.SplitWidget;
  private readonly contentsElement: typeof UI.Widget.Widget.prototype.element;
  private visibleViewMessages: ConsoleViewMessage[];
  private hiddenByFilterCount: number;
  private shouldBeHiddenCache: Set<ConsoleViewMessage>;
  private lastShownHiddenByFilterCount!: number;
  private currentMatchRangeIndex!: number;
  private searchRegex!: RegExp|null;
  private groupableMessages: Map<string, ConsoleViewMessage[]>;
  private readonly groupableMessageTitle: Map<string, ConsoleViewMessage>;
  private readonly shortcuts: Map<number, (e: KeyboardEvent) => void>;
  private regexMatchRanges: RegexMatchRange[];
  private readonly consoleContextSelector: ConsoleContextSelector;
  private readonly filterStatusText: UI.Toolbar.ToolbarText;
  private readonly showSettingsPaneSetting: Common.Settings.Setting<boolean>;
  private readonly showSettingsPaneButton: UI.Toolbar.ToolbarSettingToggle;
  private readonly progressToolbarItem: UI.Toolbar.ToolbarItem;
  private readonly groupSimilarSetting: Common.Settings.Setting<boolean>;
  private readonly showCorsErrorsSetting: Common.Settings.Setting<boolean>;
  private readonly timestampsSetting: Common.Settings.Setting<unknown>;
  private readonly consoleHistoryAutocompleteSetting: Common.Settings.Setting<boolean>;
  private selfXssWarningDisabledSetting: Common.Settings.Setting<boolean>;
  readonly pinPane: ConsolePinPane;
  private viewport: ConsoleViewport;
  private messagesElement: HTMLElement;
  private messagesCountElement: HTMLElement;
  private viewportThrottler: Common.Throttler.Throttler;
  private pendingBatchResize: boolean;
  private readonly onMessageResizedBound:
      (e: Common.EventTarget.EventTargetEvent<HTMLElement|UI.TreeOutline.TreeElement>) => void;
  private readonly promptElement: HTMLElement;
  private readonly linkifier: Components.Linkifier.Linkifier;
  private consoleMessages: ConsoleViewMessage[];
  private consoleGroupStarts: ConsoleGroupViewMessage[];
  private prompt: ConsolePrompt;
  private immediatelyFilterMessagesForTest?: boolean;
  private maybeDirtyWhileMuted?: boolean;
  private scheduledRefreshPromiseForTest?: Promise<void>;
  private needsFullUpdate?: boolean;
  private buildHiddenCacheTimeout?: number;
  private searchShouldJumpBackwards?: boolean;
  private searchProgressIndicator?: UI.ProgressIndicator.ProgressIndicator;
  #searchTimeoutId?: number;
  private muteViewportUpdates?: boolean;
  private waitForScrollTimeout?: number;
  private issueCounter: IssueCounter.IssueCounter.IssueCounter;
  private pendingSidebarMessages: ConsoleViewMessage[] = [];
  private userHasOpenedSidebarAtLeastOnce = false;
  private issueToolbarThrottle: Common.Throttler.Throttler;
  private requestResolver = new Logs.RequestResolver.RequestResolver();
  private issueResolver = new IssuesManager.IssueResolver.IssueResolver();
  #isDetached = false;
  #onIssuesCountUpdateBound = this.#onIssuesCountUpdate.bind(this);
  aiCodeCompletionConfig?: TextEditor.AiCodeCompletionProvider.AiCodeCompletionConfig;
  private aiCodeCompletionSummaryToolbarContainer?: HTMLElement;
  private aiCodeCompletionSummaryToolbar?: AiCodeCompletionSummaryToolbar;

  constructor(viewportThrottlerTimeout: number) {
    super();
    this.setMinimumSize(0, 35);
    this.registerRequiredCSS(consoleViewStyles, objectValueStyles, CodeHighlighter.codeHighlighterStyles);

    this.#searchableView = new UI.SearchableView.SearchableView(this, null);
    this.#searchableView.element.classList.add('console-searchable-view');
    this.#searchableView.setPlaceholder(i18nString(UIStrings.findStringInLogs));
    this.#searchableView.setMinimalSearchQuerySize(0);
    this.sidebar = new ConsoleSidebar();
    this.sidebar.addEventListener(Events.FILTER_SELECTED, this.onFilterChanged.bind(this));
    this.isSidebarOpen = false;
    this.filter = new ConsoleViewFilter(this.onFilterChanged.bind(this));

    this.consoleToolbarContainer = this.element.createChild('div', 'console-toolbar-container');
    this.consoleToolbarContainer.role = 'toolbar';
    this.splitWidget = new UI.SplitWidget.SplitWidget(
        true /* isVertical */, false /* secondIsSidebar */, 'console.sidebar.width', 100);
    this.splitWidget.setMainWidget(this.#searchableView);
    this.splitWidget.setSidebarWidget(this.sidebar);
    this.splitWidget.show(this.element);
    this.splitWidget.hideSidebar();
    this.splitWidget.enableShowModeSaving();
    this.isSidebarOpen = this.splitWidget.showMode() === UI.SplitWidget.ShowMode.BOTH;
    this.filter.setLevelMenuOverridden(this.isSidebarOpen);
    this.splitWidget.addEventListener(UI.SplitWidget.Events.SHOW_MODE_CHANGED, event => {
      this.isSidebarOpen = event.data === UI.SplitWidget.ShowMode.BOTH;

      if (this.isSidebarOpen) {
        if (!this.userHasOpenedSidebarAtLeastOnce) {
          /**
           * We only want to know if the user opens the sidebar once, not how
           * many times in a given session they might open and close it, hence
           * the userHasOpenedSidebarAtLeastOnce variable to track this.
           */
          Host.userMetrics.actionTaken(Host.UserMetrics.Action.ConsoleSidebarOpened);
          this.userHasOpenedSidebarAtLeastOnce = true;
        }

        // If the user has now opened the sidebar, we need to update it, so send
        // through all the pending messages.
        this.pendingSidebarMessages.forEach(message => {
          this.sidebar.onMessageAdded(message);
        });
        this.pendingSidebarMessages = [];
      }
      this.filter.setLevelMenuOverridden(this.isSidebarOpen);
      this.onFilterChanged();
    });
    this.contentsElement = this.#searchableView.element;
    this.element.classList.add('console-view');

    this.visibleViewMessages = [];
    this.hiddenByFilterCount = 0;
    this.shouldBeHiddenCache = new Set();

    this.groupableMessages = new Map();
    this.groupableMessageTitle = new Map();
    this.shortcuts = new Map();

    this.regexMatchRanges = [];

    this.consoleContextSelector = new ConsoleContextSelector();

    this.filterStatusText = new UI.Toolbar.ToolbarText();
    this.filterStatusText.element.classList.add('dimmed');
    this.showSettingsPaneSetting =
        Common.Settings.Settings.instance().createSetting('console-show-settings-toolbar', false);
    this.showSettingsPaneButton = new UI.Toolbar.ToolbarSettingToggle(
        this.showSettingsPaneSetting, 'gear', i18nString(UIStrings.consoleSettings), 'gear-filled');
    this.showSettingsPaneButton.element.setAttribute(
        'jslog', `${VisualLogging.toggleSubpane('console-settings').track({click: true})}`);
    this.progressToolbarItem = new UI.Toolbar.ToolbarItem(document.createElement('div'));
    this.groupSimilarSetting = Common.Settings.Settings.instance().moduleSetting('console-group-similar');
    this.groupSimilarSetting.addChangeListener(() => this.updateMessageList());

    this.showCorsErrorsSetting = Common.Settings.Settings.instance().moduleSetting('console-shows-cors-errors');
    this.showCorsErrorsSetting.addChangeListener(() => this.updateMessageList());

    const toolbar = this.consoleToolbarContainer.createChild('devtools-toolbar', 'console-main-toolbar');
    toolbar.setAttribute('jslog', `${VisualLogging.toolbar()}`);
    toolbar.role = 'presentation';
    toolbar.wrappable = true;
    toolbar.appendToolbarItem(this.splitWidget.createShowHideSidebarButton(
        i18nString(UIStrings.showConsoleSidebar), i18nString(UIStrings.hideConsoleSidebar),
        i18nString(UIStrings.consoleSidebarShown), i18nString(UIStrings.consoleSidebarHidden), 'console-sidebar'));
    toolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton('console.clear'));
    toolbar.appendSeparator();
    toolbar.appendToolbarItem(this.consoleContextSelector.toolbarItem());
    toolbar.appendSeparator();
    const liveExpressionButton = UI.Toolbar.Toolbar.createActionButton('console.create-pin');
    toolbar.appendToolbarItem(liveExpressionButton);
    toolbar.appendSeparator();
    toolbar.appendToolbarItem(this.filter.textFilterUI);
    toolbar.appendToolbarItem(this.filter.levelMenuButton);
    toolbar.appendToolbarItem(this.filter.levelMenuButtonInfo);
    toolbar.appendToolbarItem(this.progressToolbarItem);
    toolbar.appendSeparator();
    this.issueCounter = new IssueCounter.IssueCounter.IssueCounter();
    this.issueCounter.id = 'console-issues-counter';
    this.issueCounter.setAttribute('jslog', `${VisualLogging.counter('issues').track({click: true})}`);
    const issuesToolbarItem = new UI.Toolbar.ToolbarItem(this.issueCounter);
    this.issueCounter.data = {
      clickHandler: () => {
        Host.userMetrics.issuesPanelOpenedFrom(Host.UserMetrics.IssueOpener.STATUS_BAR_ISSUES_COUNTER);
        void UI.ViewManager.ViewManager.instance().showView('issues-pane');
      },
      issuesManager: IssuesManager.IssuesManager.IssuesManager.instance(),
      accessibleName: i18nString(UIStrings.issueToolbarTooltipGeneral),
      displayMode: IssueCounter.IssueCounter.DisplayMode.OMIT_EMPTY,
    };
    toolbar.appendToolbarItem(issuesToolbarItem);
    toolbar.appendSeparator();
    toolbar.appendToolbarItem(this.filterStatusText);
    toolbar.appendToolbarItem(this.showSettingsPaneButton);

    const monitoringXHREnabledSetting = Common.Settings.Settings.instance().moduleSetting('monitoring-xhr-enabled');
    this.timestampsSetting = Common.Settings.Settings.instance().moduleSetting('console-timestamps-enabled');
    this.consoleHistoryAutocompleteSetting =
        Common.Settings.Settings.instance().moduleSetting('console-history-autocomplete');
    this.selfXssWarningDisabledSetting = Common.Settings.Settings.instance().createSetting(
        'disable-self-xss-warning', false, Common.Settings.SettingStorageType.SYNCED);

    const settingsPane = this.contentsElement.createChild('div', 'console-settings-pane');
    UI.ARIAUtils.setLabel(settingsPane, i18nString(UIStrings.consoleSettings));
    UI.ARIAUtils.markAsGroup(settingsPane);

    const consoleEagerEvalSetting = Common.Settings.Settings.instance().moduleSetting('console-eager-eval');
    const preserveConsoleLogSetting = Common.Settings.Settings.instance().moduleSetting('preserve-console-log');
    const userActivationEvalSetting = Common.Settings.Settings.instance().moduleSetting('console-user-activation-eval');
    settingsPane.append(
        SettingsUI.SettingsUI.createSettingCheckbox(
            i18nString(UIStrings.networkMessages), this.filter.networkMessagesSetting,
            this.filter.networkMessagesSetting.title()),
        SettingsUI.SettingsUI.createSettingCheckbox(
            i18nString(UIStrings.logXMLHttpRequests), monitoringXHREnabledSetting),
        SettingsUI.SettingsUI.createSettingCheckbox(
            i18nString(UIStrings.preserveLog), preserveConsoleLogSetting,
            i18nString(UIStrings.doNotClearLogOnPageReload)),
        SettingsUI.SettingsUI.createSettingCheckbox(
            consoleEagerEvalSetting.title(), consoleEagerEvalSetting,
            i18nString(UIStrings.eagerlyEvaluateTextInThePrompt)),
        SettingsUI.SettingsUI.createSettingCheckbox(
            i18nString(UIStrings.selectedContextOnly), this.filter.filterByExecutionContextSetting,
            i18nString(UIStrings.onlyShowMessagesFromTheCurrentContext)),
        SettingsUI.SettingsUI.createSettingCheckbox(
            this.consoleHistoryAutocompleteSetting.title(), this.consoleHistoryAutocompleteSetting,
            i18nString(UIStrings.autocompleteFromHistory)),
        SettingsUI.SettingsUI.createSettingCheckbox(
            this.groupSimilarSetting.title(), this.groupSimilarSetting,
            i18nString(UIStrings.groupSimilarMessagesInConsole)),
        SettingsUI.SettingsUI.createSettingCheckbox(
            userActivationEvalSetting.title(), userActivationEvalSetting,
            i18nString(UIStrings.treatEvaluationAsUserActivation)),
        SettingsUI.SettingsUI.createSettingCheckbox(
            this.showCorsErrorsSetting.title(), this.showCorsErrorsSetting,
            i18nString(UIStrings.showCorsErrorsInConsole)),
    );

    if (!this.showSettingsPaneSetting.get()) {
      settingsPane.classList.add('hidden');
    }
    this.showSettingsPaneSetting.addChangeListener(
        () => settingsPane.classList.toggle('hidden', !this.showSettingsPaneSetting.get()));

    this.pinPane = new ConsolePinPane(() => this.prompt.focus());
    this.pinPane.element.classList.add('console-view-pinpane');
    this.pinPane.element.classList.remove('flex-auto');
    this.pinPane.show(this.contentsElement);

    this.viewport = new ConsoleViewport(this);
    this.viewport.setStickToBottom(true);
    this.viewport.contentElement().classList.add('console-group', 'console-group-messages');
    this.contentsElement.appendChild(this.viewport.element);
    this.messagesElement = this.viewport.element;
    this.messagesElement.id = 'console-messages';
    this.messagesElement.classList.add('monospace');
    this.messagesElement.addEventListener('click', this.messagesClicked.bind(this), false);
    ['paste', 'clipboard-paste', 'drop'].forEach(type => {
      this.messagesElement.addEventListener(type, this.messagesPasted.bind(this), true);
    });

    this.messagesCountElement = this.consoleToolbarContainer.createChild('div', 'message-count');
    UI.ARIAUtils.markAsPoliteLiveRegion(this.messagesCountElement, false);

    this.viewportThrottler = new Common.Throttler.Throttler(viewportThrottlerTimeout);
    this.pendingBatchResize = false;
    this.onMessageResizedBound = (e: Common.EventTarget.EventTargetEvent<HTMLElement|UI.TreeOutline.TreeElement>) => {
      void this.onMessageResized(e);
    };

    this.promptElement = this.messagesElement.createChild('div', 'source-code');
    this.promptElement.id = 'console-prompt';

    // FIXME: This is a workaround for the selection machinery bug. See crbug.com/410899
    const selectAllFixer = this.messagesElement.createChild('div', 'console-view-fix-select-all');
    selectAllFixer.textContent = '.';
    UI.ARIAUtils.setHidden(selectAllFixer, true);

    this.registerShortcuts();

    this.messagesElement.addEventListener('contextmenu', this.handleContextMenuEvent.bind(this), false);

    // Filters need to be re-applied to a console message when the message's live location changes.
    // All relevant live locations are created by the same linkifier, so it is enough to subscribe to
    // the linkifiers live location change event.
    const throttler = new Common.Throttler.Throttler(100);
    const refilterMessages = (): Promise<void> => throttler.schedule(async () => this.onFilterChanged());
    this.linkifier = new Components.Linkifier.Linkifier(UI.UIUtils.MaxLengthForDisplayedURLsInConsole);
    this.linkifier.addEventListener(Components.Linkifier.Events.LIVE_LOCATION_UPDATED, refilterMessages);

    this.consoleMessages = [];
    this.consoleGroupStarts = [];

    const devtoolsLocale = i18n.DevToolsLocale.DevToolsLocale.instance();
    this.aiCodeCompletionConfig =
        AiCodeCompletion.AiCodeCompletion.AiCodeCompletion.isAiCodeCompletionEnabled(devtoolsLocale.locale) ? {
          completionContext: {
            getPrefix: this.getConsoleMessageHistory.bind(this),
            additionalFiles: [{
              path: 'devtools-console-context.js',
              content: AiCodeCompletion.AiCodeCompletion.consoleAdditionalContextFileContent,
              included_reason: Host.AidaClient.Reason.RELATED_FILE,
            }],
            stopSequences: ['\n\n'],
          },
          generationContext: {
            additionalPreambleContext: AiCodeGeneration.AiCodeGeneration.additionalContextForConsole,
          },
          onFeatureEnabled: () => {
            this.setupAiCodeCompletion();
          },
          onFeatureDisabled: () => {
            this.cleanupAiCodeCompletion();
          },
          onSuggestionAccepted: this.#onAiCodeCompletionSuggestionAccepted.bind(this),
          onRequestTriggered: this.#onAiCodeCompletionRequestTriggered.bind(this),
          onResponseReceived: this.#onAiCodeCompletionResponseReceived.bind(this),
          panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor.CONSOLE,
        } :
                                                                                                              undefined;

    this.prompt = new ConsolePrompt(this.aiCodeCompletionConfig);
    this.prompt.show(this.promptElement);
    this.prompt.element.addEventListener('keydown', this.promptKeyDown.bind(this), true);
    this.prompt.addEventListener(ConsolePromptEvents.TEXT_CHANGED, this.promptTextChanged, this);

    this.messagesElement.addEventListener('keydown', this.messagesKeyDown.bind(this), false);
    this.prompt.element.addEventListener('focusin', () => {
      if (this.isScrolledToBottom()) {
        this.viewport.setStickToBottom(true);
      }
    });

    this.consoleHistoryAutocompleteSetting.addChangeListener(this.consoleHistoryAutocompleteChanged, this);

    this.consoleHistoryAutocompleteChanged();

    this.updateFilterStatus();
    this.timestampsSetting.addChangeListener(this.consoleTimestampsSettingChanged, this);

    this.registerWithMessageSink();

    UI.Context.Context.instance().addFlavorChangeListener(
        SDK.RuntimeModel.ExecutionContext, this.executionContextChanged, this);

    this.messagesElement.addEventListener(
        'mousedown', (event: Event) => this.updateStickToBottomOnPointerDown((event as MouseEvent).button === 2),
        false);
    this.messagesElement.addEventListener('mouseup', this.updateStickToBottomOnPointerUp.bind(this), false);
    this.messagesElement.addEventListener('mouseleave', this.updateStickToBottomOnPointerUp.bind(this), false);
    this.messagesElement.addEventListener('wheel', this.updateStickToBottomOnWheel.bind(this), false);
    this.messagesElement.addEventListener('touchstart', this.updateStickToBottomOnPointerDown.bind(this, false), false);
    this.messagesElement.addEventListener('touchend', this.updateStickToBottomOnPointerUp.bind(this), false);
    this.messagesElement.addEventListener('touchcancel', this.updateStickToBottomOnPointerUp.bind(this), false);

    SDK.TargetManager.TargetManager.instance().addModelListener(
        SDK.ConsoleModel.ConsoleModel, SDK.ConsoleModel.Events.ConsoleCleared, this.consoleCleared, this,
        {scoped: true});
    SDK.TargetManager.TargetManager.instance().addModelListener(
        SDK.ConsoleModel.ConsoleModel, SDK.ConsoleModel.Events.MessageAdded, this.onConsoleMessageAdded, this,
        {scoped: true});
    SDK.TargetManager.TargetManager.instance().addModelListener(
        SDK.ConsoleModel.ConsoleModel, SDK.ConsoleModel.Events.MessageUpdated, this.onConsoleMessageUpdated, this,
        {scoped: true});
    SDK.TargetManager.TargetManager.instance().addModelListener(
        SDK.ConsoleModel.ConsoleModel, SDK.ConsoleModel.Events.CommandEvaluated, this.commandEvaluated, this,
        {scoped: true});
    SDK.TargetManager.TargetManager.instance().observeModels(SDK.ConsoleModel.ConsoleModel, this, {scoped: true});

    const issuesManager = IssuesManager.IssuesManager.IssuesManager.instance();
    this.issueToolbarThrottle = new Common.Throttler.Throttler(100);
    issuesManager.addEventListener(
        IssuesManager.IssuesManager.Events.ISSUES_COUNT_UPDATED, this.#onIssuesCountUpdateBound);
  }

  static clearConsoleViewInstanceForTest(): void {
    consoleViewInstance = null;
  }

  static instance(opts?: {forceNew: boolean, viewportThrottlerTimeout?: number}): ConsoleView {
    if (!consoleViewInstance || opts?.forceNew) {
      consoleViewInstance = new ConsoleView(opts?.viewportThrottlerTimeout ?? 50);
    }
    return consoleViewInstance;
  }

  createAiCodeCompletionSummaryToolbar(): void {
    if (this.aiCodeCompletionSummaryToolbar) {
      return;
    }
    this.aiCodeCompletionSummaryToolbar = new AiCodeCompletionSummaryToolbar({
      citationsTooltipId: CITATIONS_TOOLTIP_ID,
      disclaimerTooltipId: DISCLAIMER_TOOLTIP_ID,
      spinnerTooltipId: SPINNER_TOOLTIP_ID,
      panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor.CONSOLE,
    });
    this.aiCodeCompletionSummaryToolbarContainer =
        this.element.createChild('div', 'ai-code-completion-summary-toolbar-container');
    this.aiCodeCompletionSummaryToolbar.show(this.aiCodeCompletionSummaryToolbarContainer, undefined, true);
  }

  #onAiCodeCompletionSuggestionAccepted(citations: Host.AidaClient.Citation[]): void {
    if (!this.aiCodeCompletionSummaryToolbar || citations.length === 0) {
      return;
    }
    const citationsUri = citations.map(citation => citation.uri).filter((uri): uri is string => Boolean(uri));
    this.aiCodeCompletionSummaryToolbar.updateCitations(citationsUri);
  }

  #onAiCodeCompletionRequestTriggered(): void {
    this.aiCodeCompletionSummaryToolbar?.setLoading(true);
  }

  #onAiCodeCompletionResponseReceived(): void {
    this.aiCodeCompletionSummaryToolbar?.setLoading(false);
  }

  clearConsole(): void {
    SDK.ConsoleModel.ConsoleModel.requestClearMessages();
    this.prompt.clearAiCodeCompletionCache();
  }

  #onIssuesCountUpdate(): void {
    void this.issueToolbarThrottle.schedule(async () => this.updateIssuesToolbarItem());
    this.issuesCountUpdatedForTest();
  }

  issuesCountUpdatedForTest(): void {
  }

  modelAdded(model: SDK.ConsoleModel.ConsoleModel): void {
    model.messages().forEach(this.addConsoleMessage, this);
  }

  modelRemoved(model: SDK.ConsoleModel.ConsoleModel): void {
    if (!Common.Settings.Settings.instance().moduleSetting('preserve-console-log').get() &&
        model.target().outermostTarget() === model.target()) {
      this.consoleCleared();
    }
  }

  private onFilterChanged(): void {
    this.filter.currentFilter.levelsMask =
        this.isSidebarOpen ? ConsoleFilter.allLevelsFilterValue() : this.filter.messageLevelFiltersSetting.get();
    this.cancelBuildHiddenCache();
    if (this.immediatelyFilterMessagesForTest) {
      for (const viewMessage of this.consoleMessages) {
        this.computeShouldMessageBeVisible(viewMessage);
      }
      this.updateMessageList();
      return;
    }
    this.buildHiddenCache(0, this.consoleMessages.slice());
  }

  protected setImmediatelyFilterMessagesForTest(): void {
    this.immediatelyFilterMessagesForTest = true;
  }

  searchableView(): UI.SearchableView.SearchableView {
    return this.#searchableView;
  }

  clearHistory(): void {
    this.prompt.history().clear();
    this.prompt.clearAiCodeCompletionCache();
  }

  private consoleHistoryAutocompleteChanged(): void {
    this.prompt.setAddCompletionsFromHistory(this.consoleHistoryAutocompleteSetting.get());
  }

  itemCount(): number {
    return this.visibleViewMessages.length;
  }

  itemElement(index: number): ConsoleViewportElement|null {
    return this.visibleViewMessages[index];
  }

  fastHeight(index: number): number {
    return this.visibleViewMessages[index].fastHeight();
  }

  minimumRowHeight(): number {
    return 16;
  }

  private registerWithMessageSink(): void {
    Common.Console.Console.instance().messages().forEach(this.addSinkMessage, this);
    Common.Console.Console.instance().addEventListener(Common.Console.Events.MESSAGE_ADDED, ({data: message}) => {
      this.addSinkMessage(message);
    }, this);
  }

  private addSinkMessage(message: Common.Console.Message): void {
    let level: Protocol.Log.LogEntryLevel = Protocol.Log.LogEntryLevel.Verbose;
    switch (message.level) {
      case Common.Console.MessageLevel.INFO:
        level = Protocol.Log.LogEntryLevel.Info;
        break;
      case Common.Console.MessageLevel.ERROR:
        level = Protocol.Log.LogEntryLevel.Error;
        break;
      case Common.Console.MessageLevel.WARNING:
        level = Protocol.Log.LogEntryLevel.Warning;
        break;
    }

    const source = message.source || Protocol.Log.LogEntrySource.Other;
    const consoleMessage = new SDK.ConsoleModel.ConsoleMessage(
        null, source, level, message.text,
        {type: SDK.ConsoleModel.FrontendMessageType.System, timestamp: message.timestamp});
    this.addConsoleMessage(consoleMessage);
  }

  private consoleTimestampsSettingChanged(): void {
    this.updateMessageList();
    this.consoleMessages.forEach(viewMessage => viewMessage.updateTimestamp());
    this.groupableMessageTitle.forEach(viewMessage => viewMessage.updateTimestamp());
  }

  private executionContextChanged(): void {
    this.prompt.clearAutocomplete();
  }

  override willHide(): void {
    super.willHide();
    this.hidePromptSuggestBox();
  }

  dispose(): void {
    SDK.TargetManager.TargetManager.instance().removeModelListener(
        SDK.ConsoleModel.ConsoleModel, SDK.ConsoleModel.Events.ConsoleCleared, this.consoleCleared, this);
    SDK.TargetManager.TargetManager.instance().removeModelListener(
        SDK.ConsoleModel.ConsoleModel, SDK.ConsoleModel.Events.MessageAdded, this.onConsoleMessageAdded, this);
    SDK.TargetManager.TargetManager.instance().removeModelListener(
        SDK.ConsoleModel.ConsoleModel, SDK.ConsoleModel.Events.MessageUpdated, this.onConsoleMessageUpdated, this);
    SDK.TargetManager.TargetManager.instance().removeModelListener(
        SDK.ConsoleModel.ConsoleModel, SDK.ConsoleModel.Events.CommandEvaluated, this.commandEvaluated, this);
    SDK.TargetManager.TargetManager.instance().unobserveModels(SDK.ConsoleModel.ConsoleModel, this);

    const issuesManager = IssuesManager.IssuesManager.IssuesManager.instance();
    issuesManager.removeEventListener(
        IssuesManager.IssuesManager.Events.ISSUES_COUNT_UPDATED, this.#onIssuesCountUpdateBound);
  }

  override wasShown(): void {
    super.wasShown();
    if (this.#isDetached) {
      const issuesManager = IssuesManager.IssuesManager.IssuesManager.instance();
      issuesManager.addEventListener(
          IssuesManager.IssuesManager.Events.ISSUES_COUNT_UPDATED, this.#onIssuesCountUpdateBound);
    }
    this.#isDetached = false;
    this.updateIssuesToolbarItem();
    this.viewport.refresh();
  }

  override focus(): void {
    if (this.viewport.hasVirtualSelection()) {
      (this.viewport.contentElement() as HTMLElement).focus();
    } else {
      this.focusPrompt();
    }
  }

  focusPrompt(): void {
    if (!this.prompt.hasFocus()) {
      const oldStickToBottom = this.viewport.stickToBottom();
      const oldScrollTop = this.viewport.element.scrollTop;
      this.prompt.focus();
      this.viewport.setStickToBottom(oldStickToBottom);
      this.viewport.element.scrollTop = oldScrollTop;
    }
  }

  override restoreScrollPositions(): void {
    if (this.viewport.stickToBottom()) {
      this.immediatelyScrollToBottom();
    } else {
      super.restoreScrollPositions();
    }
  }

  override onResize(): void {
    this.scheduleViewportRefresh();
    this.hidePromptSuggestBox();
    if (this.viewport.stickToBottom()) {
      this.immediatelyScrollToBottom();
    }
    for (let i = 0; i < this.visibleViewMessages.length; ++i) {
      this.visibleViewMessages[i].onResize();
    }
  }

  private hidePromptSuggestBox(): void {
    this.prompt.clearAutocomplete();
  }

  private async invalidateViewport(): Promise<void> {
    this.updateIssuesToolbarItem();
    if (this.muteViewportUpdates) {
      this.maybeDirtyWhileMuted = true;
      return;
    }
    if (this.needsFullUpdate) {
      this.updateMessageList();
      delete this.needsFullUpdate;
    } else {
      this.viewport.invalidate();
    }
    return;
  }

  override onDetach(): void {
    this.#isDetached = true;
    const issuesManager = IssuesManager.IssuesManager.IssuesManager.instance();
    issuesManager.removeEventListener(
        IssuesManager.IssuesManager.Events.ISSUES_COUNT_UPDATED, this.#onIssuesCountUpdateBound);
  }

  private updateIssuesToolbarItem(): void {
    if (this.#isDetached) {
      return;
    }
    const manager = IssuesManager.IssuesManager.IssuesManager.instance();
    const issueEnumeration = IssueCounter.IssueCounter.getIssueCountsEnumeration(manager);
    const issuesTitleGotoIssues = manager.numberOfIssues() === 0 ?
        i18nString(UIStrings.issueToolbarClickToGoToTheIssuesTab) :
        i18nString(UIStrings.issueToolbarClickToView, {issueEnumeration});
    const issuesTitleGeneral = i18nString(UIStrings.issueToolbarTooltipGeneral);
    const issuesTitle = `${issuesTitleGeneral} ${issuesTitleGotoIssues}`;
    UI.Tooltip.Tooltip.install(this.issueCounter, issuesTitle);
    this.issueCounter.data = {
      ...this.issueCounter.data,
      leadingText: i18nString(UIStrings.issuesWithColon, {n: manager.numberOfIssues()}),
      accessibleName: issuesTitle,
    };
  }

  private scheduleViewportRefresh(): void {
    if (this.muteViewportUpdates) {
      this.maybeDirtyWhileMuted = true;
      return;
    }

    this.scheduledRefreshPromiseForTest = this.viewportThrottler.schedule(this.invalidateViewport.bind(this));
  }

  getScheduledRefreshPromiseForTest(): Promise<void>|undefined {
    return this.scheduledRefreshPromiseForTest;
  }

  private immediatelyScrollToBottom(): void {
    // This will scroll viewport and trigger its refresh.
    this.viewport.setStickToBottom(true);
    this.promptElement.scrollIntoView(true);
  }

  private updateFilterStatus(): void {
    if (this.hiddenByFilterCount === this.lastShownHiddenByFilterCount) {
      return;
    }
    this.filterStatusText.setText(i18nString(UIStrings.sHidden, {n: this.hiddenByFilterCount}));
    this.filterStatusText.setVisible(Boolean(this.hiddenByFilterCount));
    this.lastShownHiddenByFilterCount = this.hiddenByFilterCount;
  }

  private onConsoleMessageAdded(event: Common.EventTarget.EventTargetEvent<SDK.ConsoleModel.ConsoleMessage>): void {
    const message = event.data;
    this.addConsoleMessage(message);
  }

  private addConsoleMessage(message: SDK.ConsoleModel.ConsoleMessage): void {
    const viewMessage = this.createViewMessage(message);
    consoleMessageToViewMessage.set(message, viewMessage);
    if (message.type === SDK.ConsoleModel.FrontendMessageType.Command ||
        message.type === SDK.ConsoleModel.FrontendMessageType.Result) {
      const lastMessage = this.consoleMessages[this.consoleMessages.length - 1];
      const newTimestamp = lastMessage && messagesSortedBySymbol.get(lastMessage) || 0;
      messagesSortedBySymbol.set(viewMessage, newTimestamp);
    } else {
      messagesSortedBySymbol.set(viewMessage, viewMessage.consoleMessage().timestamp);
    }

    let insertAt;
    if (!this.consoleMessages.length ||
        timeComparator(viewMessage, this.consoleMessages[this.consoleMessages.length - 1]) > 0) {
      insertAt = this.consoleMessages.length;
    } else {
      insertAt = Platform.ArrayUtilities.upperBound(this.consoleMessages, viewMessage, timeComparator);
    }
    const insertedInMiddle = insertAt < this.consoleMessages.length;
    this.consoleMessages.splice(insertAt, 0, viewMessage);

    if (message.type === SDK.ConsoleModel.FrontendMessageType.Command) {
      this.prompt.history().pushHistoryItem(message.messageText);
      if (this.prompt.history().length() >= MIN_HISTORY_LENGTH_FOR_DISABLING_SELF_XSS_WARNING &&
          !this.selfXssWarningDisabledSetting.get()) {
        this.selfXssWarningDisabledSetting.set(true);
      }
    } else if (message.type !== SDK.ConsoleModel.FrontendMessageType.Result) {
      // Maintain group tree.
      // Find parent group.
      const consoleGroupStartIndex =
          Platform.ArrayUtilities.upperBound(this.consoleGroupStarts, viewMessage, timeComparator) - 1;
      if (consoleGroupStartIndex >= 0) {
        const currentGroup = this.consoleGroupStarts[consoleGroupStartIndex];
        addToGroup(viewMessage, currentGroup);
      }
      // Add new group.
      if (message.isGroupStartMessage()) {
        insertAt = Platform.ArrayUtilities.upperBound(this.consoleGroupStarts, viewMessage, timeComparator);
        this.consoleGroupStarts.splice(insertAt, 0, viewMessage as ConsoleGroupViewMessage);
      }
    }

    this.filter.onMessageAdded(message);
    if (this.isSidebarOpen) {
      this.sidebar.onMessageAdded(viewMessage);
    } else {
      this.pendingSidebarMessages.push(viewMessage);
    }

    // If we already have similar messages, go slow path.
    let shouldGoIntoGroup = false;
    const shouldGroupSimilar = this.groupSimilarSetting.get();
    if (message.isGroupable()) {
      const groupKey = viewMessage.groupKey();
      shouldGoIntoGroup = shouldGroupSimilar && this.groupableMessages.has(groupKey);
      let list = this.groupableMessages.get(groupKey);
      if (!list) {
        list = [];
        this.groupableMessages.set(groupKey, list);
      }
      list.push(viewMessage);
    }

    this.computeShouldMessageBeVisible(viewMessage);
    if (!shouldGoIntoGroup && !insertedInMiddle) {
      this.appendMessageToEnd(
          viewMessage,
          !shouldGroupSimilar /* crbug.com/1082963: prevent collapse of same messages when "Group similar" is false */);
      this.updateFilterStatus();
      this.#searchableView.updateSearchMatchesCount(this.regexMatchRanges.length);
    } else {
      this.needsFullUpdate = true;
    }

    this.scheduleViewportRefresh();
    this.consoleMessageAddedForTest(viewMessage);

    /** Figure out whether the message should belong into this group or the parent group based on group end timestamp. **/
    function addToGroup(viewMessage: ConsoleViewMessage, currentGroup: ConsoleGroupViewMessage): void {
      const currentEnd = currentGroup.groupEnd();
      if (currentEnd !== null) {
        // Exceeds this group's end. It should belong into parent group.
        if (timeComparator(viewMessage, currentEnd) > 0) {
          const parent = currentGroup.consoleGroup();
          // No parent group. We reached ungrouped messages. Don't establish group links.
          if (parent === null) {
            return;
          }  // Add to parent group.
          addToGroup(viewMessage, parent);
          return;
        }
      }
      // Add message to this group, and set group of the message.
      if (viewMessage.consoleMessage().type === Protocol.Runtime.ConsoleAPICalledEventType.EndGroup) {
        currentGroup.setGroupEnd(viewMessage);
      } else {
        viewMessage.setConsoleGroup(currentGroup);
      }
    }

    function timeComparator(viewMessage1: ConsoleViewMessage, viewMessage2: ConsoleViewMessage): number {
      return (messagesSortedBySymbol.get(viewMessage1) || 0) - (messagesSortedBySymbol.get(viewMessage2) || 0);
    }
  }

  private onConsoleMessageUpdated(event: Common.EventTarget.EventTargetEvent<SDK.ConsoleModel.ConsoleMessage>): void {
    const message = event.data;
    const viewMessage = consoleMessageToViewMessage.get(message);
    if (viewMessage) {
      viewMessage.updateMessageElement();
      this.computeShouldMessageBeVisible(viewMessage);
      this.updateMessageList();
    }
  }

  private consoleMessageAddedForTest(_viewMessage: ConsoleViewMessage): void {
  }

  private shouldMessageBeVisible(viewMessage: ConsoleViewMessage): boolean {
    return !this.shouldBeHiddenCache.has(viewMessage);
  }

  private computeShouldMessageBeVisible(viewMessage: ConsoleViewMessage): void {
    if (this.filter.shouldBeVisible(viewMessage) &&
        (!this.isSidebarOpen || this.sidebar.shouldBeVisible(viewMessage))) {
      this.shouldBeHiddenCache.delete(viewMessage);
    } else {
      this.shouldBeHiddenCache.add(viewMessage);
    }
  }

  private appendMessageToEnd(viewMessage: ConsoleViewMessage, preventCollapse?: boolean): void {
    if (viewMessage.consoleMessage().category === Protocol.Log.LogEntryCategory.Cors &&
        !this.showCorsErrorsSetting.get()) {
      return;
    }

    const lastMessage = this.visibleViewMessages[this.visibleViewMessages.length - 1];
    if (viewMessage.consoleMessage().type === Protocol.Runtime.ConsoleAPICalledEventType.EndGroup) {
      if (lastMessage) {
        const group = lastMessage.consoleGroup();
        if (group && !group.messagesHidden()) {
          lastMessage.incrementCloseGroupDecorationCount();
        }
      }
      return;
    }

    if (!this.shouldMessageBeVisible(viewMessage)) {
      this.hiddenByFilterCount++;
      return;
    }

    if (!preventCollapse &&
        this.tryToCollapseMessages(viewMessage, this.visibleViewMessages[this.visibleViewMessages.length - 1])) {
      return;
    }

    const currentGroup = viewMessage.consoleGroup();
    showGroup(currentGroup, this.visibleViewMessages);

    if (!currentGroup?.messagesHidden()) {
      const originatingMessage = viewMessage.consoleMessage().originatingMessage();
      const adjacent = Boolean(originatingMessage && lastMessage?.consoleMessage() === originatingMessage);
      viewMessage.setAdjacentUserCommandResult(adjacent);
      this.visibleViewMessages.push(viewMessage);
      this.searchMessage(this.visibleViewMessages.length - 1);
    }

    this.messageAppendedForTests();

    /** Show the group the message belongs to, and also show parent groups. **/
    function showGroup(currentGroup: ConsoleGroupViewMessage|null, visibleViewMessages: ConsoleViewMessage[]): void {
      if (currentGroup === null) {
        return;
      }
      // Group is already being shown, no need to traverse to
      // parent groups since they are also already being shown.
      if (visibleViewMessages.includes(currentGroup)) {
        return;
      }
      const parentGroup = currentGroup.consoleGroup();
      if (parentGroup) {
        showGroup(parentGroup, visibleViewMessages);
      }
      if (!parentGroup?.messagesHidden()) {
        visibleViewMessages.push(currentGroup);
      }
    }
  }

  private messageAppendedForTests(): void {
    // This method is sniffed in tests.
  }

  private createViewMessage(message: SDK.ConsoleModel.ConsoleMessage): ConsoleViewMessage {
    switch (message.type) {
      case SDK.ConsoleModel.FrontendMessageType.Command:
        return new ConsoleCommand(
            message, this.linkifier, this.requestResolver, this.issueResolver, this.onMessageResizedBound);
      case SDK.ConsoleModel.FrontendMessageType.Result:
        return new ConsoleCommandResult(
            message, this.linkifier, this.requestResolver, this.issueResolver, this.onMessageResizedBound);
      case Protocol.Runtime.ConsoleAPICalledEventType.StartGroupCollapsed:
      case Protocol.Runtime.ConsoleAPICalledEventType.StartGroup:
        return new ConsoleGroupViewMessage(
            message, this.linkifier, this.requestResolver, this.issueResolver, this.updateMessageList.bind(this),
            this.onMessageResizedBound);
      case Protocol.Runtime.ConsoleAPICalledEventType.Table:
        return new ConsoleTableMessageView(
            message, this.linkifier, this.requestResolver, this.issueResolver, this.onMessageResizedBound);
      default:
        return new ConsoleViewMessage(
            message, this.linkifier, this.requestResolver, this.issueResolver, this.onMessageResizedBound);
    }
  }

  private async onMessageResized(event: Common.EventTarget.EventTargetEvent<HTMLElement|UI.TreeOutline.TreeElement>):
      Promise<void> {
    const treeElement = event.data instanceof UI.TreeOutline.TreeElement ? event.data.treeOutline?.element : event.data;
    if (this.pendingBatchResize || !treeElement) {
      return;
    }
    this.pendingBatchResize = true;
    await Promise.resolve();
    this.viewport.setStickToBottom(this.isScrolledToBottom());
    // Scroll, in case mutations moved the element below the visible area.
    if (treeElement.offsetHeight <= this.messagesElement.offsetHeight) {
      treeElement.scrollIntoViewIfNeeded();
    }

    this.pendingBatchResize = false;
  }

  getConsoleMessageHistory(): string {
    const currentExecutionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext);
    let consoleMessages = '';
    if (currentExecutionContext) {
      const consoleModel = currentExecutionContext.target().model(SDK.ConsoleModel.ConsoleModel);
      if (consoleModel) {
        let lastMessage = '';
        for (const message of consoleModel.messages()) {
          if (message.type !== SDK.ConsoleModel.FrontendMessageType.Command || message.messageText === lastMessage) {
            continue;
          }
          lastMessage = message.messageText;
          consoleMessages = consoleMessages + message.messageText + '\n\n';
        }
      }
    }
    return consoleMessages;
  }

  private consoleCleared(): void {
    const hadFocus = this.viewport.element.hasFocus();
    this.cancelBuildHiddenCache();
    this.currentMatchRangeIndex = -1;
    this.consoleMessages = [];
    this.groupableMessages.clear();
    this.groupableMessageTitle.clear();
    this.sidebar.clear();
    this.pendingSidebarMessages = [];
    this.updateMessageList();
    this.hidePromptSuggestBox();
    this.viewport.setStickToBottom(true);
    this.linkifier.reset();
    this.filter.clear();
    this.requestResolver.clear();
    this.consoleGroupStarts = [];
    this.aiCodeCompletionSummaryToolbar?.clearCitations();
    if (hadFocus) {
      this.prompt.focus();
    }
    UI.ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.consoleCleared));
  }

  private handleContextMenuEvent(event: Event): void {
    const contextMenu = new UI.ContextMenu.ContextMenu(event);
    const eventTarget = (event.target as Node);
    if (eventTarget.isSelfOrDescendant(this.promptElement)) {
      void contextMenu.show();
      return;
    }

    const sourceElement = eventTarget.enclosingNodeOrSelfWithClass('console-message-wrapper');
    const consoleViewMessage = sourceElement && getMessageForElement(sourceElement);
    const consoleMessage = consoleViewMessage ? consoleViewMessage.consoleMessage() : null;

    if (consoleViewMessage) {
      UI.Context.Context.instance().setFlavor(ConsoleViewMessage, consoleViewMessage);
    }

    if (consoleMessage && !consoleViewMessage?.element()?.matches('.has-insight') &&
        consoleViewMessage?.shouldShowInsights()) {
      contextMenu.headerSection().appendAction(
          consoleViewMessage?.getExplainActionId(), undefined, /* optional=*/ true);
    }

    if (consoleMessage?.url) {
      const menuTitle = i18nString(
          UIStrings.hideMessagesFromS, {PH1: new Common.ParsedURL.ParsedURL(consoleMessage.url).displayName});
      contextMenu.headerSection().appendItem(
          menuTitle, this.filter.addMessageURLFilter.bind(this.filter, consoleMessage.url),
          {jslogContext: 'hide-messages-from'});
    }

    contextMenu.defaultSection().appendAction('console.clear');
    contextMenu.defaultSection().appendAction('console.clear.history');
    contextMenu.saveSection().appendItem(
        i18nString(UIStrings.copyConsole), this.copyConsole.bind(this), {jslogContext: 'copy-console'});
    contextMenu.saveSection().appendItem(
        i18nString(UIStrings.saveAs), this.saveConsole.bind(this), {jslogContext: 'save-as'});
    if (this.element.hasSelection()) {
      contextMenu.clipboardSection().appendItem(
          i18nString(UIStrings.copyVisibleStyledSelection), this.viewport.copyWithStyles.bind(this.viewport),
          {jslogContext: 'copy-visible-styled-selection'});
    }

    if (consoleMessage) {
      const request = Logs.NetworkLog.NetworkLog.requestForConsoleMessage(consoleMessage);
      if (request && SDK.NetworkManager.NetworkManager.canReplayRequest(request)) {
        contextMenu.debugSection().appendItem(
            i18nString(UIStrings.replayXhr), SDK.NetworkManager.NetworkManager.replayRequest.bind(null, request),
            {jslogContext: 'replay-xhr'});
      }
    }

    void contextMenu.show();
  }

  private async saveConsole(): Promise<void> {
    const url = (SDK.TargetManager.TargetManager.instance().scopeTarget() as SDK.Target.Target).inspectedURL();
    const parsedURL = Common.ParsedURL.ParsedURL.fromString(url);
    const filename =
        Platform.StringUtilities.sprintf('%s-%d.log', parsedURL ? parsedURL.host : 'console', Date.now()) as
        Platform.DevToolsPath.RawPathString;
    const stream = new Bindings.FileUtils.FileOutputStream();

    const progressIndicator = document.createElement('devtools-progress');
    progressIndicator.title = i18nString(UIStrings.writingFile);
    progressIndicator.totalWork = this.itemCount();

    const chunkSize = 350;

    if (!await stream.open(filename)) {
      return;
    }
    this.progressToolbarItem.element.appendChild(progressIndicator);

    let messageIndex = 0;
    while (messageIndex < this.itemCount() && !progressIndicator.canceled) {
      const messageContents = [];
      let i;
      for (i = 0; i < chunkSize && i + messageIndex < this.itemCount(); ++i) {
        const message = (this.itemElement(messageIndex + i) as ConsoleViewMessage);
        messageContents.push(message.toExportString());
      }
      messageIndex += i;
      await stream.write(messageContents.join('\n') + '\n');
      progressIndicator.worked = messageIndex;
    }

    void stream.close();
    progressIndicator.done = true;
  }

  private async copyConsole(): Promise<void> {
    const messageContents: string[] = [];
    for (let i = 0; i < this.itemCount(); i++) {
      const message = (this.itemElement(i) as ConsoleViewMessage);
      messageContents.push(message.toExportString());
    }
    Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(messageContents.join('\n') + '\n');
  }

  private tryToCollapseMessages(viewMessage: ConsoleViewMessage, lastMessage?: ConsoleViewMessage): boolean {
    const timestampsShown = this.timestampsSetting.get();
    if (!timestampsShown && lastMessage && !viewMessage.consoleMessage().isGroupMessage() &&
        viewMessage.consoleMessage().type !== SDK.ConsoleModel.FrontendMessageType.Command &&
        viewMessage.consoleMessage().type !== SDK.ConsoleModel.FrontendMessageType.Result &&
        viewMessage.consoleMessage().isEqual(lastMessage.consoleMessage())) {
      lastMessage.incrementRepeatCount();
      if (viewMessage.isLastInSimilarGroup()) {
        lastMessage.setInSimilarGroup(true, true);
      }
      return true;
    }

    return false;
  }

  private buildHiddenCache(startIndex: number, viewMessages: ConsoleViewMessage[]): void {
    const startTime = Date.now();
    let i;
    for (i = startIndex; i < viewMessages.length; ++i) {
      this.computeShouldMessageBeVisible(viewMessages[i]);
      if (i % 10 === 0 && Date.now() - startTime > 12) {
        break;
      }
    }

    if (i === viewMessages.length) {
      this.updateMessageList();
      return;
    }
    this.buildHiddenCacheTimeout =
        this.element.window().requestAnimationFrame(this.buildHiddenCache.bind(this, i + 1, viewMessages));
  }

  private cancelBuildHiddenCache(): void {
    this.shouldBeHiddenCache.clear();
    if (this.buildHiddenCacheTimeout) {
      this.element.window().cancelAnimationFrame(this.buildHiddenCacheTimeout);
      delete this.buildHiddenCacheTimeout;
    }
  }

  private updateMessageList(): void {
    this.regexMatchRanges = [];
    this.hiddenByFilterCount = 0;
    for (const visibleViewMessage of this.visibleViewMessages) {
      visibleViewMessage.resetCloseGroupDecorationCount();
      visibleViewMessage.resetIncrementRepeatCount();
    }
    this.visibleViewMessages = [];
    if (this.groupSimilarSetting.get()) {
      this.addGroupableMessagesToEnd();
    } else {
      for (const consoleMessage of this.consoleMessages) {
        consoleMessage.setInSimilarGroup(false);
        if (consoleMessage.consoleMessage().isGroupable()) {
          // Since grouping similar messages is disabled, we need clear the
          // reference to the artificial console group start.
          consoleMessage.clearConsoleGroup();
        }
        this.appendMessageToEnd(
            consoleMessage,
            true /* crbug.com/1082963: prevent collaps`e of same messages when "Group similar" is false */);
      }
    }
    this.updateFilterStatus();
    this.#searchableView.updateSearchMatchesCount(this.regexMatchRanges.length);
    this.highlightMatch(this.currentMatchRangeIndex, false);  // Re-highlight current match without scrolling.
    this.viewport.invalidate();
    this.messagesCountElement.setAttribute(
        'aria-label', i18nString(UIStrings.filteredMessagesInConsole, {PH1: this.visibleViewMessages.length}));
  }

  private addGroupableMessagesToEnd(): void {
    const alreadyAdded = new Set<SDK.ConsoleModel.ConsoleMessage>();
    const processedGroupKeys = new Set<string>();
    for (const viewMessage of this.consoleMessages) {
      const message = viewMessage.consoleMessage();
      if (alreadyAdded.has(message)) {
        continue;
      }

      if (!message.isGroupable()) {
        this.appendMessageToEnd(viewMessage);
        alreadyAdded.add(message);
        continue;
      }

      const key = viewMessage.groupKey();
      const viewMessagesInGroup = this.groupableMessages.get(key);
      if (!viewMessagesInGroup || viewMessagesInGroup.length < 5) {
        viewMessage.setInSimilarGroup(false);
        this.appendMessageToEnd(viewMessage);
        alreadyAdded.add(message);
        continue;
      }

      if (processedGroupKeys.has(key)) {
        continue;
      }

      if (!viewMessagesInGroup.find(x => this.shouldMessageBeVisible(x))) {
        // Optimize for speed.
        for (const viewMessageInGroup of viewMessagesInGroup) {
          alreadyAdded.add(viewMessageInGroup.consoleMessage());
        }
        processedGroupKeys.add(key);
        continue;
      }

      // Create artificial group start and end messages.
      let startGroupViewMessage = this.groupableMessageTitle.get(key);
      if (!startGroupViewMessage) {
        const startGroupMessage = new SDK.ConsoleModel.ConsoleMessage(
            null, message.source, message.level, viewMessage.groupTitle(),
            {type: Protocol.Runtime.ConsoleAPICalledEventType.StartGroupCollapsed});
        startGroupViewMessage = this.createViewMessage(startGroupMessage);
        this.groupableMessageTitle.set(key, startGroupViewMessage);
      }
      startGroupViewMessage.setRepeatCount(viewMessagesInGroup.length);
      this.appendMessageToEnd(startGroupViewMessage);

      for (const viewMessageInGroup of viewMessagesInGroup) {
        viewMessageInGroup.setInSimilarGroup(
            true, viewMessagesInGroup[viewMessagesInGroup.length - 1] === viewMessageInGroup);
        viewMessageInGroup.setConsoleGroup(startGroupViewMessage as ConsoleGroupViewMessage);
        this.appendMessageToEnd(viewMessageInGroup, true);
        alreadyAdded.add(viewMessageInGroup.consoleMessage());
      }

      const endGroupMessage = new SDK.ConsoleModel.ConsoleMessage(
          null, message.source, message.level, message.messageText,
          {type: Protocol.Runtime.ConsoleAPICalledEventType.EndGroup});
      this.appendMessageToEnd(this.createViewMessage(endGroupMessage));
    }
  }

  private messagesClicked(event: Event): void {
    const target = (event.target as Node | null);
    // Do not focus prompt if messages have selection.
    if (!this.messagesElement.hasSelection()) {
      const clickedOutsideMessageList =
          target === this.messagesElement || this.prompt.belowEditorElement().isSelfOrAncestor(target);
      if (clickedOutsideMessageList) {
        this.prompt.moveCaretToEndOfPrompt();
        this.focusPrompt();
      }
    }
  }

  private messagesKeyDown(event: Event): void {
    const keyEvent = (event as KeyboardEvent);
    const hasActionModifier = keyEvent.ctrlKey || keyEvent.altKey || keyEvent.metaKey;
    if (hasActionModifier || keyEvent.key.length !== 1 || UI.UIUtils.isEditing() ||
        this.messagesElement.hasSelection()) {
      return;
    }
    this.prompt.moveCaretToEndOfPrompt();
    this.focusPrompt();
  }

  private messagesPasted(event: Event): void {
    if (!Root.Runtime.Runtime.queryParam('isChromeForTesting') &&
        !Root.Runtime.Runtime.queryParam('disableSelfXssWarnings') && !this.selfXssWarningDisabledSetting.get()) {
      event.preventDefault();
      this.prompt.showSelfXssWarning();
    }
    if (UI.UIUtils.isEditing()) {
      return;
    }
    this.prompt.focus();
  }

  private registerShortcuts(): void {
    this.shortcuts.set(
        UI.KeyboardShortcut.KeyboardShortcut.makeKey('u', UI.KeyboardShortcut.Modifiers.Ctrl.value),
        this.clearPromptBackwards.bind(this));
  }

  private clearPromptBackwards(e: KeyboardEvent): void {
    this.prompt.clear();
    void VisualLogging.logKeyDown(e.currentTarget, e, 'clear-prompt');
  }

  private promptKeyDown(event: Event): void {
    const keyboardEvent = (event as KeyboardEvent);
    if (keyboardEvent.key === 'PageUp') {
      this.updateStickToBottomOnWheel();
      return;
    }

    const shortcut = UI.KeyboardShortcut.KeyboardShortcut.makeKeyFromEvent(keyboardEvent);
    const handler = this.shortcuts.get(shortcut);
    if (handler) {
      handler(keyboardEvent);
      keyboardEvent.preventDefault();
    }
  }

  private printResult(
      result: SDK.RemoteObject.RemoteObject|null, originatingConsoleMessage: SDK.ConsoleModel.ConsoleMessage,
      exceptionDetails?: Protocol.Runtime.ExceptionDetails): void {
    if (!result) {
      return;
    }

    const level = Boolean(exceptionDetails) ? Protocol.Log.LogEntryLevel.Error : Protocol.Log.LogEntryLevel.Info;
    let message;
    if (!exceptionDetails) {
      message = new SDK.ConsoleModel.ConsoleMessage(
          result.runtimeModel(), Protocol.Log.LogEntrySource.Javascript, level, '',
          {type: SDK.ConsoleModel.FrontendMessageType.Result, parameters: [result]});
    } else {
      message = SDK.ConsoleModel.ConsoleMessage.fromException(
          result.runtimeModel(), exceptionDetails, SDK.ConsoleModel.FrontendMessageType.Result, undefined, undefined);
    }
    message.setOriginatingMessage(originatingConsoleMessage);
    result.runtimeModel().target().model(SDK.ConsoleModel.ConsoleModel)?.addMessage(message);
  }

  private commandEvaluated(event: Common.EventTarget.EventTargetEvent<SDK.ConsoleModel.CommandEvaluatedEvent>): void {
    const {data} = event;
    this.printResult(data.result, data.commandMessage, data.exceptionDetails);
  }

  override elementsToRestoreScrollPositionsFor(): Element[] {
    return [this.messagesElement];
  }

  onSearchCanceled(): void {
    this.cleanupAfterSearch();
    for (const message of this.visibleViewMessages) {
      message.setSearchRegex(null);
    }
    this.currentMatchRangeIndex = -1;
    this.regexMatchRanges = [];
    this.searchRegex = null;
    this.viewport.refresh();
  }

  performSearch(searchConfig: UI.SearchableView.SearchConfig, shouldJump: boolean, jumpBackwards?: boolean): void {
    this.onSearchCanceled();
    this.#searchableView.updateSearchMatchesCount(0);

    this.searchRegex = searchConfig.toSearchRegex(true).regex;

    this.regexMatchRanges = [];
    this.currentMatchRangeIndex = -1;

    if (shouldJump) {
      this.searchShouldJumpBackwards = Boolean(jumpBackwards);
    }

    this.searchProgressIndicator = document.createElement('devtools-progress');
    this.searchProgressIndicator.title = i18nString(UIStrings.searching);
    this.searchProgressIndicator.totalWork = this.visibleViewMessages.length;
    this.progressToolbarItem.element.appendChild(this.searchProgressIndicator);

    this.#search(0);
  }

  private cleanupAfterSearch(): void {
    delete this.searchShouldJumpBackwards;
    if (this.#searchTimeoutId) {
      clearTimeout(this.#searchTimeoutId);
      this.#searchTimeoutId = undefined;
    }
    if (this.searchProgressIndicator) {
      this.searchProgressIndicator.done = true;
      delete this.searchProgressIndicator;
    }
  }

  private searchFinishedForTests(): void {
    // This method is sniffed in tests.
  }

  #search(index: number): void {
    this.#searchTimeoutId = undefined;
    if (this.searchProgressIndicator?.canceled) {
      this.cleanupAfterSearch();
      return;
    }

    const startTime = Date.now();
    for (; index < this.visibleViewMessages.length && Date.now() - startTime < 100; ++index) {
      this.searchMessage(index);
    }

    this.#searchableView.updateSearchMatchesCount(this.regexMatchRanges.length);
    if (typeof this.searchShouldJumpBackwards !== 'undefined' && this.regexMatchRanges.length) {
      this.highlightMatch(this.searchShouldJumpBackwards ? -1 : 0);
      delete this.searchShouldJumpBackwards;
    }

    if (index === this.visibleViewMessages.length) {
      this.cleanupAfterSearch();
      window.setTimeout(this.searchFinishedForTests.bind(this), 0);
      return;
    }

    this.#searchTimeoutId = window.setTimeout(this.#search.bind(this, index), 100);
    if (this.searchProgressIndicator) {
      this.searchProgressIndicator.worked = index;
    }
  }

  private searchMessage(index: number): void {
    const message = this.visibleViewMessages[index];
    message.setSearchRegex(this.searchRegex);
    for (let i = 0; i < message.searchCount(); ++i) {
      this.regexMatchRanges.push({messageIndex: index, matchIndex: i});
    }
  }

  jumpToNextSearchResult(): void {
    this.highlightMatch(this.currentMatchRangeIndex + 1);
  }

  jumpToPreviousSearchResult(): void {
    this.highlightMatch(this.currentMatchRangeIndex - 1);
  }

  supportsCaseSensitiveSearch(): boolean {
    return true;
  }

  supportsWholeWordSearch(): boolean {
    return true;
  }

  supportsRegexSearch(): boolean {
    return true;
  }

  private highlightMatch(index: number, scrollIntoView = true): void {
    if (!this.regexMatchRanges.length) {
      return;
    }

    let matchRange;
    if (this.currentMatchRangeIndex >= 0) {
      matchRange = this.regexMatchRanges[this.currentMatchRangeIndex];
      const message = this.visibleViewMessages[matchRange.messageIndex];
      message.searchHighlightNode(matchRange.matchIndex)
          .classList.remove(Highlighting.highlightedCurrentSearchResultClassName);
    }

    index = Platform.NumberUtilities.mod(index, this.regexMatchRanges.length);
    this.currentMatchRangeIndex = index;
    this.#searchableView.updateCurrentMatchIndex(index);
    matchRange = this.regexMatchRanges[index];
    const message = this.visibleViewMessages[matchRange.messageIndex];
    const highlightNode = message.searchHighlightNode(matchRange.matchIndex);
    highlightNode.classList.add(Highlighting.highlightedCurrentSearchResultClassName);
    if (scrollIntoView) {
      this.viewport.scrollItemIntoView(matchRange.messageIndex);
      highlightNode.scrollIntoViewIfNeeded();
    }
  }

  private updateStickToBottomOnPointerDown(isRightClick?: boolean): void {
    this.muteViewportUpdates = !isRightClick;
    this.viewport.setStickToBottom(false);
    if (this.waitForScrollTimeout) {
      clearTimeout(this.waitForScrollTimeout);
      delete this.waitForScrollTimeout;
    }
  }

  private updateStickToBottomOnPointerUp(): void {
    if (!this.muteViewportUpdates) {
      return;
    }

    // Delay querying isScrolledToBottom to give time for smooth scroll
    // events to arrive. The value for the longest timeout duration is
    // retrieved from crbug.com/575409.
    this.waitForScrollTimeout = window.setTimeout(updateViewportState.bind(this), 200);

    function updateViewportState(this: ConsoleView): void {
      this.muteViewportUpdates = false;
      if (this.isShowing()) {
        this.viewport.setStickToBottom(this.isScrolledToBottom());
      }
      if (this.maybeDirtyWhileMuted) {
        this.scheduleViewportRefresh();
        delete this.maybeDirtyWhileMuted;
      }
      delete this.waitForScrollTimeout;
      this.updateViewportStickinessForTest();
    }
  }

  private updateViewportStickinessForTest(): void {
    // This method is sniffed in tests.
  }

  private updateStickToBottomOnWheel(): void {
    this.updateStickToBottomOnPointerDown();
    this.updateStickToBottomOnPointerUp();
  }

  private promptTextChanged(): void {
    const oldStickToBottom = this.viewport.stickToBottom();
    const willStickToBottom = this.isScrolledToBottom();
    this.viewport.setStickToBottom(willStickToBottom);
    if (willStickToBottom && !oldStickToBottom) {
      this.scheduleViewportRefresh();
    }
    this.promptTextChangedForTest();
  }

  private promptTextChangedForTest(): void {
    // This method is sniffed in tests.
  }

  private isScrolledToBottom(): boolean {
    const distanceToPromptEditorBottom = this.messagesElement.scrollHeight - this.messagesElement.scrollTop -
        this.messagesElement.clientHeight - (this.prompt.belowEditorElement() as HTMLElement).offsetHeight;
    return distanceToPromptEditorBottom <= 2;
  }

  private setupAiCodeCompletion(): void {
    this.createAiCodeCompletionSummaryToolbar();
  }

  private cleanupAiCodeCompletion(): void {
    this.aiCodeCompletionSummaryToolbarContainer?.remove();
    this.aiCodeCompletionSummaryToolbarContainer = undefined;
    this.aiCodeCompletionSummaryToolbar = undefined;
  }
}

// @ts-expect-error exported for Tests.js
globalThis.Console = globalThis.Console || {};
// @ts-expect-error exported for Tests.js
globalThis.Console.ConsoleView = ConsoleView;

export class ConsoleViewFilter {
  private readonly filterChanged: () => void;
  messageLevelFiltersSetting: Common.Settings.Setting<LevelsMask>;
  networkMessagesSetting: Common.Settings.Setting<boolean>;
  filterByExecutionContextSetting: Common.Settings.Setting<boolean>;
  private readonly suggestionBuilder: UI.FilterSuggestionBuilder.FilterSuggestionBuilder;
  readonly textFilterUI: UI.Toolbar.ToolbarInput;
  private readonly textFilterSetting: Common.Settings.Setting<string>;
  private readonly filterParser: TextUtils.TextUtils.FilterParser;
  currentFilter: ConsoleFilter;
  private levelLabels: Map<Protocol.Log.LogEntryLevel, string>;
  readonly levelMenuButton: UI.Toolbar.ToolbarMenuButton;
  readonly levelMenuButtonInfo: UI.Toolbar.ToolbarItem;

  constructor(filterChangedCallback: () => void) {
    this.filterChanged = filterChangedCallback;

    this.messageLevelFiltersSetting = ConsoleViewFilter.levelFilterSetting();
    this.networkMessagesSetting = Common.Settings.Settings.instance().moduleSetting('network-messages');
    this.filterByExecutionContextSetting =
        Common.Settings.Settings.instance().moduleSetting('selected-context-filter-enabled');

    this.messageLevelFiltersSetting.addChangeListener(this.onFilterChanged.bind(this));
    this.networkMessagesSetting.addChangeListener(this.onFilterChanged.bind(this));
    this.filterByExecutionContextSetting.addChangeListener(this.onFilterChanged.bind(this));
    UI.Context.Context.instance().addFlavorChangeListener(
        SDK.RuntimeModel.ExecutionContext, this.onFilterChanged, this);

    const filterKeys = Object.values(FilterType);
    this.suggestionBuilder = new UI.FilterSuggestionBuilder.FilterSuggestionBuilder(filterKeys);
    this.textFilterUI = new UI.Toolbar.ToolbarFilter(
        undefined, 1, 1, i18nString(UIStrings.egEventdCdnUrlacom),
        this.suggestionBuilder.completions.bind(this.suggestionBuilder), true);
    this.textFilterSetting = Common.Settings.Settings.instance().createSetting('console.text-filter', '');
    if (this.textFilterSetting.get()) {
      this.textFilterUI.setValue(this.textFilterSetting.get());
    }
    this.textFilterUI.addEventListener(UI.Toolbar.ToolbarInput.Event.TEXT_CHANGED, () => {
      this.textFilterSetting.set(this.textFilterUI.value());
      this.onFilterChanged();
    });
    this.filterParser = new TextUtils.TextUtils.FilterParser(filterKeys);
    this.currentFilter = new ConsoleFilter('', [], null, this.messageLevelFiltersSetting.get());
    this.updateCurrentFilter();
    this.levelLabels = new Map(([
      [Protocol.Log.LogEntryLevel.Verbose, i18nString(UIStrings.verbose)],
      [Protocol.Log.LogEntryLevel.Info, i18nString(UIStrings.info)],
      [Protocol.Log.LogEntryLevel.Warning, i18nString(UIStrings.warnings)],
      [Protocol.Log.LogEntryLevel.Error, i18nString(UIStrings.errors)],
    ]));

    this.levelMenuButton =
        new UI.Toolbar.ToolbarMenuButton(this.appendLevelMenuItems.bind(this), undefined, undefined, 'log-level');
    const levelMenuButtonInfoIcon = createIcon('info', 'console-sidebar-levels-info');
    levelMenuButtonInfoIcon.title = i18nString(UIStrings.overriddenByFilterSidebar);
    this.levelMenuButtonInfo = new UI.Toolbar.ToolbarItem(levelMenuButtonInfoIcon);
    this.levelMenuButtonInfo.setVisible(false);

    this.updateLevelMenuButtonText();
    this.messageLevelFiltersSetting.addChangeListener(this.updateLevelMenuButtonText.bind(this));
  }

  onMessageAdded(message: SDK.ConsoleModel.ConsoleMessage): void {
    if (message.type === SDK.ConsoleModel.FrontendMessageType.Command ||
        message.type === SDK.ConsoleModel.FrontendMessageType.Result || message.isGroupMessage()) {
      return;
    }
    if (message.context) {
      this.suggestionBuilder.addItem(FilterType.Context, message.context);
    }
    if (message.source) {
      this.suggestionBuilder.addItem(FilterType.Source, message.source);
    }
    if (message.url) {
      this.suggestionBuilder.addItem(FilterType.Url, message.url);
    }
  }

  setLevelMenuOverridden(overridden: boolean): void {
    this.levelMenuButton.setEnabled(!overridden);
    this.levelMenuButtonInfo.setVisible(overridden);
    if (overridden) {
      this.levelMenuButton.setText(i18nString(UIStrings.customLevels));
    } else {
      this.updateLevelMenuButtonText();
    }
  }

  static levelFilterSetting(): Common.Settings.Setting<LevelsMask> {
    return Common.Settings.Settings.instance().createSetting(
        'message-level-filters', ConsoleFilter.defaultLevelsFilterValue());
  }

  private updateCurrentFilter(): void {
    const parsedFilters = this.filterParser.parse(this.textFilterUI.value());
    for (const {key} of parsedFilters) {
      switch (key) {
        case FilterType.Context:
          Host.userMetrics.actionTaken(Host.UserMetrics.Action.ConsoleFilterByContext);
          break;
        case FilterType.Source:
          Host.userMetrics.actionTaken(Host.UserMetrics.Action.ConsoleFilterBySource);
          break;
        case FilterType.Url:
          Host.userMetrics.actionTaken(Host.UserMetrics.Action.ConsoleFilterByUrl);
          break;
      }
    }
    if (!this.networkMessagesSetting.get()) {
      parsedFilters.push(
          {key: FilterType.Source, text: Protocol.Log.LogEntrySource.Network, negative: true, regex: undefined});
    }

    this.currentFilter.executionContext = this.filterByExecutionContextSetting.get() ?
        UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext) :
        null;
    this.currentFilter.parsedFilters = parsedFilters;
    this.currentFilter.levelsMask = this.messageLevelFiltersSetting.get();
  }

  private onFilterChanged(): void {
    this.updateCurrentFilter();
    this.filterChanged();
  }

  private updateLevelMenuButtonText(): void {
    let isAll = true;
    let isDefault = true;
    const allValue = ConsoleFilter.allLevelsFilterValue();
    const defaultValue = ConsoleFilter.defaultLevelsFilterValue();

    let text: Common.UIString.LocalizedString|null = null;
    const levels = this.messageLevelFiltersSetting.get();
    const allLevels: Protocol.EnumerableEnum<typeof Protocol.Log.LogEntryLevel> = {
      Verbose: Protocol.Log.LogEntryLevel.Verbose,
      Info: Protocol.Log.LogEntryLevel.Info,
      Warning: Protocol.Log.LogEntryLevel.Warning,
      Error: Protocol.Log.LogEntryLevel.Error,
    };
    for (const name of Object.values(allLevels)) {
      isAll = isAll && levels[name] === allValue[name];
      isDefault = isDefault && levels[name] === defaultValue[name];
      if (levels[name]) {
        text = text ? i18nString(UIStrings.customLevels) :
                      i18nString(UIStrings.sOnly, {PH1: String(this.levelLabels.get(name))});
      }
    }
    if (isAll) {
      text = i18nString(UIStrings.allLevels);
    } else if (isDefault) {
      text = i18nString(UIStrings.defaultLevels);
    } else {
      text = text || i18nString(UIStrings.hideAll);
    }
    this.levelMenuButton.element.classList.toggle('warning', !isAll && !isDefault);
    this.levelMenuButton.setText(text);
    this.levelMenuButton.setTitle(i18nString(UIStrings.logLevelS, {PH1: text}));
  }

  private appendLevelMenuItems(contextMenu: UI.ContextMenu.ContextMenu): void {
    const setting = this.messageLevelFiltersSetting;
    const levels = setting.get();

    contextMenu.headerSection().appendItem(
        i18nString(UIStrings.default), () => setting.set(ConsoleFilter.defaultLevelsFilterValue()),
        {jslogContext: 'default'});
    for (const [level, levelText] of this.levelLabels.entries()) {
      contextMenu.defaultSection().appendCheckboxItem(
          levelText, toggleShowLevel.bind(null, level), {checked: levels[level], jslogContext: level});
    }

    function toggleShowLevel(level: string): void {
      levels[level] = !levels[level];
      setting.set(levels);
    }
  }

  addMessageURLFilter(url: Platform.DevToolsPath.UrlString): void {
    if (!url) {
      return;
    }
    const suffix = this.textFilterUI.value() ? ` ${this.textFilterUI.value()}` : '';
    this.textFilterUI.setValue(`-url:${url}${suffix}`);
    this.textFilterSetting.set(this.textFilterUI.value());
    this.onFilterChanged();
  }

  shouldBeVisible(viewMessage: ConsoleViewMessage): boolean {
    return this.currentFilter.shouldBeVisible(viewMessage);
  }

  clear(): void {
    this.suggestionBuilder.clear();
  }

  reset(): void {
    this.messageLevelFiltersSetting.set(ConsoleFilter.defaultLevelsFilterValue());
    this.filterByExecutionContextSetting.set(false);
    this.networkMessagesSetting.set(true);
    this.textFilterUI.setValue('');
    this.onFilterChanged();
  }
}

export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
  handleAction(_context: UI.Context.Context, actionId: string): boolean {
    switch (actionId) {
      case 'console.toggle':
        if (ConsoleView.instance().hasFocus() && UI.InspectorView.InspectorView.instance().drawerVisible()) {
          UI.InspectorView.InspectorView.instance().closeDrawer();
          return true;
        }
        Host.InspectorFrontendHost.InspectorFrontendHostInstance.bringToFront();
        Common.Console.Console.instance().show();
        ConsoleView.instance().focusPrompt();
        return true;
      case 'console.clear':
        ConsoleView.instance().clearConsole();
        return true;
      case 'console.clear.history':
        ConsoleView.instance().clearHistory();
        return true;
      case 'console.create-pin':
        ConsoleView.instance().pinPane.addPin('', true /* userGesture */);
        return true;
    }
    return false;
  }
}

const messagesSortedBySymbol = new WeakMap<ConsoleViewMessage, number>();
const consoleMessageToViewMessage = new WeakMap<SDK.ConsoleModel.ConsoleMessage, ConsoleViewMessage>();
export interface RegexMatchRange {
  messageIndex: number;
  matchIndex: number;
}
