// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable @devtools/no-imperative-dom-api */

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 type * as Protocol from '../../../../generated/protocol.js';
import {createIcon, type Icon} from '../../../kit/kit.js';
import * as UI from '../../legacy.js';

import customPreviewComponentStyles from './customPreviewComponent.css.js';
import {
  ObjectPropertiesMode,
  ObjectPropertiesSection,
  ObjectPropertiesSectionsTreeOutline,
  ObjectPropertyTreeElement,
  ObjectTree,
} from './ObjectPropertiesSection.js';

const UIStrings = {
  /**
   * @description A context menu item in the Custom Preview Component
   */
  showAsJavascriptObject: 'Show as JavaScript object',
} as const;
const str_ = i18n.i18n.registerUIStrings('ui/legacy/components/object_ui/CustomPreviewComponent.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export class CustomPreviewSection {
  private readonly sectionElement: HTMLSpanElement;
  private readonly object: SDK.RemoteObject.RemoteObject;
  private expanded: boolean;
  private cachedContent: Node|null;
  private readonly header: Node|undefined;
  private readonly expandIcon: Icon|undefined;
  constructor(object: SDK.RemoteObject.RemoteObject) {
    this.sectionElement = document.createElement('span');
    this.sectionElement.classList.add('custom-expandable-section');
    this.object = object;
    this.expanded = false;
    this.cachedContent = null;
    const customPreview = object.customPreview();

    if (!customPreview) {
      return;
    }

    let headerJSON;
    try {
      headerJSON = JSON.parse(customPreview.header);
    } catch (e) {
      Common.Console.Console.instance().error('Broken formatter: header is invalid json ' + e);
      return;
    }
    this.header = this.renderJSONMLTag(headerJSON);
    if (this.header.nodeType === Node.TEXT_NODE) {
      Common.Console.Console.instance().error('Broken formatter: header should be an element node.');
      return;
    }

    if (customPreview.bodyGetterId) {
      if (this.header instanceof Element) {
        this.header.classList.add('custom-expandable-section-header');
      }
      this.header.addEventListener('click', this.onClick.bind(this), false);
      this.expandIcon = createIcon('triangle-right', 'custom-expand-icon');
      this.header.insertBefore(this.expandIcon, this.header.firstChild);
    }

    this.sectionElement.appendChild(this.header);
  }

  element(): Element {
    return this.sectionElement;
  }

  private renderJSONMLTag(jsonML: unknown): Node {
    if (!Array.isArray(jsonML)) {
      return document.createTextNode(String(jsonML));
    }

    return jsonML[0] === 'object' ? this.layoutObjectTag(jsonML) : this.renderElement(jsonML);
  }

  // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private renderElement(object: any[]): Node {
    const tagName = object.shift();
    if (!ALLOWED_TAGS.includes(tagName)) {
      Common.Console.Console.instance().error('Broken formatter: element ' + tagName + ' is not allowed!');
      return document.createElement('span');
    }
    const element = document.createElement((tagName as string));
    if ((typeof object[0] === 'object') && !Array.isArray(object[0])) {
      const attributes = object.shift();
      for (const key in attributes) {
        const value = attributes[key];
        if ((key !== 'style') || (typeof value !== 'string')) {
          continue;
        }

        element.setAttribute(key, value);
      }
    }

    this.appendJsonMLTags(element, object);
    return element;
  }

  private layoutObjectTag(objectTag: unknown[]): Node {
    objectTag.shift();
    const attributes = objectTag.shift();
    const remoteObject = this.object.runtimeModel().createRemoteObject((attributes as Protocol.Runtime.RemoteObject));
    if (remoteObject.customPreview()) {
      return (new CustomPreviewSection(remoteObject)).element();
    }

    const sectionElement = ObjectPropertiesSection.defaultObjectPresentation(remoteObject);
    sectionElement.classList.toggle('custom-expandable-section-standard-section', remoteObject.hasChildren);
    return sectionElement;
  }

  private appendJsonMLTags(parentElement: Node, jsonMLTags: unknown[]): void {
    for (let i = 0; i < jsonMLTags.length; ++i) {
      parentElement.appendChild(this.renderJSONMLTag(jsonMLTags[i]));
    }
  }

  private onClick(event: Event): void {
    event.consume(true);
    if (this.cachedContent) {
      this.toggleExpand();
    } else {
      void this.loadBody();
    }
  }

  private toggleExpand(): void {
    this.expanded = !this.expanded;
    if (this.header instanceof Element) {
      this.header.classList.toggle('expanded', this.expanded);
    }
    if (this.cachedContent instanceof Element) {
      this.cachedContent.classList.toggle('hidden', !this.expanded);
    }
    if (this.expandIcon) {
      if (this.expanded) {
        this.expandIcon.name = 'triangle-down';
      } else {
        this.expandIcon.name = 'triangle-right';
      }
    }
  }
  private defaultBodyTreeOutline: ObjectPropertiesSectionsTreeOutline|undefined;

  async loadBody(): Promise<void> {
    const customPreview = this.object.customPreview();

    if (!customPreview) {
      return;
    }

    if (customPreview.bodyGetterId) {
      const bodyJsonML =
          await this.object.callFunctionJSON(bodyGetter => bodyGetter(), [{objectId: customPreview.bodyGetterId}]);
      if (bodyJsonML === null) {
        // Per https://firefox-source-docs.mozilla.org/devtools-user/custom_formatters/index.html#custom-formatter-structure
        // we are supposed to fall back to the default format when the `body()` callback returns `null`.
        this.defaultBodyTreeOutline = new ObjectPropertiesSectionsTreeOutline();
        this.defaultBodyTreeOutline.setShowSelectionOnKeyboardFocus(/* show */ true, /* preventTabOrder */ false);
        this.defaultBodyTreeOutline.element.classList.add('custom-expandable-section-default-body');
        void ObjectPropertyTreeElement.populate(
            this.defaultBodyTreeOutline.rootElement(), new ObjectTree(this.object, {
              readOnly: true,
              propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED,
            }),
            false, false);

        this.cachedContent = this.defaultBodyTreeOutline.element;
      } else {
        this.cachedContent = this.renderJSONMLTag(bodyJsonML);
      }

      this.sectionElement.appendChild(this.cachedContent);
      this.toggleExpand();
    }
  }
}

