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

import '../../ui/legacy/legacy.js';
import '../../ui/legacy/components/data_grid/data_grid.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 ProtocolClient from '../../core/protocol_client/protocol_client.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as Buttons from '../../ui/components/buttons/buttons.js';
import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js';
import * as UI from '../../ui/legacy/legacy.js';
import {html, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';

import {type Command, Events as JSONEditorEvents, JSONEditor, type Parameter} from './JSONEditor.js';
import protocolMonitorStyles from './protocolMonitor.css.js';

const {widgetConfig} = UI.Widget;
const UIStrings = {
  /**
   *@description Text for one or a group of functions
   */
  method: 'Method',
  /**
   * @description Text in Protocol Monitor. Title for a table column which shows in which direction
   * the particular protocol message was travelling. Values in this column will either be 'sent' or
   * 'received'.
   */
  type: 'Type',
  /**
   * @description Text in Protocol Monitor of the Protocol Monitor tab. Noun relating to a network request.
   */
  request: 'Request',
  /**
   *@description Title of a cell content in protocol monitor. A Network response refers to the act of acknowledging a
  network request. Should not be confused with answer.
   */
  response: 'Response',
  /**
   *@description Text for timestamps of items
   */
  timestamp: 'Timestamp',
  /**
   *@description Title of a cell content in protocol monitor. It describes the time between sending a request and receiving a response.
   */
  elapsedTime: 'Elapsed time',
  /**
   *@description Text in Protocol Monitor of the Protocol Monitor tab
   */
  target: 'Target',
  /**
   *@description Text to record a series of actions for analysis
   */
  record: 'Record',
  /**
   *@description Text to clear everything
   */
  clearAll: 'Clear all',
  /**
   *@description Text to filter result items
   */
  filter: 'Filter',
  /**
   *@description Text for the documentation of something
   */
  documentation: 'Documentation',
  /**
   *@description Text to open the CDP editor with the selected command
   */
  editAndResend: 'Edit and resend',
  /**
   *@description Cell text content in Protocol Monitor of the Protocol Monitor tab
   *@example {30} PH1
   */
  sMs: '{PH1} ms',
  /**
   *@description Text in Protocol Monitor of the Protocol Monitor tab
   */
  noMessageSelected: 'No message selected',
  /**
   *@description Text in Protocol Monitor of the Protocol Monitor tab if no message is selected
   */
  selectAMessageToView: 'Select a message to see its details',
  /**
   *@description Text in Protocol Monitor for the save button
   */
  save: 'Save',
  /**
   *@description Text in Protocol Monitor to describe the sessions column
   */
  session: 'Session',
  /**
   *@description A placeholder for an input in Protocol Monitor. The input accepts commands that are sent to the backend on Enter. CDP stands for Chrome DevTools Protocol.
   */
  sendRawCDPCommand: 'Send a raw `CDP` command',
  /**
   * @description A tooltip text for the input in the Protocol Monitor panel. The tooltip describes what format is expected.
   */
  sendRawCDPCommandExplanation:
      'Format: `\'Domain.commandName\'` for a command without parameters, or `\'{"command":"Domain.commandName", "parameters": {...}}\'` as a JSON object for a command with parameters. `\'cmd\'`/`\'method\'` and `\'args\'`/`\'params\'`/`\'arguments\'` are also supported as alternative keys for the `JSON` object.',

  /**
   * @description A label for a select input that allows selecting a CDP target to send the commands to.
   */
  selectTarget: 'Select a target',
  /**
   * @description Tooltip for the the console sidebar toggle in the Console panel. Command to
   * open/show the sidebar.
   */
  showCDPCommandEditor: 'Show CDP command editor',
  /**
   * @description Tooltip for the the console sidebar toggle in the Console panel. Command to
   * open/show the sidebar.
   */
  hideCDPCommandEditor: 'Hide  CDP command editor',
  /**
   * @description Screen reader announcement when the sidebar is shown in the Console panel.
   */
  CDPCommandEditorShown: 'CDP command editor shown',
  /**
   * @description Screen reader announcement when the sidebar is hidden in the Console panel.
   */
  CDPCommandEditorHidden: 'CDP command editor hidden',
};
const str_ = i18n.i18n.registerUIStrings('panels/protocol_monitor/ProtocolMonitor.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export const buildProtocolMetadata = (domains: Iterable<ProtocolDomain>):
    Map<string, {parameters: Parameter[], description: string, replyArgs: string[]}> => {
      const metadataByCommand: Map<string, {parameters: Parameter[], description: string, replyArgs: string[]}> =
          new Map();
      for (const domain of domains) {
        for (const command of Object.keys(domain.metadata)) {
          metadataByCommand.set(command, domain.metadata[command]);
        }
      }
      return metadataByCommand;
    };

const metadataByCommand = buildProtocolMetadata(
    ProtocolClient.InspectorBackend.inspectorBackend.agentPrototypes.values() as Iterable<ProtocolDomain>);
const typesByName = ProtocolClient.InspectorBackend.inspectorBackend.typeMap;
const enumsByName = ProtocolClient.InspectorBackend.inspectorBackend.enumMap;
export interface Message {
  id?: number;
  method: string;
  error?: Object;
  result?: Object;
  params?: Object;
  requestTime: number;
  elapsedTime?: number;
  sessionId?: string;
  target?: SDK.Target.Target;
}
export interface LogMessage {
  id?: number;
  domain: string;
  method: string;
  params: Object;
  type: 'send'|'recv';
}

export interface ProtocolDomain {
  readonly domain: string;
  readonly metadata: {
    [commandName: string]: {parameters: Parameter[], description: string, replyArgs: string[]},
  };
}

export interface ViewInput {
  messages: Message[];
  selectedMessage?: Message;
  filters: TextUtils.TextUtils.ParsedFilter[];
  onRecord: (e: Event) => void;
  onClear: () => void;
  onSave: () => void;
  onSelect: (e: CustomEvent<HTMLElement|null>) => void;
  onContextMenu: (e: CustomEvent<{menu: UI.ContextMenu.ContextMenu, element: HTMLElement}>) => void;
  textFilterUI: UI.Toolbar.ToolbarInput;
  showHideSidebarButton: UI.Toolbar.ToolbarButton;
  commandInput: UI.Toolbar.ToolbarInput;
  selector: UI.Toolbar.ToolbarComboBox;
}

export interface ViewOutput {}

export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void;

export class ProtocolMonitorDataGrid extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.VBox>(
    UI.Widget.VBox) {
  private started: boolean;
  private startTime: number;
  private readonly messageForId = new Map<number, Message>();
  private readonly filterParser: TextUtils.TextUtils.FilterParser;
  private readonly suggestionBuilder: UI.FilterSuggestionBuilder.FilterSuggestionBuilder;
  private readonly textFilterUI: UI.Toolbar.ToolbarInput;
  readonly selector: UI.Toolbar.ToolbarComboBox;
  #commandAutocompleteSuggestionProvider = new CommandAutocompleteSuggestionProvider();
  #selectedTargetId?: string;
  #commandInput: UI.Toolbar.ToolbarInput;
  #showHideSidebarButton: UI.Toolbar.ToolbarButton;
  #view: View;
  #messages: Message[] = [];
  #selectedMessage: Message|undefined;
  #filters: TextUtils.TextUtils.ParsedFilter[] = [];
  #splitWidget: UI.SplitWidget.SplitWidget;
  constructor(splitWidget: UI.SplitWidget.SplitWidget, view: View = (input, output, target) => {
    // clang-format off
    render(
        html`<devtools-toolbar class="protocol-monitor-toolbar"
                               jslog=${VisualLogging.toolbar('top')}>
               <devtools-button title=${i18nString(UIStrings.record)}
                                .iconName=${'record-start'}
                                .toggledIconName=${'record-stop'}
                                .jslogContext=${'protocol-monitor.toggle-recording'}
                                .variant=${Buttons.Button.Variant.ICON_TOGGLE}
                                .toggleType=${Buttons.Button.ToggleType.RED}
                                .toggled=${true}
                                @click=${input.onRecord}></devtools-button>
              <devtools-button title=${i18nString(UIStrings.clearAll)}
                               .iconName=${'clear'}
                               .variant=${Buttons.Button.Variant.TOOLBAR}
                               .jslogContext=${'protocol-monitor.clear-all'}
                               @click=${input.onClear}></devtools-button>
              <devtools-button title=${i18nString(UIStrings.save)}
                               .iconName=${'download'}
                               .variant=${Buttons.Button.Variant.TOOLBAR}
                               .jslogContext=${'protocol-monitor.save'}
                               @click=${input.onSave}></devtools-button>
              ${input.textFilterUI.element}
            </devtools-toolbar>
            <devtools-split-widget .options=${{
                vertical: true,
                secondIsSidebar: true,
                settingName: 'protocol-monitor-panel-split',
              defaultSidebarWidth: 250}}>
              <devtools-data-grid
                  striped
                  slot="main"
                  @select=${input.onSelect}
                  @contextmenu=${input.onContextMenu}
                  .filters=${input.filters}>
                <table>
                    <tr>
                      <th id="type" sortable style="text-align: center" hideable weight="1">${i18nString(UIStrings.type)}</th>
                      <th id="method" weight="5">${i18nString(UIStrings.method)}</th>
                      <th id="request" hideable weight="5">${i18nString(UIStrings.request)}</th>
                      <th id="response" hideable weight="5">${i18nString(UIStrings.response)}</th>
                      <th id="elapsed-time" sortable hideable weight="2">${i18nString(UIStrings.elapsedTime)}</th>
                      <th id="timestamp" sortable hideable weight="5">${i18nString(UIStrings.timestamp)}</th>
                      <th id="target" sortable hideable weight="5">${i18nString(UIStrings.target)}</th>
                      <th id="session" sortable hideable weight="5">${i18nString(UIStrings.session)}</th>
                    </tr>
                    ${
            input.messages.map(
                (message, index) => html`
                      <tr data-index=${index}
                          style="--override-data-grid-row-background-color: var(--sys-color-surface3)">
                        ${'id' in message ? html`
                          <td title="sent">
                            <devtools-icon name="arrow-up-down" style="color: var(--icon-request-response); width: 16px; height: 16px;">
                            </devtools-icon>
                          </td>` : html`
                          <td title="received">
                            <devtools-icon name="arrow-down" style="color: var(--icon-request); width: 16px; height: 16px;">
                            </devtools-icon>
                          </td>`}
                        <td>${message.method}</td>
                        <td>${message.params ? html`<code>${JSON.stringify(message.params)}</code>` : ''}</td>
                        <td>
                          ${message.result    ? html`<code>${JSON.stringify(message.result)}</code>` :
                                message.error ? html`<code>${JSON.stringify(message.error)}</code>` :
                                                '(pending)'}
                        </td>
                        <td data-value=${message.elapsedTime || 0}>
                          ${!('id' in message)  ? '' :
                            message.elapsedTime ? i18nString(UIStrings.sMs, {PH1: String(message.elapsedTime)})
                                                : '(pending)'}
                        </td>
                        <td data-value=${message.requestTime}>${i18nString(UIStrings.sMs, {PH1: String(message.requestTime)})}</td>
                        <td>${this.targetToString(message.target)}</td>
                        <td>${message.sessionId || ''}</td>
                      </tr>`)}
                  </table>
              </devtools-data-grid>
              <devtools-widget .widgetConfig=${widgetConfig(InfoWidget, {
                    request: input.selectedMessage?.params,
                    response: input.selectedMessage?.result || input.selectedMessage?.error,
                    type: !input.selectedMessage           ? undefined :
                          ('id' in input?.selectedMessage) ? 'sent'
                                                           : 'received',
                  })}
                  class="protocol-monitor-info"
                  slot="sidebar"></devtools-widget>
            </devtools-split-widget>
            <devtools-toolbar class="protocol-monitor-bottom-toolbar"
               jslog=${VisualLogging.toolbar('bottom')}>
              ${input.showHideSidebarButton.element}
              ${input.commandInput.element}
              ${input.selector.element}
            </devtools-toolbar>`,
        target,
        {host: input}
    );
    // clang-format on
  }) {
    super(true);
    this.#splitWidget = splitWidget;
    this.#view = view;
    this.started = false;
    this.startTime = 0;
    this.contentElement.classList.add('protocol-monitor');
    this.selector = this.#createTargetSelector();

    const keys = ['method', 'request', 'response', 'type', 'target', 'session'];
    this.filterParser = new TextUtils.TextUtils.FilterParser(keys);
    this.suggestionBuilder = new UI.FilterSuggestionBuilder.FilterSuggestionBuilder(keys);

    this.textFilterUI = new UI.Toolbar.ToolbarFilter(
        undefined, 1, .2, '', this.suggestionBuilder.completions.bind(this.suggestionBuilder), true);
    this.textFilterUI.addEventListener(UI.Toolbar.ToolbarInput.Event.TEXT_CHANGED, event => {
      const query = event.data as string;
      this.#filters = this.filterParser.parse(query);
      this.requestUpdate();
    });
    this.#showHideSidebarButton = splitWidget.createShowHideSidebarButton(
        i18nString(UIStrings.showCDPCommandEditor), i18nString(UIStrings.hideCDPCommandEditor),
        i18nString(UIStrings.CDPCommandEditorShown), i18nString(UIStrings.CDPCommandEditorHidden),
        'protocol-monitor.toggle-command-editor');
    this.#commandInput = this.#createCommandInput();
    const inputBar = this.#commandInput.element;
    const tabSelector = this.selector.element;

    const populateToolbarInput = (): void => {
      const editorWidget = splitWidget.sidebarWidget();
      if (!(editorWidget instanceof EditorWidget)) {
        return;
      }
      const commandJson = editorWidget.jsonEditor.getCommandJson();
      const targetId = editorWidget.jsonEditor.targetId;
      if (targetId) {
        const selectedIndex = this.selector.options().findIndex(option => option.value === targetId);
        if (selectedIndex !== -1) {
          this.selector.setSelectedIndex(selectedIndex);
          this.#selectedTargetId = targetId;
        }
      }
      if (commandJson) {
        this.#commandInput.setValue(commandJson);
      }
    };

    splitWidget.addEventListener(UI.SplitWidget.Events.SHOW_MODE_CHANGED, (event => {
                                   if (event.data === 'OnlyMain') {
                                     populateToolbarInput();

                                     inputBar?.setAttribute('style', 'display:flex; flex-grow: 1');
                                     tabSelector?.setAttribute('style', 'display:flex');
                                   } else {
                                     const {command, parameters} = parseCommandInput(this.#commandInput.value());
                                     this.dispatchEventToListeners(
                                         Events.COMMAND_CHANGE,
                                         {command, parameters, targetId: this.#selectedTargetId});
                                     inputBar?.setAttribute('style', 'display:none');
                                     tabSelector?.setAttribute('style', 'display:none');
                                   }
                                 }));
    this.performUpdate();
  }

  override performUpdate(): void {
    const viewInput = {
      messages: this.#messages,
      selectedMessage: this.#selectedMessage,
      filters: this.#filters,
      onRecord: (e: Event) => {
        this.setRecording((e.target as Buttons.Button.Button).toggled);
      },
      onClear: () => {
        this.#messages = [];
        this.messageForId.clear();
        this.requestUpdate();
      },
      onSave: () => {
        void this.saveAsFile();
      },
      onSelect: (e: CustomEvent<HTMLElement|null>) => {
        const index = parseInt(e.detail?.dataset?.index ?? '', 10);
        this.#selectedMessage = index ? this.#messages[index] : undefined;
        this.requestUpdate();
      },
      onContextMenu: (e: CustomEvent<{menu: UI.ContextMenu.ContextMenu, element: HTMLElement}>) => {
        const message = this.#messages[parseInt(e.detail?.element?.dataset?.index || '', 10)];
        if (message) {
          this.#populateContextMenu(e.detail.menu, message);
        }
      },
      textFilterUI: this.textFilterUI,
      showHideSidebarButton: this.#showHideSidebarButton,
      commandInput: this.#commandInput,
      selector: this.selector,
    };
    const viewOutput = {};
    this.#view(viewInput, viewOutput, this.contentElement);
  }

  #populateContextMenu(menu: UI.ContextMenu.ContextMenu, message: Message): void {
    /**
     * You can click the "Edit and resend" item in the context menu to be
     * taken to the CDP editor with the filled with the selected command.
     */
    menu.editSection().appendItem(i18nString(UIStrings.editAndResend), () => {
      if (!this.#selectedMessage) {
        return;
      }
      const parameters = this.#selectedMessage.params as {[x: string]: unknown};
      const targetId = this.#selectedMessage.target?.id() || '';
      const command = message.method;
      if (this.#splitWidget.showMode() === UI.SplitWidget.ShowMode.ONLY_MAIN) {
        this.#splitWidget.toggleSidebar();
      }
      this.dispatchEventToListeners(Events.COMMAND_CHANGE, {command, parameters, targetId});
    }, {jslogContext: 'edit-and-resend', disabled: !('id' in message)});

    /**
     * You can click the "Filter" item in the context menu to filter the
     * protocol monitor entries to those that match the method of the
     * current row.
     */
    menu.editSection().appendItem(i18nString(UIStrings.filter), () => {
      this.textFilterUI.setValue(`method:${message.method}`, true);
    }, {jslogContext: 'filter'});

    /**
     * You can click the "Documentation" item in the context menu to be
     * taken to the CDP Documentation site entry for the given method.
     */
    menu.footerSection().appendItem(i18nString(UIStrings.documentation), () => {
      const [domain, method] = message.method.split('.');
      const type = 'id' in message ? 'method' : 'event';
      Host.InspectorFrontendHost.InspectorFrontendHostInstance.openInNewTab(
          `https://chromedevtools.github.io/devtools-protocol/tot/${domain}#${type}-${method}` as
          Platform.DevToolsPath.UrlString);
    }, {jslogContext: 'documentation'});
  }

  #createCommandInput(): UI.Toolbar.ToolbarInput {
    const placeholder = i18nString(UIStrings.sendRawCDPCommand);
    const accessiblePlaceholder = placeholder;
    const growFactor = 1;
    const shrinkFactor = 0.2;
    const tooltip = i18nString(UIStrings.sendRawCDPCommandExplanation);
    const input = new UI.Toolbar.ToolbarInput(
        placeholder,
        accessiblePlaceholder,
        growFactor,
        shrinkFactor,
        tooltip,
        this.#commandAutocompleteSuggestionProvider.buildTextPromptCompletions,
        false,
        'command-input',
    );
    input.addEventListener(UI.Toolbar.ToolbarInput.Event.ENTER_PRESSED, () => {
      this.#commandAutocompleteSuggestionProvider.addEntry(input.value());
      const {command, parameters} = parseCommandInput(input.value());
      this.onCommandSend(command, parameters, this.#selectedTargetId);
    });
    return input;
  }

  #createTargetSelector(): UI.Toolbar.ToolbarComboBox {
    const selector = new UI.Toolbar.ToolbarComboBox(() => {
      this.#selectedTargetId = selector.selectedOption()?.value;
    }, i18nString(UIStrings.selectTarget), undefined, 'target-selector');
    selector.setMaxWidth(120);
    const targetManager = SDK.TargetManager.TargetManager.instance();
    const syncTargets = (): void => {
      selector.removeOptions();
      for (const target of targetManager.targets()) {
        selector.createOption(`${target.name()} (${target.inspectedURL()})`, target.id());
      }
    };
    targetManager.addEventListener(SDK.TargetManager.Events.AVAILABLE_TARGETS_CHANGED, syncTargets);
    syncTargets();
    return selector;
  }

  onCommandSend(command: string, parameters: object, target?: string): void {
    const test = ProtocolClient.InspectorBackend.test;
    const targetManager = SDK.TargetManager.TargetManager.instance();
    const selectedTarget = target ? targetManager.targetById(target) : null;
    const sessionId = selectedTarget ? selectedTarget.sessionId : '';
    // TS thinks that properties are read-only because
    // in TS test is defined as a namespace.
    // @ts-ignore
    test.sendRawMessage(command, parameters, () => {}, sessionId);
  }

  override wasShown(): void {
    if (this.started) {
      return;
    }
    this.registerRequiredCSS(protocolMonitorStyles);
    this.started = true;
    this.startTime = Date.now();
    this.setRecording(true);
  }

  private setRecording(recording: boolean): void {
    const test = ProtocolClient.InspectorBackend.test;
    if (recording) {
      // TODO: TS thinks that properties are read-only because
      // in TS test is defined as a namespace.
      // @ts-ignore
      test.onMessageSent = this.messageSent.bind(this);
      // @ts-ignore
      test.onMessageReceived = this.messageReceived.bind(this);
    } else {
      // @ts-ignore
      test.onMessageSent = null;
      // @ts-ignore
      test.onMessageReceived = null;
    }
  }

  private targetToString(target: SDK.Target.Target|undefined): string {
    if (!target) {
      return '';
    }
    return target.decorateLabel(
        `${target.name()} ${target === SDK.TargetManager.TargetManager.instance().rootTarget() ? '' : target.id()}`);
  }

  private messageReceived(message: Message, target: ProtocolClient.InspectorBackend.TargetBase|null): void {
    if ('id' in message && message.id) {
      const existingMessage = this.messageForId.get(message.id);
      if (!existingMessage) {
        return;
      }
      existingMessage.result = message.result;
      existingMessage.error = message.error;
      existingMessage.elapsedTime = Date.now() - this.startTime - existingMessage.requestTime;

      // Now we've updated the message, it won't be updated again, so we can delete it from the tracking map.
      this.messageForId.delete(message.id);
      this.requestUpdate();
      return;
    }

    this.#messages.push({
      method: message.method,
      sessionId: message.sessionId,
      target: (target ?? undefined) as SDK.Target.Target | undefined,
      requestTime: Date.now() - this.startTime,
      result: message.params as Object,
    });

    this.requestUpdate();
  }

  private messageSent(
      message: {domain: string, method: string, params: Object, id: number, sessionId?: string},
      target: ProtocolClient.InspectorBackend.TargetBase|null): void {
    const messageRecord = {
      method: message.method,
      params: message.params,
      id: message.id,
      sessionId: message.sessionId,
      target: (target ?? undefined) as SDK.Target.Target | undefined,
      requestTime: Date.now() - this.startTime,
    };
    this.#messages.push(messageRecord);
    this.requestUpdate();
    this.messageForId.set(message.id, messageRecord);
  }

  private async saveAsFile(): Promise<void> {
    const now = new Date();
    const fileName = 'ProtocolMonitor-' + Platform.DateUtilities.toISO8601Compact(now) + '.json' as
        Platform.DevToolsPath.RawPathString;
    const stream = new Bindings.FileUtils.FileOutputStream();

    const accepted = await stream.open(fileName);
    if (!accepted) {
      return;
    }

    const rowEntries = this.#messages.map(m => ({...m, target: m.target?.id()}));

    void stream.write(JSON.stringify(rowEntries, null, '  '));
    void stream.close();
  }
}

