// Copyright (c) 2020 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.

/* eslint-disable rulesdir/no_underscored_properties */

import * as DataGrid from '../data_grid/data_grid.js';
import * as Host from '../host/host.js';
import * as i18n from '../i18n/i18n.js';
import * as SDK from '../sdk/sdk.js';  // eslint-disable-line no-unused-vars
import * as TextUtils from '../text_utils/text_utils.js';
import * as UI from '../ui/ui.js';

export 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 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
  */
  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 a file size of 1 byte
  */
  Byte: '1 byte',
  /**
  *@description Accessible text for the value in bytes in memory allocation or coverage view.
  *@example {12345} PH1
  */
  sBytes: '{PH1} bytes',
};
const str_ = i18n.i18n.registerUIStrings('developer_resources/DeveloperResourcesListView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export class DeveloperResourcesListView extends UI.Widget.VBox {
  _nodeForItem: Map<SDK.PageResourceLoader.PageResource, GridNode>;
  _isVisibleFilter: (arg0: SDK.PageResourceLoader.PageResource) => boolean;
  _highlightRegExp: RegExp|null;
  _dataGrid: DataGrid.SortableDataGrid.SortableDataGrid<GridNode>;
  constructor(isVisibleFilter: (arg0: SDK.PageResourceLoader.PageResource) => boolean) {
    super(true);
    this._nodeForItem = new Map();
    this._isVisibleFilter = isVisibleFilter;
    this._highlightRegExp = null;
    this.registerRequiredCSS('developer_resources/developerResourcesListView.css', {enableLegacyPatching: false});

    const columns = [
      {id: 'status', title: i18nString(UIStrings.status), width: '60px', fixedWidth: true, sortable: true},
      {id: 'url', title: i18nString(UIStrings.url), width: '250px', fixedWidth: false, sortable: true},
      {id: 'initiator', title: i18nString(UIStrings.initiator), width: '80px', fixedWidth: false, sortable: true},
      {
        id: 'size',
        title: i18nString(UIStrings.totalBytes),
        width: '80px',
        fixedWidth: true,
        sortable: true,
        align: DataGrid.DataGrid.Align.Right,
      },
      {
        id: 'errorMessage',
        title: i18nString(UIStrings.error),
        width: '200px',
        fixedWidth: false,
        sortable: true,
      },
    ] as DataGrid.DataGrid.ColumnDescriptor[];
    this._dataGrid = new DataGrid.SortableDataGrid.SortableDataGrid({
      displayName: i18nString(UIStrings.developerResources),
      columns,
      editCallback: undefined,
      refreshCallback: undefined,
      deleteCallback: undefined,
    });
    this._dataGrid.setResizeMethod(DataGrid.DataGrid.ResizeMethod.Last);
    this._dataGrid.element.classList.add('flex-auto');
    this._dataGrid.addEventListener(DataGrid.DataGrid.Events.SortingChanged, this._sortingChanged, this);
    this._dataGrid.setRowContextMenuCallback(this._populateContextMenu.bind(this));

    const dataGridWidget = this._dataGrid.asWidget();
    dataGridWidget.show(this.contentElement);
    this.setDefaultFocusedChild(dataGridWidget);
  }

  _populateContextMenu(
      contextMenu: UI.ContextMenu.ContextMenu,
      gridNode: DataGrid.DataGrid.DataGridNode<
          DataGrid.ViewportDataGrid.ViewportDataGridNode<DataGrid.SortableDataGrid.SortableDataGridNode<GridNode>>>):
      void {
    const item = (gridNode as GridNode).item;
    contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyUrl), () => {
      Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(item.url);
    });
    if (item.initiator.initiatorUrl) {
      contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyInitiatorUrl), () => {
        Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(item.initiator.initiatorUrl);
      });
    }
  }

  update(items: Iterable<SDK.PageResourceLoader.PageResource>): void {
    let hadUpdates = false;
    const rootNode = this._dataGrid.rootNode();
    for (const item of items) {
      let node = this._nodeForItem.get(item);
      if (node) {
        if (this._isVisibleFilter(node.item)) {
          hadUpdates = node._refreshIfNeeded() || hadUpdates;
        }
        continue;
      }
      node = new GridNode(item);
      this._nodeForItem.set(item, node);
      if (this._isVisibleFilter(node.item)) {
        rootNode.appendChild(node);
        hadUpdates = true;
      }
    }
    if (hadUpdates) {
      this._sortingChanged();
    }
  }

  reset(): void {
    this._nodeForItem.clear();
    this._dataGrid.rootNode().removeChildren();
  }

  updateFilterAndHighlight(highlightRegExp: RegExp|null): void {
    this._highlightRegExp = highlightRegExp;
    let hadTreeUpdates = false;
    for (const node of this._nodeForItem.values()) {
      const shouldBeVisible = this._isVisibleFilter(node.item);
      const isVisible = Boolean(node.parent);
      if (shouldBeVisible) {
        node._setHighlight(this._highlightRegExp);
      }
      if (shouldBeVisible === isVisible) {
        continue;
      }
      hadTreeUpdates = true;
      if (!shouldBeVisible) {
        node.remove();
      } else {
        this._dataGrid.rootNode().appendChild(node);
      }
    }
    if (hadTreeUpdates) {
      this._sortingChanged();
    }
  }

  _sortingChanged(): void {
    const columnId = this._dataGrid.sortColumnId();
    if (!columnId) {
      return;
    }

    const sortFunction = GridNode.sortFunctionForColumn(columnId) as (
                             (arg0: DataGrid.SortableDataGrid.SortableDataGridNode<GridNode>,
                              arg1: DataGrid.SortableDataGrid.SortableDataGridNode<GridNode>) => number) |
        null;
    if (sortFunction) {
      this._dataGrid.sortNodes(sortFunction, !this._dataGrid.isSortOrderAscending());
    }
  }
}