const ALLOWED_TAGS = ['span', 'div', 'ol', 'li', 'table', 'tr', 'td'];

export class CustomPreviewComponent {
  private readonly object: SDK.RemoteObject.RemoteObject;
  private customPreviewSection: CustomPreviewSection|null;
  element: HTMLSpanElement;
  constructor(object: SDK.RemoteObject.RemoteObject) {
    this.object = object;
    this.customPreviewSection = new CustomPreviewSection(object);
    this.element = document.createElement('span');
    this.element.classList.add('source-code');
    const shadowRoot = UI.UIUtils.createShadowRootWithCoreStyles(this.element, {cssFile: customPreviewComponentStyles});
    this.element.addEventListener('contextmenu', this.contextMenuEventFired.bind(this), false);
    shadowRoot.appendChild(this.customPreviewSection.element());
  }

  expandIfPossible(): void {
    const customPreview = this.object.customPreview();
    if (customPreview && customPreview.bodyGetterId && this.customPreviewSection) {
      void this.customPreviewSection.loadBody();
    }
  }

  private contextMenuEventFired(event: Event): void {
    const contextMenu = new UI.ContextMenu.ContextMenu(event);
    if (this.customPreviewSection) {
      contextMenu.revealSection().appendItem(
          i18nString(UIStrings.showAsJavascriptObject), this.disassemble.bind(this),
          {jslogContext: 'show-as-javascript-object'});
    }
    contextMenu.appendApplicableItems(this.object);
    void contextMenu.show();
  }

  private disassemble(): void {
    if (this.element.shadowRoot) {
      this.element.shadowRoot.textContent = '';
      this.customPreviewSection = null;
      this.element.shadowRoot.appendChild(ObjectPropertiesSection.defaultObjectPresentation(this.object));
    }
  }
}
