// 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 {html, render} from '../../ui/lit/lit.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_);

export interface ViewInput {
  treeOutlineElement: HTMLElement;
  layerCount: number;
  totalLayerMemory: number;
}
export type View = (input: ViewInput, output: object, target: HTMLElement) => void;

const DEFAULT_VIEW: View = (input, _output, target) => {
  render(html`
    <style>${layerTreeOutlineStyles}</style>
    <div class="vbox layer-tree-wrapper">
      <div style="flex-grow: 1; overflow: auto; display: flex;">
        ${input.treeOutlineElement}
      </div>
      <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);
};

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 treeOutline: UI.TreeOutline.TreeOutlineInShadow;
  private lastHoveredNode: LayerTreeElement|null;
  private layerTree?: SDK.LayerTreeBase.LayerTreeBase|null;
  private layerSnapshotMap?: Map<SDK.LayerTreeBase.Layer, SnapshotSelection>;

  #view: View;
  #layerCount = 0;
  #totalLayerMemory = 0;

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

    this.treeOutline = new UI.TreeOutline.TreeOutlineInShadow();
    this.treeOutline.element.classList.add('layer-tree', 'overflow-auto');
    this.treeOutline.element.addEventListener('mousemove', this.onMouseMove.bind(this) as EventListener, false);
    this.treeOutline.element.addEventListener('mouseout', this.onMouseMove.bind(this) as EventListener, false);
    this.treeOutline.element.addEventListener('contextmenu', this.onContextMenu.bind(this) as EventListener, true);
    UI.ARIAUtils.setLabel(this.treeOutline.contentElement, i18nString(UIStrings.layersTreePane));

    this.lastHoveredNode = null;

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

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

  override performUpdate(): void {
    this.#view({
      treeOutlineElement: this.treeOutline.element,
      layerCount: this.#layerCount,
      totalLayerMemory: this.#totalLayerMemory,
    },
               {}, this.contentElement);
  }

  override focus(): void {
    this.treeOutline.focus();
  }

  selectObject(selection: Selection|null): void {
    this.hoverObject(null);
    const layer = selection?.layer();
    const node = layer && layerToTreeElement.get(layer);
    if (node) {
      node.revealAndSelect(true);
    } else if (this.treeOutline.selectedTreeElement) {
      this.treeOutline.selectedTreeElement.deselect();
    }
  }

  hoverObject(selection: Selection|null): void {
    const layer = selection?.layer();
    const node = layer && layerToTreeElement.get(layer);
    if (node === this.lastHoveredNode) {
      return;
    }
    if (this.lastHoveredNode) {
      this.lastHoveredNode.setHovered(false);
    }
    if (node) {
      node.setHovered(true);
    }
    this.lastHoveredNode = node as LayerTreeElement;
  }

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

  private update(): void {
    const showInternalLayers = this.layerViewHost.showInternalLayersSetting().get();
    const seenLayers = new Set<SDK.LayerTreeBase.Layer>();
    let root: (SDK.LayerTreeBase.Layer|null)|null = null;
    if (this.layerTree) {
      if (!showInternalLayers) {
        root = this.layerTree.contentRoot();
      }
      if (!root) {
        root = this.layerTree.root();
      }
    }

    let layerCount = 0;
    let totalLayerMemory = 0;

    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 syncNode =
        (layer: SDK.LayerTreeBase.Layer, parent: UI.TreeOutline.TreeOutline|UI.TreeOutline.TreeElement): void => {
          seenLayers.add(layer);
          let node: LayerTreeElement|null = layerToTreeElement.get(layer) || null;
          if (!node) {
            node = new LayerTreeElement(this, layer);
            parent.appendChild(node);
            // Expand all new non-content layers to expose content layers better.
            if (!layer.drawsContent()) {
              node.expand();
            }
          } else {
            if (node.parent !== parent) {
              const oldSelection = this.treeOutline.selectedTreeElement;
              if (node.parent) {
                node.parent.removeChild(node);
              }
              parent.appendChild(node);
              if (oldSelection && oldSelection !== this.treeOutline.selectedTreeElement) {
                oldSelection.select();
              }
            }
            node.update();
          }

          const children = childrenMap.get(layer) || [];
          for (const child of children) {
            syncNode(child, node);
          }
        };

    if (root && (root.drawsContent() || showInternalLayers)) {
      syncNode(root, this.treeOutline.rootElement());
    }

    // Clean up layers that don't exist anymore from tree.
    const rootElement = this.treeOutline.rootElement();
    for (let node = rootElement.firstChild(); node instanceof LayerTreeElement && !node.root;) {
      if (seenLayers.has(node.layer)) {
        node = node.traverseNextTreeElement(false);
      } else {
        const nextNode = node.nextSibling || node.parent;
        if (node.parent) {
          node.parent.removeChild(node);
        }
        if (node === this.lastHoveredNode) {
          this.lastHoveredNode = null;
        }
        node = nextNode;
      }
    }
    if (!this.treeOutline.selectedTreeElement && this.layerTree) {
      const elementToSelect = this.layerTree.contentRoot() || this.layerTree.root();
      if (elementToSelect) {
        const layer = layerToTreeElement.get(elementToSelect);
        if (layer) {
          layer.revealAndSelect(true);
        }
      }
    }

    this.#layerCount = layerCount;
    this.#totalLayerMemory = totalLayerMemory;
    this.requestUpdate();
  }

  private onMouseMove(event: MouseEvent): void {
    const node = this.treeOutline.treeElementFromEvent(event) as LayerTreeElement | null;
    if (node === this.lastHoveredNode) {
      return;
    }
    this.layerViewHost.hoverObject(this.selectionForNode(node));
  }

  selectedNodeChanged(node: LayerTreeElement): void {
    this.layerViewHost.selectObject(this.selectionForNode(node));
  }

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

  private selectionForNode(node: LayerTreeElement|null): Selection|null {
    return node?.layer ? new LayerSelection(node.layer) : null;
  }
}

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

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

export class LayerTreeElement extends UI.TreeOutline.TreeElement {
  // Watch out: This is different from treeOutline that
  // LayerTreeElement inherits from UI.TreeOutline.TreeElement.
  #treeOutline: LayerTreeOutline;
  layer: SDK.LayerTreeBase.Layer;

  constructor(tree: LayerTreeOutline, layer: SDK.LayerTreeBase.Layer) {
    super();
    this.#treeOutline = tree;
    this.layer = layer;
    layerToTreeElement.set(layer, this);
    this.update();
  }

  update(): void {
    const node = this.layer.nodeForSelfOrAncestor();
    const title = document.createDocumentFragment();
    UI.UIUtils.createTextChild(title, node ? node.simpleSelector() : '#' + this.layer.id());
    const details = title.createChild('span', 'dimmed');
    details.textContent =
        i18nString(UIStrings.updateChildDimension, {PH1: this.layer.width(), PH2: this.layer.height()});
    this.title = title;
  }

  override onselect(): boolean {
    this.#treeOutline.selectedNodeChanged(this);
    return false;
  }

  setHovered(hovered: boolean): void {
    this.listItemElement.classList.toggle('hovered', hovered);
  }
}

export const layerToTreeElement = new WeakMap<SDK.LayerTreeBase.Layer, LayerTreeElement>();
