// Copyright 2023 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/kit/kit.js';

import * as Common from '../../../../core/common/common.js';
import * as i18n from '../../../../core/i18n/i18n.js';
import type * as Platform from '../../../../core/platform/platform.js';
import {assertNotNullOrUndefined} from '../../../../core/platform/platform.js';
import * as SDK from '../../../../core/sdk/sdk.js';
import * as Protocol from '../../../../generated/protocol.js';
import * as UI from '../../../../ui/legacy/legacy.js';
import {Directives, html, type LitTemplate, nothing, render} from '../../../../ui/lit/lit.js';
import * as VisualLogging from '../../../../ui/visual_logging/visual_logging.js';
import * as NetworkForward from '../../../network/forward/forward.js';
import * as PreloadingHelper from '../helper/helper.js';

import * as PreloadingString from './PreloadingString.js';
import ruleSetGridStyles from './ruleSetGrid.css.js';

const {styleMap} = Directives;

const UIStrings = {
  /**
   * @description Column header: Short URL of rule set.
   */
  ruleSet: 'Rule set',
  /**
   * @description Column header: Show how many preloads are associated if valid, error counts if invalid.
   */
  status: 'Status',
  /**
   * @description button: Title of button to reveal the corresponding request of rule set in Elements panel
   */
  clickToOpenInElementsPanel: 'Click to open in Elements panel',
  /**
   * @description button: Title of button to reveal the corresponding request of rule set in Network panel
   */
  clickToOpenInNetworkPanel: 'Click to open in Network panel',
  /**
   * @description Value of status, specifying rule set contains how many errors.
   */
  errors: '{errorCount, plural, =1 {# error} other {# errors}}',
  /**
   * @description button: Title of button to reveal preloading attempts with filter by selected rule set
   */
  buttonRevealPreloadsAssociatedWithRuleSet: 'Reveal speculative loads associated with this rule set',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/application/preloading/components/RuleSetGrid.ts', UIStrings);
export const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export interface RuleSetGridData {
  rows: RuleSetGridRow[];
  pageURL: Platform.DevToolsPath.UrlString;
}

export interface RuleSetGridRow {
  ruleSet: Protocol.Preload.RuleSet;
  preloadsStatusSummary: string;
}

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

export interface ViewInput {
  data: RuleSetGridData|null;
  onSelect: (ruleSetId: Protocol.Preload.RuleSetId) => void;
  onRevealInElements: (ruleSet: Protocol.Preload.RuleSet) => void;
  onRevealInNetwork: (ruleSet: Protocol.Preload.RuleSet) => void;
  onRevealPreloadsAssociatedWithRuleSet: (ruleSet: Protocol.Preload.RuleSet) => void;
}

export type ViewOutput = unknown;

export const DEFAULT_VIEW: View = (input, _output, target) => {
  let template: LitTemplate = nothing;
  if (input.data !== null) {
    const {rows, pageURL} = input.data;

    // Disabled until https://crbug.com/1079231 is fixed.
    // clang-format off
    template =
      html`
          <style>${ruleSetGridStyles}</style>
          <div class="ruleset-container" jslog=${VisualLogging.pane('preloading-rules')}>
            <devtools-data-grid striped>
              <table>
                <tr>
                  <th id="rule-set" weight="20" sortable>
                    ${i18nString(UIStrings.ruleSet)}
                  </th>
                  <th id="status" weight="80" sortable>
                    ${i18nString(UIStrings.status)}
                  </th>
                </tr>
                ${rows.map(({ruleSet, preloadsStatusSummary}) => {
                  const location = PreloadingString.ruleSetTagOrLocationShort(ruleSet, pageURL);
                  const revealInElements = ruleSet.backendNodeId !== undefined;
                  const revealInNetwork = ruleSet.url !== undefined && ruleSet.requestId;
                  return html`
                    <tr @select=${() => input.onSelect(ruleSet.id)}>
                      <td>
                        ${revealInElements || revealInNetwork ? html`
                          <button class="link" role="link"
                              @click=${() => {
                                if (revealInElements) {
                                  input.onRevealInElements(ruleSet);
                                } else {
                                  input.onRevealInNetwork(ruleSet);
                                }
                              }}
                              title=${revealInElements ? i18nString(UIStrings.clickToOpenInElementsPanel)
                                                      : i18nString(UIStrings.clickToOpenInNetworkPanel)}
                              style=${styleMap({
                                border: 'none',
                                background: 'none',
                                color: 'var(--icon-link)',
                                cursor: 'pointer',
                                'text-decoration': 'underline',
                                'padding-inline-start': '0',
                                'padding-inline-end': '0',
                              })}
                              jslog=${VisualLogging
                                  .action(revealInElements ? 'reveal-in-elements' : 'reveal-in-network')
                                  .track({click: true})}
                            >
                              <devtools-icon name=${revealInElements ? 'code-circle' : 'arrow-up-down-circle'} class="medium"
                                style=${styleMap({
                                  color: 'var(--icon-link)',
                                  'vertical-align': 'sub',
                                })}
                              ></devtools-icon>
                              ${location}
                            </button>`
                            : location}
                    </td>
                    <td>
                      ${ruleSet.errorType !== undefined ? html`
                        <span style=${styleMap({color: 'var(--sys-color-error)'})}>
                          ${i18nString(UIStrings.errors, {errorCount: 1})}
                        </span>` : ''} ${ruleSet.errorType !== Protocol.Preload.RuleSetErrorType.SourceIsNotJsonObject &&
                        ruleSet.errorType !== Protocol.Preload.RuleSetErrorType.InvalidRulesetLevelTag ?
                        html`
                        <button class="link" role="link"
                          @click=${() => input.onRevealPreloadsAssociatedWithRuleSet(ruleSet)}
                          title=${i18nString(UIStrings.buttonRevealPreloadsAssociatedWithRuleSet)}
                          style=${styleMap({
                            color: 'var(--sys-color-primary)',
                            'text-decoration': 'underline',
                            cursor: 'pointer',
                            border: 'none',
                            background: 'none',
                            'padding-inline-start': '0',
                            'padding-inline-end': '0',
                          })}
                          jslog=${VisualLogging.action('reveal-preloads').track({click: true})}>
                          ${preloadsStatusSummary}
                        </button>` : ''}
                    </td>
                  </tr>
                `;})}
              </table>
            </devtools-data-grid>
          </div>`;
    // clang-format on
  }
  render(template, target);
};

/** Grid component to show SpeculationRules rule sets. **/
export class RuleSetGrid extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox) {
  readonly #view: View;
  #data: RuleSetGridData|null = null;

