/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, you can obtain one at http://mozilla.org/MPL/2.0/. */

import CUSTOMIZABLE_ITEMS from "resource:///modules/CustomizableItemsDetails.mjs";

const { EXTENSION_PREFIX } = ChromeUtils.importESModule(
  "resource:///modules/CustomizableItems.sys.mjs"
);

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  ExtensionCommon: "resource://gre/modules/ExtensionCommon.sys.mjs",
  ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
});
const browserActionFor = extensionId =>
  lazy.ExtensionParent.apiManager.global.browserActionFor?.(
    lazy.ExtensionParent.GlobalManager.getExtension(extensionId)
  );

/**
 * Wrapper element for elements whose position can be customized.
 *
 * Template ID: #unifiedToolbarCustomizableElementTemplate
 * Attributes:
 * - item-id: ID of the customizable item this represents. Not observed.
 * - disabled: Gets passed on to the live content.
 */
export default class CustomizableElement extends HTMLLIElement {
  static get observedAttributes() {
    return ["disabled", "tabindex"];
  }

  connectedCallback() {
    if (this.hasConnected) {
      return;
    }
    this.hasConnected = true;

    this.setAttribute("is", "customizable-element");

    const template = document
      .getElementById("unifiedToolbarCustomizableElementTemplate")
      .content.cloneNode(true);

    const itemId = this.getAttribute("item-id");

    if (itemId.startsWith(EXTENSION_PREFIX)) {
      const extensionId = itemId.slice(EXTENSION_PREFIX.length);
      this.append(template);
      this.#initializeForExtension(extensionId);
      return;
    }

    const details = CUSTOMIZABLE_ITEMS.find(item => item.id === itemId);
    if (!details) {
      throw new Error(`Could not find definition for ${itemId}`);
    }
    this.append(template);
    this.#initializeFromDetails(details).catch(console.error);
  }

  attributeChangedCallback(attribute) {
    switch (attribute) {
      case "disabled": {
        const isDisabled = this.disabled;
        for (const child of this.querySelector(".live-content")?.children ??
          []) {
          child.toggleAttribute("disabled", isDisabled);
        }
        break;
      }
      case "tabindex": {
        const tabIndex = this.getAttribute("tabindex");
        if (tabIndex === null) {
          return;
        }
        if (this.details?.skipFocus && tabIndex !== "-1") {
          this.removeAttribute("tabindex");
          // Let the container know that an element that shouldn't be focused is
          // currently marked with a tabindex instruction.
          if (this.hasConnected) {
            this.dispatchEvent(new CustomEvent("buttondisabled"));
          }
          return;
        }
        const tabIndexNumber = parseInt(tabIndex, 10);
        for (const child of this.querySelector(".live-content")?.children ??
          []) {
          child.tabIndex = tabIndexNumber;
        }
        if (tabIndex !== "-1") {
          this.removeAttribute("tabindex");
        }
        break;
      }
    }
  }

