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

import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as SDK from '../../core/sdk/sdk.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, nothing, render, type TemplateResult} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';

import layerTreeOutlineStyles from './layerTreeOutline.css.js';
import {
  LayerSelection,
  type LayerView,
  type LayerViewHost,
  type Selection,
  type SnapshotSelection,
} from './LayerViewHost.js';

const UIStrings = {
  /**
   * @description A count of the number of rendering layers in Layer Tree Outline of the Layers panel
   * @example {10} PH1
   */
  layerCount: '{PH1} layers',
  /**
   * @description Label for layers sidepanel tree
   */
  layersTreePane: 'Layers Tree Pane',
  /**
   * @description A context menu item in the DView of the Layers panel
   */
  showPaintProfiler: 'Show Paint Profiler',
  /**
   * @description Details text content in Layer Tree Outline of the Layers panel
   * @example {10} PH1
   * @example {10} PH2
   */
  updateChildDimension: ' ({PH1} × {PH2})',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/layer_viewer/LayerTreeOutline.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

const {ref, repeat} = Directives;
export interface LayerTreeNode {
  layer: SDK.LayerTreeBase.Layer;
  isExpanded: boolean;
  children: LayerTreeNode[];
}

export interface ViewInput {
  treeData: LayerTreeNode[];
  selectedLayer: SDK.LayerTreeBase.Layer|null;
  hoveredLayer: SDK.LayerTreeBase.Layer|null;
  layerCount: number;
  totalLayerMemory: number;
  onSelect: (layer: SDK.LayerTreeBase.Layer) => void;
  onHover: (layer: SDK.LayerTreeBase.Layer|null) => void;
  onContextMenu: (event: MouseEvent, layer: SDK.LayerTreeBase.Layer|null) => void;
}

export interface ViewOutput {
  focusTree?: () => void;
  revealLayer?: (layer: SDK.LayerTreeBase.Layer) => void;
}

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

export const DEFAULT_VIEW: View = (input, output, target) => {
  const renderNode = (node: LayerTreeNode): TemplateResult => {
    const layer = node.layer;
    const domNode = layer.nodeForSelfOrAncestor();
    const titleText = domNode ? domNode.simpleSelector() : '#' + layer.id();
    const detailsText = i18nString(UIStrings.updateChildDimension, {PH1: layer.width(), PH2: layer.height()});
    const isSelected = layer === input.selectedLayer;
    const isHovered = layer === input.hoveredLayer;

    // clang-format off
    return html`
      <li role="treeitem"
          data-layer-id=${layer.id()}
          class=${isHovered ? 'hovered' : ''}
          ?selected=${isSelected}
          aria-expanded=${node.isExpanded ? 'true' : 'false'}
          @select=${() => input.onSelect(layer)}
          @mouseenter=${() => input.onHover(layer)}
          @mouseleave=${() => input.onHover(null)}>
        <span class="tree-node-title" @contextmenu=${(event: MouseEvent) => {
          input.onContextMenu(event, layer);
          event.stopPropagation();
        }}>
          ${titleText} <span class="dimmed">${detailsText}</span>
        </span>
        ${node.children.length > 0 ? html`<ul role="group">${repeat(node.children, n => n.layer.id(), renderNode)}</ul>` : nothing}
      </li>
    `;
    // clang-format on
  };

  // clang-format off
  render(html`
    <style>${layerTreeOutlineStyles}</style>
    <div class="vbox layer-tree-wrapper" jslog=${VisualLogging.pane('layers-tree')}>
      <devtools-tree class="layer-tree overflow-auto"
        @contextmenu=${(event: MouseEvent) => input.onContextMenu(event, null)}
        ${ref((el: Element | undefined) => {
          if (el) {
            output.focusTree = () => {
              (el as UI.TreeOutline.TreeViewElement).focus();
            };
            output.revealLayer = (layer: SDK.LayerTreeBase.Layer) => {
              const li = el.querySelector(`li[data-layer-id="${layer.id()}"]`);
              if (li) {
                // Toggling the selected attribute forces TreeViewElement to scroll the node into view.
                li.removeAttribute('selected');
                li.setAttribute('selected', '');
              }
            };
          }
        })}
        .template=${html`
          <style>${layerTreeOutlineStyles}</style>
          <ul role="tree" aria-label=${i18nString(UIStrings.layersTreePane)}>
            ${input.treeData.map(renderNode)}
          </ul>
        `}>
      </devtools-tree>
      <div class="hbox layer-summary">
        <span class="layer-count">${i18nString(UIStrings.layerCount, {
           PH1: input.layerCount,
         })}</span>
        <span>${i18n.ByteUtilities.bytesToString(input.totalLayerMemory)}</span>
      </div>
    </div>
  `, target);
  // clang-format on
};

export class LayerTreeOutline extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.Widget>(
    UI.Widget.Widget) implements Common.EventTarget.EventTarget<EventTypes>, LayerView {
  private layerViewHost: LayerViewHost;
  private layerTree?: SDK.LayerTreeBase.LayerTreeBase|null;
  private layerSnapshotMap?: Map<SDK.LayerTreeBase.Layer, SnapshotSelection>;
  #layerCount = 0;
  #totalLayerMemory = 0;
  #treeData: LayerTreeNode[] = [];
  #viewOutput: ViewOutput = {};
  #view: View;
  #hoveredLayer: SDK.LayerTreeBase.Layer|null = null;

  constructor(layerViewHost: LayerViewHost, view: View = DEFAULT_VIEW) {
    super();
    this.#view = view;
    this.layerViewHost = layerViewHost;
    this.layerViewHost.registerView(this);

    this.layerViewHost.showInternalLayersSetting().addChangeListener(this.update, this);
  }

  override wasShown(): void {
    super.wasShown();
    this.requestUpdate();
  }

  override performUpdate(): void {
    this.#view({
      treeData: this.#treeData,
      selectedLayer: this.layerViewHost.selection()?.layer() || null,
      hoveredLayer: this.#hoveredLayer,
      layerCount: this.#layerCount,
      totalLayerMemory: this.#totalLayerMemory,
      onSelect: this.onSelect.bind(this),
      onHover: this.onHover.bind(this),
      onContextMenu: this.onContextMenu.bind(this),
    },
               this.#viewOutput, this.contentElement);
  }

  override focus(): void {
    this.#viewOutput.focusTree?.();
  }

  selectObject(selection: Selection|null): void {
    this.hoverObject(null);
    this.requestUpdate();
    if (selection) {
      // Defer revealLayer to ensure Lit has finished rendering DOM updates
      queueMicrotask(() => {
        this.#viewOutput.revealLayer?.(selection.layer());
      });
    }
  }

  hoverObject(selection: Selection|null): void {
    this.#hoveredLayer = selection?.layer() || null;
    this.requestUpdate();
  }

  setLayerTree(layerTree: SDK.LayerTreeBase.LayerTreeBase|null): void {
    this.layerTree = layerTree;
    this.update();
  }

  private update(): void {
    let layerCount = 0;
    let totalLayerMemory = 0;

    const showInternalLayers = this.layerViewHost.showInternalLayersSetting().get();
    let root: SDK.LayerTreeBase.Layer|null = null;
    if (this.layerTree) {
      if (!showInternalLayers) {
        root = this.layerTree.contentRoot();
      }
      if (!root) {
        root = this.layerTree.root();
      }
    }

    const childrenMap = new Map<SDK.LayerTreeBase.Layer, SDK.LayerTreeBase.Layer[]>();

    if (this.layerTree && root) {
      const buildTree = (layer: SDK.LayerTreeBase.Layer): void => {
        if (!layer.drawsContent() && !showInternalLayers) {
          return;
        }

        layerCount++;
        totalLayerMemory += layer.gpuMemoryUsage();

        if (layer === root) {
          return;
        }

        let parentLayer = layer.parent();
        // Skip till nearest visible ancestor.
        while (parentLayer && parentLayer !== root && !parentLayer.drawsContent() && !showInternalLayers) {
          parentLayer = parentLayer.parent();
        }

        if (parentLayer) {
          let children = childrenMap.get(parentLayer);
          if (!children) {
            children = [];
            childrenMap.set(parentLayer, children);
          }
          children.push(layer);
        } else {
          console.assert(false, 'Internal error: multiple root layers');
        }
      };

      this.layerTree.forEachLayer(buildTree, root);
    }

    const makeTreeNode = (layer: SDK.LayerTreeBase.Layer): LayerTreeNode => {
      const children = childrenMap.get(layer) || [];
      return {
        layer,
        isExpanded: !layer.drawsContent(),
        children: children.map(makeTreeNode),
      };
    };

    this.#treeData = root && (root.drawsContent() || showInternalLayers) ? [makeTreeNode(root)] : [];
    this.#layerCount = layerCount;
    this.#totalLayerMemory = totalLayerMemory;
    this.requestUpdate();

    if (!this.layerViewHost.selection() && this.layerTree && this.#treeData.length > 0) {
      const elementToSelect = this.layerTree.contentRoot() || this.layerTree.root();
      if (elementToSelect) {
        this.layerViewHost.selectObject(new LayerSelection(elementToSelect));
      }
    }
  }

  private onHover(layer: SDK.LayerTreeBase.Layer|null): void {
    this.layerViewHost.hoverObject(layer ? new LayerSelection(layer) : null);
  }

  private onSelect(layer: SDK.LayerTreeBase.Layer): void {
    if (this.layerViewHost.selection()?.layer() === layer) {
      return;
    }
    this.layerViewHost.selectObject(new LayerSelection(layer));
  }

  private onContextMenu(event: MouseEvent, layer: SDK.LayerTreeBase.Layer|null): void {
    const selection = layer ? new LayerSelection(layer) : null;
    const contextMenu = new UI.ContextMenu.ContextMenu(event);
    this.layerSnapshotMap = this.layerViewHost.getLayerSnapshotMap();
    if (layer && this.layerSnapshotMap.has(layer) && selection) {
      contextMenu.defaultSection().appendItem(
          i18nString(UIStrings.showPaintProfiler),
          () => this.dispatchEventToListeners(Events.PAINT_PROFILER_REQUESTED, selection),
          {jslogContext: 'layers.paint-profiler'});
    }
    this.layerViewHost.showContextMenu(contextMenu, selection);
  }
}

export const enum Events {
  PAINT_PROFILER_REQUESTED = 'PaintProfilerRequested',
}

export interface EventTypes {
  [Events.PAINT_PROFILER_REQUESTED]: Selection;
}
