/* 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/. */

"use strict";

// This is loaded into all XUL windows. Wrap in a block to prevent
// leaking to window scope.
{
  const { AppConstants } = ChromeUtils.importESModule(
    "resource://gre/modules/AppConstants.sys.mjs"
  );
  const MozXULMenuElement = MozElements.MozElementMixin(XULMenuElement);
  const MenuBaseControl = MozElements.BaseControlMixin(MozXULMenuElement);

  class MozMenuList extends MenuBaseControl {
    constructor() {
      super();

      this.addEventListener(
        "command",
        event => {
          if (event.target.parentNode.parentNode == this) {
            this.selectedItem = event.target;
          }
        },
        true
      );

      this.addEventListener("popupshowing", event => {
        if (event.target.parentNode == this) {
          this.activeChild = null;
          if (this.selectedItem) {
            // Not ready for auto-setting the active child in hierarchies yet.
            // For now, only do this when the outermost menupopup opens.
            this.activeChild = this.mSelectedInternal;
          }
        }
      });

      this.addEventListener(
        "keypress",
        event => {
          if (
            event.defaultPrevented ||
            event.altKey ||
            event.ctrlKey ||
            event.metaKey
          ) {
            return;
          }

          if (
            AppConstants.platform === "macosx" &&
            !this.open &&
            (event.keyCode == KeyEvent.DOM_VK_UP ||
              event.keyCode == KeyEvent.DOM_VK_DOWN)
          ) {
            // This should open the menulist on macOS, see
            // XULButtonElement::PostHandleEvent.
            return;
          }

          if (
            event.keyCode == KeyEvent.DOM_VK_UP ||
            event.keyCode == KeyEvent.DOM_VK_DOWN ||
            event.keyCode == KeyEvent.DOM_VK_PAGE_UP ||
            event.keyCode == KeyEvent.DOM_VK_PAGE_DOWN ||
            event.keyCode == KeyEvent.DOM_VK_HOME ||
            event.keyCode == KeyEvent.DOM_VK_END ||
            event.keyCode == KeyEvent.DOM_VK_BACK_SPACE ||
            event.charCode > 0
          ) {
            // Moving relative to an item: start from the currently selected item
            this.activeChild = this.mSelectedInternal;
            if (this.handleKeyPress(event)) {
              this.activeChild.doCommand();
              event.preventDefault();
            }
          }
        },
        { mozSystemGroup: true }
      );

      this.attachShadow({ mode: "open" });
    }

    static get inheritedAttributes() {
      return {
        image: "src=image",
        "#label": "value=label,crop,accesskey",
        "#highlightable-label": "text=label,crop,accesskey",
        dropmarker: "disabled,open",
      };
    }

    static get markup() {
      // Accessibility information of these nodes will be presented
      // on XULComboboxAccessible generated from <menulist>;
      // hide these nodes from the accessibility tree.
      return `
        <html:link href="chrome://global/skin/menulist.css" rel="stylesheet"/>
        <hbox id="label-box" part="label-box" flex="1" role="none">
          <image part="icon" role="none"/>
          <label id="label" part="label" crop="end" flex="1" role="none"/>
          <label id="highlightable-label" part="label" crop="end" flex="1" role="none"/>
        </hbox>
        <dropmarker part="dropmarker" type="menu" role="none"/>
        <html:slot/>
    `;
    }

    connectedCallback() {
      if (this.delayConnectedCallback()) {
        return;
      }

      // Append and track children so they can be removed in disconnectedCallback.
      if (!this.hasAttribute("popuponly")) {
        this.shadowRoot.appendChild(this.constructor.fragment);
      } else {
        this.shadowRoot.appendChild(document.createElement("slot"));
      }

      this._managedNodes = [];
      if (this.shadowRoot.children) {
        let childElements = Array.from(this.shadowRoot.children);
        childElements.forEach(child => {
          this._managedNodes.push(child);
        });
      }

      if (!this.hasAttribute("popuponly")) {
        this.initializeAttributeInheritance();
      }

      this.setInitialSelection();
    }

    // nsIDOMXULSelectControlElement
    set value(val) {
      // if the new value is null, we still need to remove the old value
      if (val == null) {
        this.selectedItem = val;
        return;
      }

      var arr = null;
      var popup = this.menupopup;
      if (popup) {
        arr = popup.getElementsByAttribute("value", val);
      }

      if (arr && arr.item(0)) {
        this.selectedItem = arr[0];
      } else {
        this.selectedItem = null;
        this.setAttribute("value", val);
      }
    }

    // nsIDOMXULSelectControlElement
    get value() {
      return this.getAttribute("value") || "";
    }

    // nsIDOMXULMenuListElement
    set image(val) {
      this.setAttribute("image", val);
    }

    // nsIDOMXULMenuListElement
    get image() {
      return this.getAttribute("image") || "";
    }

    // nsIDOMXULMenuListElement
    get label() {
      return this.getAttribute("label") || "";
    }

    set description(val) {
      this.setAttribute("description", val);
    }

    get description() {
      return this.getAttribute("description") || "";
    }

    // nsIDOMXULMenuListElement
    set open(val) {
      this.openMenu(val);
    }

    // nsIDOMXULMenuListElement
    get open() {
      return this.hasAttribute("open");
    }

    // nsIDOMXULSelectControlElement
    get itemCount() {
      return this.menupopup ? this.menupopup.children.length : 0;
    }

    get menupopup() {
      var popup = this.firstElementChild;
      while (popup && popup.localName != "menupopup") {
        popup = popup.nextElementSibling;
      }
      return popup;
    }

    // nsIDOMXULSelectControlElement
    set selectedIndex(val) {
      var popup = this.menupopup;
      if (popup && 0 <= val) {
        if (val < popup.children.length) {
          this.selectedItem = popup.children[val];
        }
      } else {
        this.selectedItem = null;
      }
    }

    // nsIDOMXULSelectControlElement
    get selectedIndex() {
      // Quick and dirty. We won't deal with hierarchical menulists yet.
      if (
        !this.selectedItem ||
        !this.mSelectedInternal.parentNode ||
        this.mSelectedInternal.parentNode.parentNode != this
      ) {
        return -1;
      }

      var children = this.mSelectedInternal.parentNode.children;
      var i = children.length;
      while (i--) {
        if (children[i] == this.mSelectedInternal) {
          break;
        }
      }

      return i;
    }

    // nsIDOMXULSelectControlElement
    set selectedItem(val) {
      var oldval = this.mSelectedInternal;
      if (oldval == val) {
        return;
      }

      if (val && !this.contains(val)) {
        return;
      }

      if (oldval) {
        oldval.removeAttribute("selected");
        this.mAttributeObserver.disconnect();
      }

      this.mSelectedInternal = val;
      let attributeFilter = ["value", "label", "image", "description"];
      if (val) {
        val.setAttribute("selected", "true");
        for (let attr of attributeFilter) {
          if (val.hasAttribute(attr)) {
            this.setAttribute(attr, val.getAttribute(attr));
          } else {
            this.removeAttribute(attr);
          }
        }

        this.mAttributeObserver = new MutationObserver(
          this.handleMutation.bind(this)
        );
        this.mAttributeObserver.observe(val, { attributeFilter });
      } else {
        for (let attr of attributeFilter) {
          this.removeAttribute(attr);
        }
      }

      var event = document.createEvent("Events");
      event.initEvent("select", true, true);
      this.dispatchEvent(event);

      event = document.createEvent("Events");
      event.initEvent("ValueChange", true, true);
      this.dispatchEvent(event);
    }

    // nsIDOMXULSelectControlElement
    get selectedItem() {
      return this.mSelectedInternal;
    }

    setInitialSelection() {
      if (this.getAttribute("noinitialselection") === "true") {
        return;
      }

      this.mSelectedInternal = null;
      this.mAttributeObserver = null;

      var popup = this.menupopup;
      if (popup) {
        var arr = popup.getElementsByAttribute("selected", "true");

        var editable = this.editable;
        var value = this.value;
        if (!arr.item(0) && value) {
          arr = popup.getElementsByAttribute(
            editable ? "label" : "value",
            value
          );
        }

        if (arr.item(0)) {
          this.selectedItem = arr[0];
        } else if (!editable) {
          this.selectedIndex = 0;
        }
      }
    }

    contains(item) {
      if (!item) {
        return false;
      }

      var parent = item.parentNode;
      return parent && parent.parentNode == this;
    }

    handleMutation(aRecords) {
      for (let record of aRecords) {
        let t = record.target;
        if (t == this.mSelectedInternal) {
          let attrName = record.attributeName;
          switch (attrName) {
            case "value":
            case "label":
            case "image":
            case "description":
              if (t.hasAttribute(attrName)) {
                this.setAttribute(attrName, t.getAttribute(attrName));
              } else {
                this.removeAttribute(attrName);
              }
          }
        }
      }
    }

    // nsIDOMXULSelectControlElement
    getIndexOfItem(item) {
      var popup = this.menupopup;
      if (popup) {
        var children = popup.children;
        var i = children.length;
        while (i--) {
          if (children[i] == item) {
            return i;
          }
        }
      }
      return -1;
    }

    // nsIDOMXULSelectControlElement
    getItemAtIndex(index) {
      var popup = this.menupopup;
      if (popup) {
        var children = popup.children;
        if (index >= 0 && index < children.length) {
          return children[index];
        }
      }
      return null;
    }

    appendItem(label, value, description) {
      if (!this.menupopup) {
        this.appendChild(MozXULElement.parseXULToFragment(`<menupopup />`));
      }

      var popup = this.menupopup;
      popup.appendChild(MozXULElement.parseXULToFragment(`<menuitem />`));

      var item = popup.lastElementChild;
      if (label !== undefined) {
        item.setAttribute("label", label);
      }
      item.setAttribute("value", value);
      if (description) {
        item.setAttribute("description", description);
      }

      return item;
    }

    removeAllItems() {
      this.selectedItem = null;
      var popup = this.menupopup;
      if (popup) {
        this.removeChild(popup);
      }
    }

    disconnectedCallback() {
      if (this.mAttributeObserver) {
        this.mAttributeObserver.disconnect();
      }

      if (this._managedNodes) {
        this._managedNodes.forEach(node => node.remove());
        this._managedNodes = null;
      }
    }
  }

  MenuBaseControl.implementCustomInterface(MozMenuList, [
    Ci.nsIDOMXULMenuListElement,
    Ci.nsIDOMXULSelectControlElement,
  ]);

  customElements.define("menulist", MozMenuList);
}