  /**
   * Initialize the template contents from item details. Can't operate on the
   * template directly due to being async.
   *
   * @param {CustomizableItemDetails} itemDetails
   */
  async #initializeFromDetails(itemDetails) {
    if (this.details) {
      return;
    }
    this.details = itemDetails;
    this.classList.add(itemDetails.id);
    if (Array.isArray(itemDetails.requiredModules)) {
      await Promise.all(
        itemDetails.requiredModules.map(module => {
          return import(module); // eslint-disable-line no-unsanitized/method
        })
      );
    }
    if (itemDetails.templateId) {
      const contentTemplate = document.getElementById(itemDetails.templateId);
      this.querySelector(".live-content").append(
        contentTemplate.content.cloneNode(true)
      );
      if (this.disabled) {
        this.attributeChangedCallback("disabled");
      }
    }
    if (itemDetails.skipFocus) {
      this.classList.add("skip-focus");
    }
    if (this.hasAttribute("tabindex")) {
      this.attributeChangedCallback("tabindex");
    }
    // We need to manually re-emit this event, since it might've been emitted
    // after we cloned the template.
    if (this.querySelector(".live-content button[disabled]")) {
      this.dispatchEvent(new CustomEvent("buttondisabled"));
    }
    document.l10n.setAttributes(
      this.querySelector(".preview-label"),
      `${itemDetails.labelId}-label`
    );
  }

  /**
   * Initialize the contents of this customizable element for a button from an
   * extension.
   *
   * @param {string} extensionId - ID of the extension the button is from.
   */
  async #initializeForExtension(extensionId) {
    const extensionAction = browserActionFor(extensionId);
    if (!extensionAction?.extension) {
      return;
    }
    this.details = {
      allowMultiple: false,
      spaces: extensionAction.allowedSpaces ?? ["mail"],
    };
    if (!customElements.get("extension-action-button")) {
      await import("./extension-action-button.mjs");
    }
    const { extension } = extensionAction;
    this.classList.add("extension-action");
    const extensionButton = document.createElement("button", {
      is: "extension-action-button",
    });
    extensionButton.setAttribute("extension", extensionId);
    extensionButton.dataset.telemetryId = `${lazy.ExtensionCommon.makeWidgetId(
      extensionId
    )}-browserAction-toolbarbutton`;
    this.querySelector(".live-content").append(extensionButton);
    if (this.disabled) {
      this.attributeChangedCallback("disabled");
    }
    if (this.hasAttribute("tabindex")) {
      this.attributeChangedCallback("tabindex");
    }
    // We need to manually re-emit this event, since it might've been emitted
    // before the button was attached to the DOM.
    if (this.querySelector(".live-content button[disabled]")) {
      this.dispatchEvent(new CustomEvent("buttondisabled"));
    }
    const previewLabel = this.querySelector(".preview-label");
    const labelText = extension.name || extensionId;
    previewLabel.textContent = labelText;
    previewLabel.title = labelText;
    const { IconDetails } = lazy.ExtensionParent;
    if (extension.manifest.icons) {
      let { icon } = IconDetails.getPreferredIcon(
        extension.manifest.icons,
        extension,
        16
      );
      let { icon: icon2x } = IconDetails.getPreferredIcon(
        extension.manifest.icons,
        extension,
        32
      );
      this.style.setProperty(
        "--webextension-icon",
        `url("${lazy.ExtensionParent.IconDetails.escapeUrl(icon)}")`
      );
      this.style.setProperty(
        "--webextension-icon-2x",
        `url("${lazy.ExtensionParent.IconDetails.escapeUrl(icon2x)}")`
      );
    }
  }

  /**
   * Holds a reference to the palette this element belongs to.
   *
   * @type {CustomizationPalette}
   */
  get palette() {
    const paletteClass = this.details.spaces?.length
      ? "space-specific-palette"
      : "generic-palette";
    return this.getRootNode().querySelector(`.${paletteClass}`);
  }

  /**
   * If multiple instances of this element are allowed in the same space.
   *
   * @type {boolean}
   */
  get allowMultiple() {
    return Boolean(this.details?.allowMultiple);
  }

  /**
   * Human readable label for the widget.
   *
   * @type {string}
   */
  get label() {
    return this.querySelector(".preview-label").textContent;
  }

  /**
   * Calls onTabSwitched on the first button contained in the live content.
   * No-op if this item is disabled. Called by unified-toolbar's tab monitor.
   *
   * @param {TabInfo} tab - Tab that is now selected.
   * @param {TabInfo} oldTab - Tab that was selected before.
   */
  onTabSwitched(tab, oldTab) {
    if (this.disabled) {
      return;
    }
    this.querySelector(".live-content button")?.onTabSwitched?.(tab, oldTab);
  }

  /**
   * Calls onTabClosing on the first button contained in the live content.
   * No-op if this item is disabled. Called by unified-toolbar's tab monitor.
   *
   * @param {TabInfo} tab - Tab that was closed.
   */
  onTabClosing(tab) {
    if (this.disabled) {
      return;
    }
    this.querySelector(".live-content button")?.onTabClosing?.(tab);
  }

  /**
   * If this item can be added to all spaces.
   *
   * @type {boolean}
   */
  get allSpaces() {
    return !this.details.spaces?.length;
  }

  /**
   * If this item wants to provide its own context menu.
   *
   * @type {boolean}
   */
  get hasContextMenu() {
    return Boolean(this.details?.hasContextMenu);
  }

  /**
   * @type {boolean}
   */
  get disabled() {
    return this.hasAttribute("disabled");
  }

  set disabled(value) {
    this.toggleAttribute("disabled", value);
  }

  focus() {
    this.querySelector(".live-content *:first-child")?.focus();
  }
}
customElements.define("customizable-element", CustomizableElement, {
  extends: "li",
});
