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

/*
 * Chrome side handling of form validation popup.
 */

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
});

class PopupShownObserver {
  _weakContext = null;

  constructor(browsingContext) {
    this._weakContext = Cu.getWeakReference(browsingContext);
  }

  observe(subject, topic) {
    let ctxt = this._weakContext.get();
    let actor = ctxt.currentWindowGlobal?.getExistingActor("FormValidation");
    if (!actor) {
      Services.obs.removeObserver(this, "popup-shown");
      return;
    }
    // If any panel besides ourselves shows, hide ourselves again.
    if (topic == "popup-shown" && subject != actor._panel) {
      actor._hidePopup();
    }
  }

  QueryInterface = ChromeUtils.generateQI([
    Ci.nsIObserver,
    Ci.nsISupportsWeakReference,
  ]);
}

export class FormValidationParent extends JSWindowActorParent {
  constructor() {
    super();

    this._panel = null;
    this._obs = null;
  }

  static hasOpenPopups(ownPanel = null) {
    for (let win of lazy.BrowserWindowTracker.orderedWindows) {
      let popups = win.document.querySelectorAll("panel,menupopup");
      for (let popup of popups) {
        if (popup == ownPanel) {
          continue; // Skip our own panel if provided.
        }
        let { state } = popup;
        if (state == "open" || state == "showing") {
          return true;
        }
      }
    }
    return false;
  }

  /*
   * Public apis
   */

  uninit() {
    this._panel = null;
    this._obs = null;
  }

  hidePopup() {
    this._hidePopup();
  }

  /*
   * Events
   */

  receiveMessage(aMessage) {
    switch (aMessage.name) {
      case "FormValidation:ShowPopup": {
        let browser = this.browsingContext.top.embedderElement;
        let window = browser.ownerGlobal;
        let data = aMessage.data;
        let tabBrowser = window.gBrowser;

        // target is the <browser>, make sure we're receiving a message
        // from the foreground tab.
        if (tabBrowser && browser != tabBrowser.selectedBrowser) {
          return;
        }

        // If any other popups are open, we don't show the form validation
        // popup. We have to fall through for our own popup to make sure the
        // popup is updated if we are asked to reshow it with a different
        // message or for a different element.
        if (FormValidationParent.hasOpenPopups(this._panel)) {
          return;
        }

        this._showPopup(browser, data);
        break;
      }
      case "FormValidation:HidePopup":
        this._hidePopup();
        break;
    }
  }

  handleEvent(aEvent) {
    switch (aEvent.type) {
      case "FullZoomChange":
      case "TextZoomChange":
      case "scroll":
        this._hidePopup();
        break;
      case "popuphidden":
        this._onPopupHidden(aEvent);
        break;
    }
  }

  /*
   * Internal
   */

  _onPopupHidden(aEvent) {
    aEvent.originalTarget.removeEventListener("popuphidden", this, true);
    Services.obs.removeObserver(this._obs, "popup-shown");
    let tabBrowser = aEvent.originalTarget.ownerGlobal.gBrowser;
    tabBrowser.selectedBrowser.removeEventListener("scroll", this, true);
    tabBrowser.selectedBrowser.removeEventListener("FullZoomChange", this);
    tabBrowser.selectedBrowser.removeEventListener("TextZoomChange", this);

    this._obs = null;
    this._panel = null;
  }

  /**
   * Shows the form validation popup at a specified position or updates the
   * messaging and position if the popup is already displayed.
   *
   * @aBrowser - Browser element that requests the popup.
   * @aPanelData - Object that contains popup information
   *  aPanelData stucture detail:
   *   screenRect - the screen rect of the target element.
   *   position - popup positional string constants.
   *   message - the form element validation message text.
   */
  _showPopup(aBrowser, aPanelData) {
    let previouslyShown = !!this._panel;
    this._panel = this._getAndMaybeCreatePanel();
    this._panel.firstChild.textContent = aPanelData.message;

    // Display the panel if it isn't already visible.
    if (previouslyShown) {
      return;
    }
    // Cleanup after the popup is hidden
    this._panel.addEventListener("popuphidden", this, true);
    // Hide ourselves if other popups shown
    this._obs = new PopupShownObserver(this.browsingContext);
    Services.obs.addObserver(this._obs, "popup-shown", true);

    // Hide if the user scrolls the page
    aBrowser.addEventListener("scroll", this, true);
    aBrowser.addEventListener("FullZoomChange", this);
    aBrowser.addEventListener("TextZoomChange", this);

    aBrowser.constrainPopup(this._panel);

    // Open the popup
    let rect = aPanelData.screenRect;
    this._panel.openPopupAtScreenRect(
      aPanelData.position,
      rect.left,
      rect.top,
      rect.width,
      rect.height,
      false,
      false
    );
  }

  /*
   * Hide the popup if currently displayed. Will fire an event to onPopupHiding
   * above if visible.
   */
  _hidePopup() {
    this._panel?.hidePopup();
  }

  _getAndMaybeCreatePanel() {
    // Lazy load the invalid form popup the first time we need to display it.
    if (!this._panel) {
      let browser = this.browsingContext.top.embedderElement;
      let window = browser.ownerGlobal;
      let template = window.document.getElementById("invalidFormTemplate");
      if (template) {
        template.replaceWith(template.content);
      }
      this._panel = window.document.getElementById("invalid-form-popup");
    }

    return this._panel;
  }
}