export class ProtocolMonitorImpl extends UI.Widget.VBox {
  #split: UI.SplitWidget.SplitWidget;
  #editorWidget = new EditorWidget();
  #protocolMonitorDataGrid: ProtocolMonitorDataGrid;
  // This width corresponds to the optimal width to use the editor properly
  // It is randomly chosen
  #sideBarMinWidth = 400;
  constructor() {
    super(true);
    this.element.setAttribute('jslog', `${VisualLogging.panel('protocol-monitor').track({resize: true})}`);
    this.#split =
        new UI.SplitWidget.SplitWidget(true, false, 'protocol-monitor-split-container', this.#sideBarMinWidth);
    this.#split.show(this.contentElement);
    this.#protocolMonitorDataGrid = new ProtocolMonitorDataGrid(this.#split);
    this.#protocolMonitorDataGrid.addEventListener(Events.COMMAND_CHANGE, event => {
      this.#editorWidget.jsonEditor.displayCommand(event.data.command, event.data.parameters, event.data.targetId);
    });

    this.#editorWidget.element.style.overflow = 'hidden';
    this.#split.setMainWidget(this.#protocolMonitorDataGrid);
    this.#split.setSidebarWidget(this.#editorWidget);
    this.#split.hideSidebar(true);
    this.#editorWidget.addEventListener(Events.COMMAND_SENT, event => {
      this.#protocolMonitorDataGrid.onCommandSend(event.data.command, event.data.parameters, event.data.targetId);
    });
  }
}

