/* 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.
(() => {
  class MozRadiogroup extends MozElements.BaseControl {
    constructor() {
      super();

      this.addEventListener("mousedown", event => {
        if (this.disabled) {
          event.preventDefault();
        }
      });

      /**
       * keyboard navigation  Here's how keyboard navigation works in radio groups on Windows:
       * The group takes 'focus'
       * The user is then free to navigate around inside the group
       * using the arrow keys. Accessing previous or following radio buttons
       * is done solely through the arrow keys and not the tab button. Tab
       * takes you to the next widget in the tab order
       */
      this.addEventListener("keypress", event => {
        if (event.key != " " || event.originalTarget != this) {
          return;
        }
        this.selectedItem = this.focusedItem;
        this.selectedItem.doCommand();
        // Prevent page from scrolling on the space key.
        event.preventDefault();
      });

      this.addEventListener("keypress", event => {
        if (
          event.keyCode != KeyEvent.DOM_VK_UP ||
          event.originalTarget != this
        ) {
          return;
        }
        this.checkAdjacentElement(false);
        event.stopPropagation();
        event.preventDefault();
      });

      this.addEventListener("keypress", event => {
        if (
          event.keyCode != KeyEvent.DOM_VK_LEFT ||
          event.originalTarget != this
        ) {
          return;
        }
        // left arrow goes back when we are ltr, forward when we are rtl
        this.checkAdjacentElement(
          document.defaultView.getComputedStyle(this).direction == "rtl"
        );
        event.stopPropagation();
        event.preventDefault();
      });

      this.addEventListener("keypress", event => {
        if (
          event.keyCode != KeyEvent.DOM_VK_DOWN ||
          event.originalTarget != this
        ) {
          return;
        }
        this.checkAdjacentElement(true);
        event.stopPropagation();
        event.preventDefault();
      });

      this.addEventListener("keypress", event => {
        if (
          event.keyCode != KeyEvent.DOM_VK_RIGHT ||
          event.originalTarget != this
        ) {
          return;
        }
        // right arrow goes forward when we are ltr, back when we are rtl
        this.checkAdjacentElement(
          document.defaultView.getComputedStyle(this).direction == "ltr"
        );
        event.stopPropagation();
        event.preventDefault();
      });

      /**
       * set a focused attribute on the selected item when the group
       * receives focus so that we can style it as if it were focused even though
       * it is not (Windows platform behaviour is for the group to receive focus,
       * not the item
       */
      this.addEventListener("focus", event => {
        if (event.originalTarget != this) {
          return;
        }
        this.setAttribute("focused", "true");
        if (this.focusedItem) {
          return;
        }

        var val = this.selectedItem;
        if (!val || val.disabled || val.hidden || val.collapsed) {
          var children = this._getRadioChildren();
          for (var i = 0; i < children.length; ++i) {
            if (
              !children[i].hidden &&
              !children[i].collapsed &&
              !children[i].disabled
            ) {
              val = children[i];
              break;
            }
          }
        }
        this.focusedItem = val;
      });

      this.addEventListener("blur", event => {
        if (event.originalTarget != this) {
          return;
        }
        this.removeAttribute("focused");
        this.focusedItem = null;
      });
    }

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

      // When this is called via `connectedCallback` there are two main variations:
      //   1) The radiogroup and radio children are defined in markup.
      //   2) We are appending a DocumentFragment
      // In both cases, the <radiogroup> connectedCallback fires first. But in (2),
      // the children <radio>s won't be upgraded yet, so r.control will be undefined.
      // To avoid churn in this case where we would have to reinitialize the list as each
      // child radio gets upgraded as a result of init(), ignore the resulting calls
      // to radioAttached.
      this.ignoreRadioChildConstruction = true;
      this.init();
      this.ignoreRadioChildConstruction = false;
      if (!this.value) {
        this.selectedIndex = 0;
      }
    }

    init() {
      this._radioChildren = null;

      if (this.getAttribute("disabled") == "true") {
        this.disabled = true;
      }

      var children = this._getRadioChildren();
      var length = children.length;
      for (var i = 0; i < length; i++) {
        if (children[i].getAttribute("selected") == "true") {
          this.selectedIndex = i;
          return;
        }
      }

      var value = this.value;
      if (value) {
        this.value = value;
      }
    }

    /**
     * Called when a new <radio> gets added to an already connected radiogroup.
     * This can happen due to DOM getting appended after the <radiogroup> is created.
     * When this happens, reinitialize the UI if necessary to make sure the state is
     * consistent.
     *
     * @param {DOMNode} child
     *                  The <radio> element that got added
     */
    radioAttached(child) {
      if (this.ignoreRadioChildConstruction) {
        return;
      }
      if (!this._radioChildren || !this._radioChildren.includes(child)) {
        this.init();
      }
    }

    /**
     * Called when a new <radio> gets removed from a radio group.
     *
     * @param {DOMNode} child
     *                  The <radio> element that got removed
     */
    radioUnattached(child) {
      // Just invalidate the cache, next time it's fetched it'll get rebuilt.
      this._radioChildren = null;
    }

    set value(val) {
      this.setAttribute("value", val);
      var children = this._getRadioChildren();
      for (var i = 0; i < children.length; i++) {
        if (String(children[i].value) == String(val)) {
          this.selectedItem = children[i];
          break;
        }
      }
    }

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

    set disabled(val) {
      if (val) {
        this.setAttribute("disabled", "true");
      } else {
        this.removeAttribute("disabled");
      }
      var children = this._getRadioChildren();
      for (var i = 0; i < children.length; ++i) {
        children[i].disabled = val;
      }
    }

    get disabled() {
      if (this.getAttribute("disabled") == "true") {
        return true;
      }
      var children = this._getRadioChildren();
      for (var i = 0; i < children.length; ++i) {
        if (
          !children[i].hidden &&
          !children[i].collapsed &&
          !children[i].disabled
        ) {
          return false;
        }
      }
      return true;
    }

    get itemCount() {
      return this._getRadioChildren().length;
    }

    set selectedIndex(val) {
      this.selectedItem = this._getRadioChildren()[val];
    }

    get selectedIndex() {
      var children = this._getRadioChildren();
      for (var i = 0; i < children.length; ++i) {
        if (children[i].selected) {
          return i;
        }
      }
      return -1;
    }

    set selectedItem(val) {
      var focused = this.getAttribute("focused") == "true";
      var alreadySelected = false;

      if (val) {
        alreadySelected = val.getAttribute("selected") == "true";
        val.setAttribute("focused", focused);
        val.setAttribute("selected", "true");
        this.setAttribute("value", val.value);
      } else {
        this.removeAttribute("value");
      }

      // uncheck all other group nodes
      var children = this._getRadioChildren();
      var previousItem = null;
      for (var i = 0; i < children.length; ++i) {
        if (children[i] != val) {
          if (children[i].getAttribute("selected") == "true") {
            previousItem = children[i];
          }

          children[i].removeAttribute("selected");
          children[i].removeAttribute("focused");
        }
      }

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

      if (focused) {
        if (alreadySelected) {
          // Notify accessibility that this item got focus.
          event = document.createEvent("Events");
          event.initEvent("DOMMenuItemActive", true, true);
          val.dispatchEvent(event);
        } else {
          // Only report if actual change
          if (val) {
            // Accessibility will fire focus for this.
            event = document.createEvent("Events");
            event.initEvent("RadioStateChange", true, true);
            val.dispatchEvent(event);
          }

          if (previousItem) {
            event = document.createEvent("Events");
            event.initEvent("RadioStateChange", true, true);
            previousItem.dispatchEvent(event);
          }
        }
      }
    }

    get selectedItem() {
      var children = this._getRadioChildren();
      for (var i = 0; i < children.length; ++i) {
        if (children[i].selected) {
          return children[i];
        }
      }
      return null;
    }

    set focusedItem(val) {
      if (val) {
        val.setAttribute("focused", "true");
        // Notify accessibility that this item got focus.
        let event = document.createEvent("Events");
        event.initEvent("DOMMenuItemActive", true, true);
        val.dispatchEvent(event);
      }

      // unfocus all other group nodes
      var children = this._getRadioChildren();
      for (var i = 0; i < children.length; ++i) {
        if (children[i] != val) {
          children[i].removeAttribute("focused");
        }
      }
    }

    get focusedItem() {
      var children = this._getRadioChildren();
      for (var i = 0; i < children.length; ++i) {
        if (children[i].getAttribute("focused") == "true") {
          return children[i];
        }
      }
      return null;
    }

    checkAdjacentElement(aNextFlag) {
      var currentElement = this.focusedItem || this.selectedItem;
      var i;
      var children = this._getRadioChildren();
      for (i = 0; i < children.length; ++i) {
        if (children[i] == currentElement) {
          break;
        }
      }
      var index = i;

      if (aNextFlag) {
        do {
          if (++i == children.length) {
            i = 0;
          }
          if (i == index) {
            break;
          }
        } while (
          children[i].hidden ||
          children[i].collapsed ||
          children[i].disabled
        );
        // XXX check for display/visibility props too

        this.selectedItem = children[i];
        children[i].doCommand();
      } else {
        do {
          if (i == 0) {
            i = children.length;
          }
          if (--i == index) {
            break;
          }
        } while (
          children[i].hidden ||
          children[i].collapsed ||
          children[i].disabled
        );
        // XXX check for display/visibility props too

        this.selectedItem = children[i];
        children[i].doCommand();
      }
    }

    _getRadioChildren() {
      if (this._radioChildren) {
        return this._radioChildren;
      }

      let radioChildren = [];
      if (this.hasChildNodes()) {
        for (let radio of this.querySelectorAll("radio")) {
          customElements.upgrade(radio);
          if (radio.control == this) {
            radioChildren.push(radio);
          }
        }
      } else {
        const XUL_NS =
          "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
        for (let radio of this.ownerDocument.getElementsByAttribute(
          "group",
          this.id
        )) {
          if (radio.namespaceURI == XUL_NS && radio.localName == "radio") {
            customElements.upgrade(radio);
            radioChildren.push(radio);
          }
        }
      }

      return (this._radioChildren = radioChildren);
    }

    getIndexOfItem(item) {
      return this._getRadioChildren().indexOf(item);
    }

    getItemAtIndex(index) {
      var children = this._getRadioChildren();
      return index >= 0 && index < children.length ? children[index] : null;
    }

    appendItem(label, value) {
      var radio = document.createXULElement("radio");
      radio.setAttribute("label", label);
      radio.setAttribute("value", value);
      this.appendChild(radio);
      return radio;
    }
  }

  MozXULElement.implementCustomInterface(MozRadiogroup, [
    Ci.nsIDOMXULSelectControlElement,
    Ci.nsIDOMXULRadioGroupElement,
  ]);

  customElements.define("radiogroup", MozRadiogroup);

  class MozRadio extends MozElements.BaseText {
    static get markup() {
      return `
      <image class="radio-check"></image>
      <hbox class="radio-label-box" align="center" flex="1">
        <image class="radio-icon"></image>
        <label class="radio-label" flex="1"></label>
      </hbox>
      `;
    }

    static get inheritedAttributes() {
      return {
        ".radio-check": "disabled,selected",
        ".radio-label": "text=label,accesskey,crop",
        ".radio-icon": "src",
      };
    }

    constructor() {
      super();
      this.addEventListener("click", event => {
        if (!this.disabled) {
          this.control.selectedItem = this;
        }
      });

      this.addEventListener("mousedown", event => {
        if (!this.disabled) {
          this.control.focusedItem = this;
        }
      });
    }

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

      if (!this.connectedOnce) {
        this.connectedOnce = true;
        // If the caller didn't provide custom content then append the default:
        if (!this.firstElementChild) {
          this.appendChild(this.constructor.fragment);
          this.initializeAttributeInheritance();
        }
      }

      var control = (this._control = this.control);
      if (control) {
        control.radioAttached(this);
      }
    }

    disconnectedCallback() {
      if (this.control) {
        this.control.radioUnattached(this);
      }
      this._control = null;
    }

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

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

    get selected() {
      return this.hasAttribute("selected");
    }

    get radioGroup() {
      return this.control;
    }

    get control() {
      if (this._control) {
        return this._control;
      }

      var radiogroup = this.closest("radiogroup");
      if (radiogroup) {
        return radiogroup;
      }

      var group = this.getAttribute("group");
      if (!group) {
        return null;
      }

      var parent = this.ownerDocument.getElementById(group);
      if (!parent || parent.localName != "radiogroup") {
        parent = null;
      }
      return parent;
    }
  }

  MozXULElement.implementCustomInterface(MozRadio, [
    Ci.nsIDOMXULSelectControlItemElement,
  ]);
  customElements.define("radio", MozRadio);
})();