/* 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, {
  PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
});

export var ClipboardContextMenu = {
  MENU_POPUP_ID: "clipboardReadPasteMenuPopup",

  // EventListener interface.
  handleEvent(aEvent) {
    switch (aEvent.type) {
      case "command": {
        this.onCommand();
        break;
      }
      case "popuphiding": {
        this.onPopupHiding();
        break;
      }
      case "keydown": {
        this.onKeyDown(aEvent);
        break;
      }
    }
  },

  _pasteMenuItemClicked: false,

  onCommand() {
    // onPopupHiding is responsible for returning result by calling onComplete
    // function.
    this._pasteMenuItemClicked = true;
  },

  onPopupHiding() {
    // Remove the listeners before potentially sending the async message
    // below, because that might throw.
    this._removeMenupopupEventListeners();
    this._clearDelayTimer();
    this._stopWatchingForSpammyActivation();

    this._menupopup = null;
    this._menuitem = null;

    let propBag = lazy.PromptUtils.objectToPropBag({
      ok: this._pasteMenuItemClicked,
    });
    this._pendingRequest.resolve(propBag);

    // A result has already been responded to. Reset the state to properly
    // handle further click or dismiss events.
    this._pasteMenuItemClicked = false;
    this._pendingRequest = null;
  },

  _lastBeepTime: 0,

  onKeyDown(aEvent) {
    if (!this._menuitem.disabled) {
      return;
    }

    let accesskey = this._menuitem.getAttribute("accesskey");
    if (
      aEvent.key == accesskey.toLowerCase() ||
      aEvent.key == accesskey.toUpperCase()
    ) {
      if (Date.now() - this._lastBeepTime > 1000) {
        Cc["@mozilla.org/sound;1"].getService(Ci.nsISound).beep();
        this._lastBeepTime = Date.now();
      }
      this._refreshDelayTimer();
    }
  },

  _menupopup: null,
  _menuitem: null,
  _pendingRequest: null,

  confirmUserPaste(aWindowContext) {
    return new Promise((resolve, reject) => {
      if (!aWindowContext) {
        reject(
          Components.Exception("Null window context.", Cr.NS_ERROR_INVALID_ARG)
        );
        return;
      }

      let { document } = aWindowContext.browsingContext.topChromeWindow;
      if (!document) {
        reject(
          Components.Exception(
            "Unable to get chrome document.",
            Cr.NS_ERROR_FAILURE
          )
        );
        return;
      }

      if (this._pendingRequest) {
        reject(
          Components.Exception(
            "There is an ongoing request.",
            Cr.NS_ERROR_FAILURE
          )
        );
        return;
      }

      this._pendingRequest = { resolve, reject };
      this._menupopup = this._getMenupopup(document);
      this._menuitem = this._menupopup.firstElementChild;
      this._addMenupopupEventListeners();

      let mouseXInCSSPixels = {};
      let mouseYInCSSPixels = {};
      document.ownerGlobal.windowUtils.getLastOverWindowPointerLocationInCSSPixels(
        mouseXInCSSPixels,
        mouseYInCSSPixels
      );

      this._menuitem.disabled = true;
      this._startWatchingForSpammyActivation();
      // `openPopup` is a no-op if the popup is already opened.
      // That property is used when `navigator.clipboard.readText()` or
      // `navigator.clipboard.read()`is called from two different frames, e.g.
      // an iframe and the top level frame. In that scenario, the two frames
      // correspond to different `navigator.clipboard` instances. When
      // `readText()` or `read()` is called from both frames, an actor pair is
      // instantiated for each of them. Both actor parents will call `openPopup`
      // on the same `_menupopup` object. If the popup is already open,
      // `openPopup` is a no-op. When the popup is clicked or dismissed both
      // actor parents will receive the corresponding event.
      this._menupopup.openPopup(null, {
        isContextMenu: true,
        position: "overlap",
        x: mouseXInCSSPixels.value,
        y: mouseYInCSSPixels.value,
      });

      this._refreshDelayTimer(document);
    });
  },

  _addMenupopupEventListeners() {
    this._menupopup.addEventListener("command", this);
    this._menupopup.addEventListener("popuphiding", this);
  },

  _removeMenupopupEventListeners() {
    this._menupopup.removeEventListener("command", this);
    this._menupopup.removeEventListener("popuphiding", this);
  },

  _createMenupopup(aChromeDoc) {
    let menuitem = aChromeDoc.createXULElement("menuitem");
    menuitem.id = "clipboardReadPasteMenuItem";
    aChromeDoc.l10n.setAttributes(menuitem, "text-action-paste");

    let menupopup = aChromeDoc.createXULElement("menupopup");
    menupopup.id = this.MENU_POPUP_ID;
    menupopup.setAttribute("tabspecific", "true");
    menupopup.setAttribute("locationspecific", "true");
    menupopup.appendChild(menuitem);
    return menupopup;
  },

  _getMenupopup(aChromeDoc) {
    let menupopup = aChromeDoc.getElementById(this.MENU_POPUP_ID);
    if (menupopup == null) {
      menupopup = this._createMenupopup(aChromeDoc);
      const parent =
        aChromeDoc.querySelector("popupset") || aChromeDoc.documentElement;
      parent.appendChild(menupopup);
    }

    return menupopup;
  },

  _startWatchingForSpammyActivation() {
    let doc = this._menuitem.ownerDocument;
    doc.addEventListener("keydown", this, {
      capture: true,
      mozSystemGroup: true,
    });
  },

  _stopWatchingForSpammyActivation() {
    let doc = this._menuitem.ownerDocument;
    doc.removeEventListener("keydown", this, {
      capture: true,
      mozSystemGroup: true,
    });
  },

  _delayTimer: null,

  _clearDelayTimer() {
    if (this._delayTimer) {
      let window = this._menuitem.ownerGlobal;
      window.clearTimeout(this._delayTimer);
      this._delayTimer = null;
    }
  },

  _refreshDelayTimer() {
    this._clearDelayTimer();

    let window = this._menuitem.ownerGlobal;
    let delay = Services.prefs.getIntPref("security.dialog_enable_delay");
    this._delayTimer = window.setTimeout(() => {
      this._menuitem.disabled = false;
      this._stopWatchingForSpammyActivation();
      this._delayTimer = null;
    }, delay);
  },
};
