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

import type * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as TreeOutline from '../../ui/components/tree_outline/tree_outline.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Lit from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';

import {ElementsPanel} from './ElementsPanel.js';
import layersWidgetStyles from './layersWidget.css.js';

const {render, html, Directives: {ref}} = Lit;

const UIStrings = {
  /**
   * @description Title of a section in the Element State Pane Widget of the Elements panel.
   * The widget shows the layers present in the context of the currently selected node.
   * */
  cssLayersTitle: 'CSS layers',
  /**
   * @description Tooltip text in Element State Pane Widget of the Elements panel.
   * For a button that opens a tool that shows the layers present in the current document.
   */
  toggleCSSLayers: 'Toggle CSS Layers view',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/elements/LayersWidget.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

interface ViewInput {
  rootLayer: Protocol.CSS.CSSLayerData;
}

interface ViewOutput {
  treeOutline: TreeOutline.TreeOutline.TreeOutline<string>|undefined;
}

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

const DEFAULT_VIEW: View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => {
  const makeTreeNode = (parentId: string) => (layer: Protocol.CSS.CSSLayerData) => {
    const subLayers = layer.subLayers;
    const name = SDK.CSSModel.CSSModel.readableLayerName(layer.name);
    const treeNodeData = layer.order + ': ' + name;
    const id = parentId ? parentId + '.' + name : name;
    if (!subLayers) {
      return {treeNodeData, id};
    }
    return {
      treeNodeData,
      id,
      children: async () => subLayers.sort((layer1, layer2) => layer1.order - layer2.order).map(makeTreeNode(id)),
    };
  };
  const {defaultRenderer} = TreeOutline.TreeOutline;
  const tree = [makeTreeNode('')(input.rootLayer)];
  const data: TreeOutline.TreeOutline.TreeOutlineData<string> = {
    defaultRenderer,
    tree,
  };
  const captureTreeOutline = (e?: Element): void => {
    output.treeOutline = e as typeof output.treeOutline;
  };
  const template = html`
  <style>${layersWidgetStyles}</style>
  <div class="layers-widget">
    <div class="layers-widget-title">${UIStrings.cssLayersTitle}</div>
    <devtools-tree-outline ${ref(captureTreeOutline)}
                           .data=${data}></devtools-tree-outline>
  </div>
  `;
  render(template, target);
};

let layersWidgetInstance: LayersWidget;

export class LayersWidget extends UI.Widget.Widget {
  #node: SDK.DOMModel.DOMNode|null = null;
  #view: View;
  #layerToReveal: string|null = null;

  constructor(view: View = DEFAULT_VIEW) {
    super({jslog: `${VisualLogging.pane('css-layers')}`});
    this.#view = view;
  }

  override wasShown(): void {
    super.wasShown();
    UI.Context.Context.instance().addFlavorChangeListener(SDK.DOMModel.DOMNode, this.#onDOMNodeChanged, this);
    this.#onDOMNodeChanged({data: UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode)});
  }

  override wasHidden(): void {
    UI.Context.Context.instance().addFlavorChangeListener(SDK.DOMModel.DOMNode, this.#onDOMNodeChanged, this);
    this.#onDOMNodeChanged({data: null});
    super.wasHidden();
  }

  #onDOMNodeChanged(event: Common.EventTarget.EventTargetEvent<SDK.DOMModel.DOMNode|null>): void {
    const node = event.data?.enclosingElementOrSelf();
    if (this.#node === node) {
      return;
    }
    if (this.#node) {
      this.#node.domModel().cssModel().removeEventListener(
          SDK.CSSModel.Events.StyleSheetChanged, this.requestUpdate, this);
    }
    this.#node = event.data;
    if (this.#node) {
      this.#node.domModel().cssModel().addEventListener(
          SDK.CSSModel.Events.StyleSheetChanged, this.requestUpdate, this);
    }
    if (this.isShowing()) {
      this.requestUpdate();
    }
  }

  override async performUpdate(): Promise<void> {
    if (!this.#node) {
      return;
    }

    const rootLayer = await this.#node.domModel().cssModel().getRootLayer(this.#node.id);
    const input = {rootLayer};
    const output: ViewOutput = {treeOutline: undefined};
    this.#view(input, output, this.contentElement);

    if (output.treeOutline) {
      // We only expand the first 5 user-defined layers to not make the
      // view too overwhelming.
      await output.treeOutline.expandRecursively(5);
      if (this.#layerToReveal) {
        await output.treeOutline.expandToAndSelectTreeNodeId(this.#layerToReveal);
        this.#layerToReveal = null;
      }
    }
  }

  async revealLayer(layerName: string): Promise<void> {
    if (!this.isShowing()) {
      ElementsPanel.instance().showToolbarPane(this, ButtonProvider.instance().item());
    }
    this.#layerToReveal = `implicit outer layer.${layerName}`;
    this.requestUpdate();
    await this.updateComplete;
  }

  static instance(opts: {
    forceNew: boolean|null,
  }|undefined = {forceNew: null}): LayersWidget {
    const {forceNew} = opts;
    if (!layersWidgetInstance || forceNew) {
      layersWidgetInstance = new LayersWidget();
    }

    return layersWidgetInstance;
  }
}

let buttonProviderInstance: ButtonProvider;

export class ButtonProvider implements UI.Toolbar.Provider {
  private readonly button: UI.Toolbar.ToolbarToggle;
  private constructor() {
    this.button = new UI.Toolbar.ToolbarToggle(i18nString(UIStrings.toggleCSSLayers), 'layers', 'layers-filled');
    this.button.setVisible(false);
    this.button.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, this.clicked, this);
    this.button.element.classList.add('monospace');
    this.button.element.setAttribute('jslog', `${VisualLogging.toggleSubpane('css-layers').track({click: true})}`);
  }

  static instance(opts: {
    forceNew: boolean|null,
  } = {forceNew: null}): ButtonProvider {
    const {forceNew} = opts;
    if (!buttonProviderInstance || forceNew) {
      buttonProviderInstance = new ButtonProvider();
    }

    return buttonProviderInstance;
  }

  private clicked(): void {
    const view = LayersWidget.instance();
    ElementsPanel.instance().showToolbarPane(!view.isShowing() ? view : null, this.button);
  }

  item(): UI.Toolbar.ToolbarToggle {
    return this.button;
  }
}
