// Copyright 2025 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 SDK from '../../core/sdk/sdk.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as CodeHighlighter from '../../ui/components/code_highlighter/code_highlighter.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import {PanelUtils} from '../utils/utils.js';

import type {EditorHandles} from './ElementsTreeElement.js';

export class AdoptedStyleSheetSetTreeElement extends UI.TreeOutline.TreeElement {
  constructor(readonly adoptedStyleSheets: SDK.DOMModel.AdoptedStyleSheet[]) {
    super('');
    const documentElement = this.listItemElement.createChild('span');
    UI.UIUtils.createTextChild(documentElement, '#adopted-style-sheets');
    for (const adoptedStyleSheet of adoptedStyleSheets) {
      this.appendChild(new AdoptedStyleSheetTreeElement(adoptedStyleSheet));
    }
  }
}

export class AdoptedStyleSheetTreeElement extends UI.TreeOutline.TreeElement {
  private eventListener: Common.EventTarget.EventDescriptor|null = null;

  constructor(readonly adoptedStyleSheet: SDK.DOMModel.AdoptedStyleSheet) {
    super('');
    const header = adoptedStyleSheet.cssModel.styleSheetHeaderForId(adoptedStyleSheet.id);
    if (header) {
      AdoptedStyleSheetTreeElement.createContents(header, this);
    } else {
      this.eventListener = adoptedStyleSheet.cssModel.addEventListener(
          SDK.CSSModel.Events.StyleSheetAdded, this.onStyleSheetAdded, this);
    }
  }

  onStyleSheetAdded({data: header}: Common.EventTarget.EventTargetEvent<SDK.CSSStyleSheetHeader.CSSStyleSheetHeader>):
      void {
    if (header.id === this.adoptedStyleSheet.id) {
      AdoptedStyleSheetTreeElement.createContents(header, this);
      this.adoptedStyleSheet.cssModel.removeEventListener(
          SDK.CSSModel.Events.StyleSheetAdded, this.onStyleSheetAdded, this);
      this.eventListener = null;
    }
  }

  static createContents(header: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader, treeElement: UI.TreeOutline.TreeElement):
      void {
    const documentElement = treeElement.listItemElement.createChild('span');
    const linkText = header.sourceURL;
    UI.UIUtils.createTextChild(documentElement, '#adopted-style-sheet' + (linkText ? ' (' : ''));
    if (linkText) {
      documentElement.appendChild(Components.Linkifier.Linkifier.linkifyURL(linkText, {
        text: linkText,
        preventClick: true,
        showColumnNumber: false,
        inlineFrameIndex: 0,
      }));
      UI.UIUtils.createTextChild(documentElement, ')');
    }
    treeElement.appendChild(new AdoptedStyleSheetContentsTreeElement(header));
  }

  highlight(): void {
    PanelUtils.highlightElement(this.listItemElement);
  }
}

export class AdoptedStyleSheetContentsTreeElement extends UI.TreeOutline.TreeElement {
  private editing: EditorHandles|null = null;

  constructor(private readonly styleSheetHeader: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader) {
    super('');
  }

  override onbind(): void {
    this.styleSheetHeader.cssModel().addEventListener(
        SDK.CSSModel.Events.StyleSheetChanged, this.onStyleSheetChanged, this);
    void this.onpopulate();
  }

  override onunbind(): void {
    if (this.editing) {
      this.editing.cancel();
    }
    this.styleSheetHeader.cssModel().removeEventListener(
        SDK.CSSModel.Events.StyleSheetChanged, this.onStyleSheetChanged, this);
  }

  override async onpopulate(): Promise<void> {
    const data = await this.styleSheetHeader.requestContentData();
    if (!TextUtils.ContentData.ContentData.isError(data) && data.isTextContent) {
      this.listItemElement.removeChildren();
      const newNode = this.listItemElement.createChild('span', 'webkit-html-text-node webkit-html-css-node');
      newNode.setAttribute('jslog', `${VisualLogging.value('css-text-node').track({change: true, dblclick: true})}`);
      const text = data.text;
      newNode.textContent = text.replace(/^[\n\r]+|\s+$/g, '');
      void CodeHighlighter.CodeHighlighter.highlightNode(newNode, 'text/css');
    }
  }

  onStyleSheetChanged({data: {styleSheetId}}: Common.EventTarget.EventTargetEvent<SDK.CSSModel.StyleSheetChangedEvent>):
      void {
    if (styleSheetId === this.styleSheetHeader.id) {
      void this.onpopulate();
    }
  }

  override ondblclick(event: Event): boolean {
    if (this.editing) {
      return false;
    }
    void this.startEditing(event.target as Element);
    return false;
  }

  override onenter(): boolean {
    if (this.editing) {
      return false;
    }
    const target = this.listItemElement.querySelector('.webkit-html-text-node');
    if (target) {
      void this.startEditing(target);
      return true;
    }
    return false;
  }

  private async startEditing(target: Element): Promise<void> {
    if (this.editing || UI.UIUtils.isBeingEdited(target)) {
      return;
    }

    const textNode = target.enclosingNodeOrSelfWithClass('webkit-html-text-node');
    if (!textNode) {
      return;
    }

    const data = await this.styleSheetHeader.requestContentData();
    textNode.textContent = (TextUtils.ContentData.ContentData.isError(data) || !data.isTextContent) ? '' : data.text;

    const config =
        new UI.InplaceEditor.Config(this.editingCommitted.bind(this), () => this.editingCancelled(), undefined);

    const editorHandles = UI.InplaceEditor.InplaceEditor.startEditing(textNode, config);
    if (!editorHandles) {
      return;
    }

    this.editing = {
      commit: editorHandles.commit,
      cancel: editorHandles.cancel,
      editor: undefined,
      resize: () => {},
    };

    const componentSelection = this.listItemElement.getComponentSelection();
    componentSelection?.selectAllChildren(textNode);
  }

  private async editingCommitted(element: Element, newText: string, oldText: string|null): Promise<void> {
    this.editing = null;

    if (newText !== oldText) {
      await this.styleSheetHeader.cssModel().setStyleSheetText(this.styleSheetHeader.id, newText, false);
    }

    this.editingCancelled();
  }

  private editingCancelled(): void {
    this.editing = null;
    void this.onpopulate();
  }

  isEditing(): boolean {
    return this.editing !== null;
  }
}
