// 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.

import '../../ui/legacy/components/data_grid/data_grid.js';
import '../../ui/components/highlighting/highlighting.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 SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import type * as TextUtils from '../../models/text_utils/text_utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import {html, nothing, render} from '../../ui/lit/lit.js';

import developerResourcesListViewStyles from './developerResourcesListView.css.js';

const UIStrings = {
  /**
   * @description Text for the status of something
   */
  status: 'Status',
  /**
   * @description Text for web URLs
   */
  url: 'URL',
  /**
   * @description Text for the initiator of something
   */
  initiator: 'Initiator',
  /**
   * @description Text in Coverage List View of the Coverage tab
   */
  totalBytes: 'Total Bytes',
  /**
   * @description Column header. The column contains the time it took to load a resource.
   */
  duration: 'Duration',
  /**
   * @description Text for errors
   */
  error: 'Error',
  /**
   * @description Title for the Developer resources tab
   */
  developerResources: 'Developer resources',
  /**
   * @description Text for a context menu entry
   */
  copyUrl: 'Copy URL',
  /**
   * @description Text for a context menu entry. Command to copy a URL to the clipboard. The initiator
   * of a request is the entity that caused this request to be sent.
   */
  copyInitiatorUrl: 'Copy initiator URL',
  /**
   * @description Text for the status column of a list view
   */
  pending: 'pending',
  /**
   * @description Text for the status column of a list view
   */
  success: 'success',
  /**
   * @description Text for the status column of a list view
   */
  failure: 'failure',
  /**
   * @description Accessible text for the value in bytes in memory allocation.
   */
  sBytes: '{n, plural, =1 {# byte} other {# bytes}}',
  /**
   * @description Number of resource(s) match
   */
  numberOfResourceMatch: '{n, plural, =1 {# resource matches} other {# resources match}}',
  /**
   * @description No resource matches
   */
  noResourceMatches: 'No resource matches',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/developer_resources/DeveloperResourcesListView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const {withThousandsSeparator} = Platform.NumberUtilities;

export interface ViewInput {
  items: SDK.PageResourceLoader.PageResource[];
  selectedItem: SDK.PageResourceLoader.PageResource|null;
  filters: TextUtils.TextUtils.ParsedFilter[];
  onContextMenu: (e: CustomEvent<{menu: UI.ContextMenu.ContextMenu, element: HTMLElement}>) => void;
  onSelect: (e: CustomEvent<HTMLElement>) => void;
  onInitiatorMouseEnter: (frameId: Protocol.Page.FrameId|null) => void;
  onInitiatorMouseLeave: () => void;
}

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

const DEFAULT_VIEW: View = (input, _output, target) => {
  function highlightRange(textContent: string|undefined, columnId: string): string {
    if (!textContent) {
      return '';
    }
    const filter = input.filters.find(filter => filter.key?.split(',')?.includes(columnId));
    if (!filter?.regex) {
      return '';
    }
    const matches = filter.regex.exec(textContent);
    if (!matches?.length) {
      return '';
    }
    return `${matches.index},${matches[0].length}`;
  }
  // clang-format off
  render(html`
      <style>${developerResourcesListViewStyles}</style>
      <devtools-data-grid name=${i18nString(UIStrings.developerResources)} striped class="flex-auto"
         .filters=${input.filters} @contextmenu=${input.onContextMenu} @selected=${input.onSelect}>
        <table>
          <tr>
            <th id="status" sortable fixed width="60px">
              ${i18nString(UIStrings.status)}
            </th>
            <th id="url" sortable width="250px">
              ${i18nString(UIStrings.url)}
            </th>
            <th id="initiator" sortable width="80px">
              ${i18nString(UIStrings.initiator)}
            </th>
            <th id="size" sortable fixed width="80px" align="right">
              ${i18nString(UIStrings.totalBytes)}
            </th>
            <th id="duration" sortable fixed width="80px" align="right">
              ${i18nString(UIStrings.duration)}
            </th>
            <th id="error-message" sortable width="200px">
              ${i18nString(UIStrings.error)}
            </th>
          </tr>
          ${input.items.map((item, index) => {
            const splitURL = /^(.*)(\/[^/]*)$/.exec(item.url);
            return html`
            <tr selected=${(item === input.selectedItem) || nothing}
                data-url=${item.url ?? nothing}
                data-initiator-url=${item.initiator.initiatorUrl ?? nothing}
                data-index=${index}>
              <td>${item.success === true  ? i18nString(UIStrings.success) :
                    item.success === false ? i18nString(UIStrings.failure) :
                                             i18nString(UIStrings.pending)}</td>
              <td title=${item.url} aria-label=${item.url}>
                <devtools-highlight aria-hidden="true" part="url-outer"
                                    ranges=${highlightRange(item.url, 'url')}>
                  <div part="url-prefix">${splitURL ? splitURL[1] : item.url}</div>
                  <div part="url-suffix">${splitURL ? splitURL[2] : ''}</div>
                </devtools-highlight>
              </td>
              <td title=${item.initiator.initiatorUrl || ''}
                  aria-label=${item.initiator.initiatorUrl || ''}
                  @mouseenter=${() => input.onInitiatorMouseEnter(item.initiator.frameId)}
                  @mouseleave=${input.onInitiatorMouseLeave}
              >${item.initiator.initiatorUrl || ''}</td>
              <td aria-label=${item.size !== null ? i18nString(UIStrings.sBytes, {n: item.size}) : nothing}
                  data-value=${item.size ?? nothing}>${
                item.size !== null ?  html`<span>${withThousandsSeparator(item.size)}</span>` : ''}</td>
              <td aria-label=${item.duration !== null ? i18n.TimeUtilities.millisToString(item.duration) : nothing}
                  data-value=${item.duration ?? nothing}>${
                  item.duration !== null ? html`<span>${i18n.TimeUtilities.millisToString(item.duration)}</span>` : ''}</td>
              <td class="error-message">
                ${item.errorMessage ? html`
                <devtools-highlight ranges=${highlightRange(item.errorMessage, 'error-message')}>
                  ${item.errorMessage}
                </devtools-highlight>` : nothing}
              </td>
            </tr>`;
          })}
          </table>
        </devtools-data-grid>`,
        target);
  // clang-format on
};

export class DeveloperResourcesListView extends UI.Widget.VBox {
  #items: SDK.PageResourceLoader.PageResource[] = [];
  #selectedItem: SDK.PageResourceLoader.PageResource|null = null;
  #onSelect: ((item: SDK.PageResourceLoader.PageResource|null) => void)|null = null;
  readonly #view: View;
  #filters: TextUtils.TextUtils.ParsedFilter[] = [];
  constructor(element: HTMLElement, view = DEFAULT_VIEW) {
    super(element, {useShadowDom: true});
    this.#view = view;
  }

  set selectedItem(item: SDK.PageResourceLoader.PageResource|null) {
    this.#selectedItem = item;
    this.requestUpdate();
  }

  set onSelect(onSelect: (item: SDK.PageResourceLoader.PageResource|null) => void) {
    this.#onSelect = onSelect;
  }

  #populateContextMenu(contextMenu: UI.ContextMenu.ContextMenu, element: HTMLElement): void {
    const url = element.dataset.url;
    if (url) {
      contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyUrl), () => {
        Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(url);
      }, {jslogContext: 'copy-url'});
    }

    const initiatorUrl = element.dataset.initiatorUrl;
    if (initiatorUrl) {
      contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyInitiatorUrl), () => {
        Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(initiatorUrl);
      }, {jslogContext: 'copy-initiator-url'});
    }
  }

  set items(items: Iterable<SDK.PageResourceLoader.PageResource>) {
    this.#items = [...items];
    this.requestUpdate();
  }

  reset(): void {
    this.items = [];
    this.requestUpdate();
  }

  set filters(filters: TextUtils.TextUtils.ParsedFilter[]) {
    this.#filters = filters;
    this.requestUpdate();
    void this.updateComplete.then(() => {
      const numberOfResourceMatch =
          Number(this.contentElement.querySelector('devtools-data-grid')?.getAttribute('aria-rowcount')) ?? 0;
      let resourceMatch = '';
      if (numberOfResourceMatch === 0) {
        resourceMatch = i18nString(UIStrings.noResourceMatches);
      } else {
        resourceMatch = i18nString(UIStrings.numberOfResourceMatch, {n: numberOfResourceMatch});
      }
      UI.ARIAUtils.LiveAnnouncer.alert(resourceMatch);
    });
  }

  override performUpdate(): void {
    const input = {
      items: this.#items,
      selectedItem: this.#selectedItem,
      filters: this.#filters,
      onContextMenu: (e: CustomEvent<{menu: UI.ContextMenu.ContextMenu, element: HTMLElement}>) => {
        if (e.detail?.element) {
          this.#populateContextMenu(e.detail.menu, e.detail.element);
        }
      },
      onSelect: (e: CustomEvent<HTMLElement|null>) => {
        this.#selectedItem = e.detail ? this.#items[Number(e.detail.dataset.index)] : null;
        this.#onSelect?.(this.#selectedItem);
      },
      onInitiatorMouseEnter: (frameId: Protocol.Page.FrameId|null) => {
        const frame = frameId ? SDK.FrameManager.FrameManager.instance().getFrame(frameId) : null;
        if (frame) {
          void frame.highlight();
        }
      },
      onInitiatorMouseLeave: () => {
        SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
      },
    };
    const output = {};
    this.#view(input, output, this.contentElement);
  }
}