  constructor(view: View = DEFAULT_VIEW) {
    super({useShadowDom: true});
    this.#view = view;
  }

  get data(): RuleSetGridData|null {
    return this.#data;
  }

  set data(data: RuleSetGridData|null) {
    this.#data = data;
    this.requestUpdate();
  }

  override performUpdate(): void {
    const input: ViewInput = {
      data: this.#data,
      onSelect: this.dispatchEventToListeners.bind(this, Events.SELECT),
      onRevealInElements: this.#revealSpeculationRulesInElements.bind(this),
      onRevealInNetwork: this.#revealSpeculationRulesInNetwork.bind(this),
      onRevealPreloadsAssociatedWithRuleSet: this.#revealAttemptViewWithFilter.bind(this),
    };
    const output = undefined;
    this.#view(input, output, this.contentElement);
  }

  #revealSpeculationRulesInElements(ruleSet: Protocol.Preload.RuleSet): void {
    assertNotNullOrUndefined(ruleSet.backendNodeId);

    const target = SDK.TargetManager.TargetManager.instance().scopeTarget();
    if (target === null) {
      return;
    }

    void Common.Revealer.reveal(new SDK.DOMModel.DeferredDOMNode(target, ruleSet.backendNodeId));
  }

  #revealSpeculationRulesInNetwork(ruleSet: Protocol.Preload.RuleSet): void {
    assertNotNullOrUndefined(ruleSet.requestId);
    const request = SDK.TargetManager.TargetManager.instance()
                        .scopeTarget()
                        ?.model(SDK.NetworkManager.NetworkManager)
                        ?.requestForId(ruleSet.requestId) ||
        null;
    if (request === null) {
      return;
    }

    const requestLocation = NetworkForward.UIRequestLocation.UIRequestLocation.tab(
        request, NetworkForward.UIRequestLocation.UIRequestTabs.PREVIEW, {clearFilter: false});
    void Common.Revealer.reveal(requestLocation);
  }

  #revealAttemptViewWithFilter(ruleSet: Protocol.Preload.RuleSet): void {
    void Common.Revealer.reveal(new PreloadingHelper.PreloadingForward.AttemptViewWithFilter(ruleSet.id));
  }
}

export const enum Events {
  SELECT = 'select',
}

export interface EventTypes {
  [Events.SELECT]: Protocol.Preload.RuleSetId;
}