class GridNode extends DataGrid.SortableDataGrid.SortableDataGridNode<GridNode> {
  item: SDK.PageResourceLoader.PageResource;
  _highlightRegExp: RegExp|null;
  constructor(item: SDK.PageResourceLoader.PageResource) {
    super();
    this.item = item;
    this._highlightRegExp = null;
  }

  _setHighlight(highlightRegExp: RegExp|null): void {
    if (this._highlightRegExp === highlightRegExp) {
      return;
    }
    this._highlightRegExp = highlightRegExp;
    this.refresh();
  }

  _refreshIfNeeded(): boolean {
    this.refresh();
    return true;
  }

  createCell(columnId: string): HTMLElement {
    const cell = this.createTD(columnId) as HTMLElement;
    switch (columnId) {
      case 'url': {
        UI.Tooltip.Tooltip.install(cell, this.item.url);
        const outer = cell.createChild('div', 'url-outer');
        const prefix = outer.createChild('div', 'url-prefix');
        const suffix = outer.createChild('div', 'url-suffix');
        const splitURL = /^(.*)(\/[^/]*)$/.exec(this.item.url);
        prefix.textContent = splitURL ? splitURL[1] : this.item.url;
        suffix.textContent = splitURL ? splitURL[2] : '';
        if (this._highlightRegExp) {
          this._highlight(outer, this.item.url);
        }
        this.setCellAccessibleName(this.item.url, cell, columnId);
        break;
      }
      case 'initiator': {
        const url = this.item.initiator.initiatorUrl || '';
        cell.textContent = url;
        UI.Tooltip.Tooltip.install(cell, url);
        this.setCellAccessibleName(url, cell, columnId);
        cell.onmouseenter = (): void => {
          const frame = SDK.FrameManager.FrameManager.instance().getFrame(this.item.initiator.frameId || '');
          if (frame) {
            frame.highlight();
          }
        };
        cell.onmouseleave = (): void => SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
        break;
      }
      case 'status': {
        if (this.item.success === null) {
          cell.textContent = i18nString(UIStrings.pending);
        } else {
          cell.textContent = this.item.success ? i18nString(UIStrings.success) : i18nString(UIStrings.failure);
        }
        break;
      }
      case 'size': {
        const size = this.item.size;
        if (size !== null) {
          const sizeSpan = cell.createChild('span');
          sizeSpan.textContent = Number.withThousandsSeparator(size);
          const sizeAccessibleName =
              (size === 1) ? i18nString(UIStrings.Byte) : i18nString(UIStrings.sBytes, {PH1: size});
          this.setCellAccessibleName(sizeAccessibleName, cell, columnId);
        }
        break;
      }
      case 'errorMessage': {
        cell.classList.add('error-message');
        if (this.item.errorMessage) {
          cell.textContent = this.item.errorMessage;
          if (this._highlightRegExp) {
            this._highlight(cell, this.item.errorMessage);
          }
        }
        break;
      }
    }
    return cell;
  }

  _highlight(element: Element, textContent: string): void {
    if (!this._highlightRegExp) {
      return;
    }
    const matches = this._highlightRegExp.exec(textContent);
    if (!matches || !matches.length) {
      return;
    }
    const range = new TextUtils.TextRange.SourceRange(matches.index, matches[0].length);
    UI.UIUtils.highlightRangesWithStyleClass(element, [range], 'filter-highlight');
  }

  static sortFunctionForColumn(columnId: string): ((arg0: GridNode, arg1: GridNode) => number)|null {
    const nullToNegative = (x: boolean|number|null): number => x === null ? -1 : Number(x);
    switch (columnId) {
      case 'url':
        return (a: GridNode, b: GridNode): number => a.item.url.localeCompare(b.item.url);
      case 'status':
        return (a: GridNode, b: GridNode): number => {
          return nullToNegative(a.item.success) - nullToNegative(b.item.success);
        };
      case 'size':
        return (a: GridNode, b: GridNode): number => nullToNegative(a.item.size) - nullToNegative(b.item.size);
      case 'initiator':
        return (a: GridNode, b: GridNode): number =>
                   (a.item.initiator.initiatorUrl || '').localeCompare(b.item.initiator.initiatorUrl || '');
      case 'errorMessage':
        return (a: GridNode, b: GridNode): number =>
                   (a.item.errorMessage || '').localeCompare(b.item.errorMessage || '');
      default:
        console.assert(false, 'Unknown sort field: ' + columnId);
        return null;
    }
  }
}
