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

export var PromptUtils = {
  // Fire a dialog open/close event. Used by tabbrowser to focus the
  // tab which is triggering a prompt.
  // For remote dialogs, we pass in a different DOM window and a separate
  // target. If the caller doesn't pass in the target, then we'll simply use
  // the passed-in DOM window.
  // The detail may contain information about the principal on which the
  // prompt is triggered, as well as whether or not this is a tabprompt
  // (ie tabmodal alert/prompt/confirm and friends)
  fireDialogEvent(domWin, eventName, maybeTarget, detail) {
    let target = maybeTarget || domWin;
    let eventOptions = { cancelable: true, bubbles: true };
    if (detail) {
      eventOptions.detail = detail;
    }
    let event = new domWin.CustomEvent(eventName, eventOptions);
    let winUtils = domWin.windowUtils;
    winUtils.dispatchEventToChromeOnly(target, event);
  },

  objectToPropBag(obj) {
    let bag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
      Ci.nsIWritablePropertyBag2
    );
    bag.QueryInterface(Ci.nsIWritablePropertyBag);

    for (let propName in obj) {
      bag.setProperty(propName, obj[propName]);
    }

    return bag;
  },

  propBagToObject(propBag, obj) {
    // Here we iterate over the object's original properties, not the bag
    // (ie, the prompt can't return more/different properties than were
    // passed in). This just helps ensure that the caller provides default
    // values, lest the prompt forget to set them.
    for (let propName in obj) {
      obj[propName] = propBag.getProperty(propName);
    }
  },
};

/**
 * This helper handles the enabling/disabling of dialogs that might
 * be subject to fast-clicking attacks. It handles the initial delayed
 * enabling of the dialog, as well as disabling it on blur and reapplying
 * the delay when the dialog regains focus.
 *
 * @param enableDialog   A custom function to be called when the dialog
 *                       is to be enabled.
 * @param diableDialog   A custom function to be called when the dialog
 *                       is to be disabled.
 * @param focusTarget    The window used to watch focus/blur events.
 */
export var EnableDelayHelper = function ({
  enableDialog,
  disableDialog,
  focusTarget,
}) {
  this.enableDialog = makeSafe(enableDialog);
  this.disableDialog = makeSafe(disableDialog);
  this.focusTarget = focusTarget;

  this.disableDialog();

  this.focusTarget.addEventListener("blur", this);
  this.focusTarget.addEventListener("focus", this);
  // While the user key-repeats, we want to renew the timer until keyup:
  this.focusTarget.addEventListener("keyup", this, true);
  this.focusTarget.addEventListener("keydown", this, true);
  this.focusTarget.document.addEventListener("unload", this);

  // If we're not part of the active window, don't even start the timer yet.
  let topWin = focusTarget.browsingContext.top.window;
  if (topWin != Services.focus.activeWindow) {
    return;
  }

  this.startOnFocusDelay();
};

EnableDelayHelper.prototype = {
  get delayTime() {
    return Services.prefs.getIntPref("security.dialog_enable_delay");
  },

  handleEvent(event) {
    if (
      !event.type.startsWith("key") &&
      event.target != this.focusTarget &&
      event.target != this.focusTarget.document
    ) {
      return;
    }

    switch (event.type) {
      case "keyup":
        // As soon as any key goes up, we can stop treating keypresses
        // as indicative of key-repeating that should prolong the timer.
        this.focusTarget.removeEventListener("keyup", this, true);
        this.focusTarget.removeEventListener("keydown", this, true);
        break;

      case "keydown":
        // Renew timer for repeating keydowns:
        if (this._focusTimer) {
          this._focusTimer.cancel();
          this._focusTimer = null;
          this.startOnFocusDelay();
          event.preventDefault();
        }
        break;

      case "blur":
        this.onBlur();
        break;

      case "focus":
        this.onFocus();
        break;

      case "unload":
        this.onUnload();
        break;
    }
  },

  onBlur() {
    this.disableDialog();
    // If we blur while waiting to enable the buttons, just cancel the
    // timer to ensure the delay doesn't fire while not focused.
    if (this._focusTimer) {
      this._focusTimer.cancel();
      this._focusTimer = null;
    }
  },

  onFocus() {
    this.startOnFocusDelay();
  },

  onUnload() {
    this.focusTarget.removeEventListener("blur", this);
    this.focusTarget.removeEventListener("focus", this);
    this.focusTarget.removeEventListener("keyup", this, true);
    this.focusTarget.removeEventListener("keydown", this, true);
    this.focusTarget.document.removeEventListener("unload", this);

    if (this._focusTimer) {
      this._focusTimer.cancel();
      this._focusTimer = null;
    }

    this.focusTarget = this.enableDialog = this.disableDialog = null;
  },

  startOnFocusDelay() {
    if (this._focusTimer) {
      return;
    }

    this._focusTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    this._focusTimer.initWithCallback(
      () => {
        this.onFocusTimeout();
      },
      this.delayTime,
      Ci.nsITimer.TYPE_ONE_SHOT
    );
  },

  onFocusTimeout() {
    this._focusTimer = null;
    this.enableDialog();
  },
};

function makeSafe(fn) {
  return function () {
    // The dialog could be gone by now (if the user closed it),
    // which makes it likely that the given fn might throw.
    try {
      fn();
    } catch (e) {}
  };
}