export class CommandAutocompleteSuggestionProvider {
  #maxHistorySize = 200;
  #commandHistory = new Set<string>();

  constructor(maxHistorySize?: number) {
    if (maxHistorySize !== undefined) {
      this.#maxHistorySize = maxHistorySize;
    }
  }

  buildTextPromptCompletions =
      async(expression: string, prefix: string, force?: boolean): Promise<UI.SuggestBox.Suggestions> => {
    if (!prefix && !force && expression) {
      return [];
    }

    const newestToOldest = [...this.#commandHistory].reverse();
    newestToOldest.push(...metadataByCommand.keys());
    return newestToOldest.filter(cmd => cmd.startsWith(prefix)).map(text => ({
                                                                      text,
                                                                    }));
  };

  addEntry(value: string): void {
    if (this.#commandHistory.has(value)) {
      this.#commandHistory.delete(value);
    }
    this.#commandHistory.add(value);
    if (this.#commandHistory.size > this.#maxHistorySize) {
      const earliestEntry = this.#commandHistory.values().next().value as string;
      this.#commandHistory.delete(earliestEntry);
    }
  }
}

export class InfoWidget extends UI.Widget.VBox {
  private readonly tabbedPane: UI.TabbedPane.TabbedPane;
  request: {[x: string]: unknown}|undefined;
  response: {[x: string]: unknown}|undefined;
  type: 'sent'|'received'|undefined;
  selectedTab: 'request'|'response'|undefined;
  constructor(element: HTMLElement) {
    super(undefined, undefined, element);
    this.tabbedPane = new UI.TabbedPane.TabbedPane();
    this.tabbedPane.appendTab('request', i18nString(UIStrings.request), new UI.Widget.Widget());
    this.tabbedPane.appendTab('response', i18nString(UIStrings.response), new UI.Widget.Widget());
    this.tabbedPane.show(this.contentElement);
    this.tabbedPane.selectTab('response');
    this.request = {};
  }

