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

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs",
});

/**
 * DateTimePickerChild is the communication channel between the input box
 * (content) for date/time input types and its picker (chrome).
 */
export class DateTimePickerChild extends JSWindowActorChild {
  /**
   * On init, just listen for the event to open the picker, once the picker is
   * opened, we'll listen for update and close events.
   */
  constructor() {
    super();

    this._inputElement = null;
  }

  /**
   * Cleanup function called when picker is closed.
   */
  close() {
    this.removeListeners(this._inputElement);
    let dateTimeBoxElement = this._inputElement.dateTimeBoxElement;
    if (!dateTimeBoxElement) {
      this._inputElement = null;
      return;
    }

    // dateTimeBoxElement is within UA Widget Shadow DOM.
    // An event dispatch to it can't be accessed by document.
    let win = this._inputElement.ownerGlobal;
    dateTimeBoxElement.dispatchEvent(
      new win.CustomEvent("MozSetDateTimePickerState", { detail: false })
    );

    this._inputElement = null;
  }

  /**
   * Called after picker is opened to start listening for input box update
   * events.
   */
  addListeners(aElement) {
    aElement.ownerGlobal.addEventListener("pagehide", this);
  }

  /**
   * Stop listeneing for events when picker is closed.
   */
  removeListeners(aElement) {
    aElement.ownerGlobal.removeEventListener("pagehide", this);
  }

  /**
   * Helper function that returns the CSS direction property of the element.
   */
  getComputedDirection(aElement) {
    return aElement.ownerGlobal
      .getComputedStyle(aElement)
      .getPropertyValue("direction");
  }

  /**
   * Helper function that returns the rect of the element, which is the position
   * relative to the left/top of the content area.
   */
  getBoundingContentRect(aElement) {
    return lazy.LayoutUtils.getElementBoundingScreenRect(aElement);
  }

  getTimePickerPref() {
    return Services.prefs.getBoolPref("dom.forms.datetime.timepicker");
  }

  /**
   * nsIMessageListener.
   */
  receiveMessage(aMessage) {
    switch (aMessage.name) {
      case "FormDateTime:PickerClosed": {
        if (!this._inputElement) {
          return;
        }

        this.close();
        break;
      }
      case "FormDateTime:PickerValueChanged": {
        if (!this._inputElement) {
          return;
        }

        let dateTimeBoxElement = this._inputElement.dateTimeBoxElement;
        if (!dateTimeBoxElement) {
          return;
        }

        let win = this._inputElement.ownerGlobal;

        // dateTimeBoxElement is within UA Widget Shadow DOM.
        // An event dispatch to it can't be accessed by document.
        dateTimeBoxElement.dispatchEvent(
          new win.CustomEvent("MozPickerValueChanged", {
            detail: Cu.cloneInto(aMessage.data, win),
          })
        );
        break;
      }
      default:
        break;
    }
  }

  /**
   * nsIDOMEventListener, for chrome events sent by the input element and other
   * DOM events.
   */
  handleEvent(aEvent) {
    switch (aEvent.type) {
      case "MozOpenDateTimePicker": {
        // Time picker is disabled when preffed off
        if (
          !aEvent.originalTarget.ownerGlobal.HTMLInputElement.isInstance(
            aEvent.originalTarget
          ) ||
          (aEvent.originalTarget.type == "time" && !this.getTimePickerPref())
        ) {
          return;
        }

        if (this._inputElement) {
          // This happens when we're trying to open a picker when another picker
          // is still open. We ignore this request to let the first picker
          // close gracefully.
          return;
        }

        this._inputElement = aEvent.originalTarget;

        let dateTimeBoxElement = this._inputElement.dateTimeBoxElement;
        if (!dateTimeBoxElement) {
          throw new Error("How do we get this event without a UA Widget?");
        }

        // dateTimeBoxElement is within UA Widget Shadow DOM.
        // An event dispatch to it can't be accessed by document, because
        // the event is not composed.
        let win = this._inputElement.ownerGlobal;
        dateTimeBoxElement.dispatchEvent(
          new win.CustomEvent("MozSetDateTimePickerState", { detail: true })
        );

        this.addListeners(this._inputElement);

        let value = this._inputElement.getDateTimeInputBoxValue();
        this.sendAsyncMessage("FormDateTime:OpenPicker", {
          rect: this.getBoundingContentRect(this._inputElement),
          dir: this.getComputedDirection(this._inputElement),
          type: this._inputElement.type,
          detail: {
            // Pass partial value if it's available, otherwise pass input
            // element's value.
            value: Object.keys(value).length ? value : this._inputElement.value,
            min: this._inputElement.getMinimum(),
            max: this._inputElement.getMaximum(),
            step: this._inputElement.getStep(),
            stepBase: this._inputElement.getStepBase(),
          },
        });
        break;
      }
      case "MozUpdateDateTimePicker": {
        let value = this._inputElement.getDateTimeInputBoxValue();
        value.type = this._inputElement.type;
        this.sendAsyncMessage("FormDateTime:UpdatePicker", { value });
        break;
      }
      case "MozCloseDateTimePicker": {
        this.sendAsyncMessage("FormDateTime:ClosePicker", {});
        this.close();
        break;
      }
      case "pagehide": {
        if (
          this._inputElement &&
          this._inputElement.ownerDocument == aEvent.target
        ) {
          this.sendAsyncMessage("FormDateTime:ClosePicker", {});
          this.close();
        }
        break;
      }
      default:
        break;
    }
  }
}
