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

/**
 * InputPickerChildCommon is the communication channel between the input box
 * (content) for each input types and its picker (chrome).
 */
export class InputPickerChildCommon extends JSWindowActorChild {
  /** @type {HTMLInputElement} */
  #inputElement = null;
  #inputType = "";
  #namespace;
  /** @type {AbortController} */
  #abortController;

  /**
   * On init, just listen for the event to open the picker, once the picker is
   * opened, we'll listen for update and close events.
   *
   * @param {string} namespace Affects the event names, e.g. Foo makes it
   *                           accept FooValueChanged event.
   *                           Align it with ActorManagerParent declaration.
   */
  constructor(namespace) {
    super();
    this.#namespace = namespace;
  }

  /**
   * Cleanup function called when picker is closed.
   */
  close() {
    this.#abortController.abort();
    this.closeImpl(this.#inputElement);
    this.#inputElement = null;
    this.#inputType = "";
  }

  /**
   * @param {HTMLInputElement} _inputElement
   */
  closeImpl(_inputElement) {
    throw new Error("Not implemented");
  }

  /**
   * Called after picker is opened to start listening for input box update
   * events.
   */
  addListeners(aElement) {
    this.#abortController = new AbortController();
    aElement.documentGlobal.addEventListener("pagehide", this, {
      signal: this.#abortController.signal,
    });
  }

  /**
   * Helper function that returns the CSS direction property of the element.
   */
  getComputedDirection(aElement) {
    return aElement.documentGlobal
      .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) {
    let win = aElement.documentGlobal;
    return win.windowUtils.getElementBoundingScreenRect(aElement);
  }

  /**
   * MessageListener
   */
  receiveMessage(aMessage) {
    if (!this.#inputElement || this.#inputElement.type !== this.#inputType) {
      // Either we are already closed by content or the input type is changed
      return;
    }
    switch (aMessage.name) {
      case "InputPicker:Closed": {
        this.close();
        break;
      }
      case "InputPicker:ValueChanged": {
        this.pickerValueChangedImpl(aMessage, this.#inputElement);
        break;
      }
    }
  }

  /**
   * Element updater function called when the picker value is changed.
   *
   * @param {ReceiveMessageArgument} _aMessage
   * @param {HTMLInputElement} _inputElement
   */
  pickerValueChangedImpl(_aMessage, _inputElement) {
    throw new Error("Not implemented");
  }

  /**
   * nsIDOMEventListener, for chrome events sent by the input element and other
   * DOM events.
   */
  handleEvent(aEvent) {
    switch (aEvent.type) {
      case `MozOpen${this.#namespace}`: {
        if (
          !aEvent.originalTarget.documentGlobal.HTMLInputElement.isInstance(
            aEvent.originalTarget
          )
        ) {
          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;
        }

        /** @type {HTMLInputElement} */
        const inputElement = aEvent.originalTarget;
        const openPickerDetail = this.openPickerImpl(inputElement);
        if (!openPickerDetail) {
          // The impl doesn't want to proceed in this case
          return;
        }

        this.#inputElement = inputElement;
        this.#inputType = inputElement.type;
        this.addListeners(inputElement);

        this.sendAsyncMessage(`InputPicker:Open`, {
          rect: this.getBoundingContentRect(inputElement),
          dir: this.getComputedDirection(inputElement),
          type: inputElement.type,
          detail: openPickerDetail,
        });
        break;
      }
      case `MozClose${this.#namespace}`: {
        this.sendAsyncMessage(`InputPicker:Close`, {});
        this.close();
        break;
      }
      case "pagehide": {
        if (this.#inputElement?.ownerDocument == aEvent.target) {
          this.sendAsyncMessage(`InputPicker:Close`, {});
          this.close();
        }
        break;
      }
      default:
        break;
    }
  }

  /**
   * Picker initialization function called when opening the picker
   *
   * @param {HTMLInputElement} _inputElement
   * @returns An argument object to pass to the picker, or undefined to stop opening one.
   */
  openPickerImpl(_inputElement) {
    throw new Error("Not implemented");
  }

  /**
   * Picker updater function when the input value is updated
   *
   * @param {HTMLInputElement} _inputElement
   * @returns An argument object to pass to the picker
   */
  updatePickerImpl(_inputElement) {
    throw new Error("Not implemented");
  }
}