  override performUpdate(): void {
    if (!this.request && !this.response) {
      this.tabbedPane.changeTabView(
          'request',
          new UI.EmptyWidget.EmptyWidget(
              i18nString(UIStrings.noMessageSelected), i18nString(UIStrings.selectAMessageToView)));
      this.tabbedPane.changeTabView(
          'response',
          new UI.EmptyWidget.EmptyWidget(
              i18nString(UIStrings.noMessageSelected), i18nString(UIStrings.selectAMessageToView)));
      return;
    }

    const requestEnabled = this.type && this.type === 'sent';
    this.tabbedPane.setTabEnabled('request', Boolean(requestEnabled));
    if (!requestEnabled) {
      this.tabbedPane.selectTab('response');
    }

    this.tabbedPane.changeTabView('request', SourceFrame.JSONView.JSONView.createViewSync(this.request || null));
    this.tabbedPane.changeTabView('response', SourceFrame.JSONView.JSONView.createViewSync(this.response || null));
    if (this.selectedTab) {
      this.tabbedPane.selectTab(this.selectedTab);
    }
  }
}

export const enum Events {
  COMMAND_SENT = 'CommandSent',
  COMMAND_CHANGE = 'CommandChange',
}

export interface EventTypes {
  [Events.COMMAND_SENT]: Command;
  [Events.COMMAND_CHANGE]: Command;
}

export class EditorWidget extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox) {
  readonly jsonEditor: JSONEditor;
  constructor() {
    super();
    this.element.setAttribute('jslog', `${VisualLogging.pane('command-editor').track({resize: true})}`);
    this.jsonEditor = new JSONEditor(metadataByCommand, typesByName as Map<string, Parameter[]>, enumsByName);
    this.jsonEditor.show(this.element);
    this.jsonEditor.addEventListener(
        JSONEditorEvents.SUBMIT_EDITOR,
        ({data}: Common.EventTarget.EventTargetEvent<Command>) =>
            this.dispatchEventToListeners(Events.COMMAND_SENT, data));
  }
}

export function parseCommandInput(input: string): {command: string, parameters: {[paramName: string]: unknown}} {
  // If input cannot be parsed as json, we assume it's the command name
  // for a command without parameters. Otherwise, we expect an object
  // with "command"/"method"/"cmd" and "parameters"/"params"/"args"/"arguments" attributes.
  let json = null;
  try {
    json = JSON.parse(input);
  } catch {
  }

  const command = json ? json.command || json.method || json.cmd || '' : input;
  const parameters = json?.parameters || json?.params || json?.args || json?.arguments || {};

  return {command, parameters};
}
