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

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 SDK from '../../core/sdk/sdk.js';
import * as Buttons from '../../ui/components/buttons/buttons.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';

import eventSourceMessagesViewStyles from './eventSourceMessagesView.css.js';

const {repeat} = Directives;

const UIStrings = {
  /**
   * @description Text in Event Source Messages View of the Network panel
   */
  id: 'Id',
  /**
   * @description Text that refers to some types
   */
  type: 'Type',
  /**
   * @description Text in Event Source Messages View of the Network panel
   */
  data: 'Data',
  /**
   * @description Text that refers to the time
   */
  time: 'Time',
  /**
   * @description Data grid name for Event Source data grids
   */
  eventSource: 'Event Source',
  /**
   * @description A context menu item in the Resource Web Socket Frame View of the Network panel
   */
  copyMessage: 'Copy message',
  /**
   * @description Text to clear everything
   */
  clearAll: 'Clear all',
  /**
   * @description Example for placeholder text
   */
  filterByRegex: 'Filter using regex (example: https?)',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/network/EventSourceMessagesView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export interface ViewInput {
  messages: readonly SDK.NetworkRequest.EventSourceMessage[];
  filterSetting: Common.Settings.Setting<string>;
  onClear: () => void;
  onFilterChanged: (event: Event) => void;
  onRowContextMenu: (message: SDK.NetworkRequest.EventSourceMessage, event: MouseEvent) => void;
}

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

export const DEFAULT_VIEW: View = (input, _output, target) => {
  // clang-format off
  render(html`
    <style>
      ${eventSourceMessagesViewStyles}
    </style>
    <div class="event-source-messages-view">
      <devtools-toolbar>
        <devtools-button title=${i18nString(UIStrings.clearAll)} .iconName=${'clear'}
            @click=${input.onClear}
            .variant=${Buttons.Button.Variant.TOOLBAR}
            .jslogContext=${'clear'}></devtools-button>
        <devtools-toolbar-input
            type="filter"
            placeholder=${i18nString(UIStrings.filterByRegex)}
            @change=${input.onFilterChanged}
            .value=${input.filterSetting.get()}
            style="flex-grow: 0.4"></devtools-toolbar-input>
      </devtools-toolbar>
      <devtools-data-grid name=${i18nString(UIStrings.eventSource)} autoscroll striped
        .template=${html`
          <table>
            <thead>
              <tr>
                <th id="id" weight="8" sortable>${i18nString(UIStrings.id)}</th>
                <th id="type" weight="8" sortable>${i18nString(UIStrings.type)}</th>
                <th id="data" weight="88">${i18nString(UIStrings.data)}</th>
                <th id="time" weight="8" sortable sort="ascending">${i18nString(UIStrings.time)}</th>
              </tr>
            </thead>
            <tbody>
              ${repeat(input.messages, message => message, message => {
                const date = new Date(message.time * 1000);
                return html`<tr @contextmenu=${(e: MouseEvent) => {
                                e.preventDefault();
                                input.onRowContextMenu(message, e);
                              }}>
                                <td>${message.eventId}</td>
                                <td>${message.eventName}</td>
                                <td>${message.data}</td>
                                <td title=${date.toLocaleString()} data-value=${message.time}>${formatTime(date)}</td></tr>`;
              })}
            </tbody>
          </table>
        `}>
      </devtools-data-grid>
    </div>
  `, target);
  // clang-format on
};

export class EventSourceMessagesView extends UI.Widget.VBox {
  private readonly request: SDK.NetworkRequest.NetworkRequest;
  private messageFilterSetting: Common.Settings.Setting<string>;
  private filterRegex: RegExp|null = null;
  #view: View;

  constructor(request: SDK.NetworkRequest.NetworkRequest, view: View = DEFAULT_VIEW) {
    super({jslog: `${VisualLogging.pane('event-stream').track({resize: true})}`});
    this.#view = view;
    this.request = request;

    this.messageFilterSetting =
        Common.Settings.Settings.instance().createSetting('network-event-source-message-filter', '');
    const filter = this.messageFilterSetting.get();
    this.setFilter(filter);
  }

  override wasShown(): void {
    super.wasShown();
    this.request.addEventListener(SDK.NetworkRequest.Events.EVENT_SOURCE_MESSAGE_ADDED, this.messageAdded, this);
    this.requestUpdate();
  }

  override willHide(): void {
    super.willHide();
    this.request.removeEventListener(SDK.NetworkRequest.Events.EVENT_SOURCE_MESSAGE_ADDED, this.messageAdded, this);
  }

  private messageAdded(event: Common.EventTarget.EventTargetEvent<SDK.NetworkRequest.EventSourceMessage>): void {
    const message = event.data;
    if (!this.messageFilter(message)) {
      return;
    }
    this.requestUpdate();
  }

  private messageFilter(message: SDK.NetworkRequest.EventSourceMessage): boolean {
    return !this.filterRegex || this.filterRegex.test(message.eventName) || this.filterRegex.test(message.eventId) ||
        this.filterRegex.test(message.data);
  }

  private clearMessages(): void {
    clearMessageOffsets.set(this.request, this.request.eventSourceMessages().length);
    this.requestUpdate();
  }

  private onFilterChanged(event: Event): void {
    const inputElement = event.target as UI.Toolbar.ToolbarInputElement;
    const text = inputElement.value;
    this.messageFilterSetting.set(text);
    this.setFilter(text);
    this.requestUpdate();
  }

  private setFilter(text: string): void {
    this.filterRegex = null;
    if (text) {
      try {
        this.filterRegex = new RegExp(text, 'i');
      } catch {
        // this regex will never match any input
        this.filterRegex = new RegExp('(?!)', 'i');
      }
    }
  }

  private onRowContextMenu(message: SDK.NetworkRequest.EventSourceMessage, event: MouseEvent): void {
    const contextMenu = new UI.ContextMenu.ContextMenu(event);
    contextMenu.clipboardSection().appendItem(
        i18nString(UIStrings.copyMessage),
        Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText.bind(
            Host.InspectorFrontendHost.InspectorFrontendHostInstance, message.data),
        {jslogContext: 'copy'});
    void contextMenu.show();
  }

  override performUpdate(): void {
    let messages = this.request.eventSourceMessages();
    const offset = clearMessageOffsets.get(this.request) || 0;
    messages = messages.slice(offset);
    messages = messages.filter(this.messageFilter.bind(this));

    const input: ViewInput = {
      messages,
      filterSetting: this.messageFilterSetting,
      onClear: this.clearMessages.bind(this),
      onFilterChanged: this.onFilterChanged.bind(this),
      onRowContextMenu: this.onRowContextMenu.bind(this),
    };

    this.#view(input, {}, this.contentElement);
  }
}

function formatTime(d: Date): string {
  return ('0' + d.getHours()).substr(-2) + ':' + ('0' + d.getMinutes()).substr(-2) + ':' +
      ('0' + d.getSeconds()).substr(-2) + '.' + ('00' + d.getMilliseconds()).substr(-3);
}

const clearMessageOffsets = new WeakMap<SDK.NetworkRequest.NetworkRequest, number>();
