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

// Provides functionality for creating and sending DOM events.

"use strict";

const {interfaces: Ci, utils: Cu, classes: Cc} = Components;

Cu.import("resource://gre/modules/Log.jsm");
const logger = Log.repository.getLogger("Marionette");

Cu.import("chrome://marionette/content/element.js");
Cu.import("chrome://marionette/content/error.js");

this.EXPORTED_SYMBOLS = ["event"];

// must be synchronised with nsIDOMWindowUtils
const COMPOSITION_ATTR_RAWINPUT = 0x02;
const COMPOSITION_ATTR_SELECTEDRAWTEXT = 0x03;
const COMPOSITION_ATTR_CONVERTEDTEXT = 0x04;
const COMPOSITION_ATTR_SELECTEDCONVERTEDTEXT = 0x05;

// TODO(ato): Document!
let seenEvent = false;

function getDOMWindowUtils(win) {
  if (!win) {
    win = window;
  }

  // this assumes we are operating in chrome space
  return win.QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIDOMWindowUtils);
}

this.event = {};

event.MouseEvents = {
  click: 0,
  dblclick: 1,
  mousedown: 2,
  mouseup: 3,
  mouseover: 4,
  mouseout: 5,
};

event.Modifiers = {
  shiftKey: 0,
  ctrlKey: 1,
  altKey: 2,
  metaKey: 3,
};

/**
 * Sends a mouse event to given target.
 *
 * @param {nsIDOMMouseEvent} mouseEvent
 *     Event to send.
 * @param {(DOMElement|string)} target
 *     Target of event.  Can either be an element or the ID of an element.
 * @param {Window=} window
 *     Window object.  Defaults to the current window.
 *
 * @throws {TypeError}
 *     If the event is unsupported.
 */
event.sendMouseEvent = function (mouseEvent, target, window = undefined) {
  if (!event.MouseEvents.hasOwnProperty(mouseEvent.type)) {
    throw new TypeError("Unsupported event type: " + mouseEvent.type);
  }

  if (!target.nodeType && typeof target != "string") {
    throw new TypeError("Target can only be a DOM element or a string: " + target);
  }

  if (!target.nodeType) {
    target = window.document.getElementById(target);
  } else {
    window = window || target.ownerDocument.defaultView;
  }

  let ev = window.document.createEvent("MouseEvent");

  let type = mouseEvent.type;
  let view = window;

  let detail = mouseEvent.detail;
  if (!detail) {
    if (mouseEvent.type in ["click", "mousedown", "mouseup"]) {
      detail = 1;
    } else if (mouseEvent.type == "dblclick") {
      detail = 2;
    } else {
      detail = 0;
    }
  }

  let screenX = mouseEvent.screenX || 0;
  let screenY = mouseEvent.screenY || 0;
  let clientX = mouseEvent.clientX || 0;
  let clientY = mouseEvent.clientY || 0;
  let ctrlKey = mouseEvent.ctrlKey || false;
  let altKey = mouseEvent.altKey || false;
  let shiftKey = mouseEvent.shiftKey || false;
  let metaKey = mouseEvent.metaKey || false;
  let button = mouseEvent.button || 0;
  let relatedTarget = mouseEvent.relatedTarget || null;

  ev.initMouseEvent(
      mouseEvent.type,
      /* canBubble */ true,
      /* cancelable */ true,
      view,
      detail,
      screenX,
      screenY,
      clientX,
      clientY,
      ctrlKey,
      altKey,
      shiftKey,
      metaKey,
      button,
      relatedTarget);
};

/**
 * Send character to the currently focused element.
 *
 * This function handles casing of characters (sends the right charcode,
 * and sends a shift key for uppercase chars).  No other modifiers are
 * handled at this point.
 *
 * For now this method only works for English letters (lower and upper
 * case) and the digits 0-9.
 */
event.sendChar = function (char, window = undefined) {
  // DOM event charcodes match ASCII (JS charcodes) for a-zA-Z0-9
  let hasShift = (char == char.toUpperCase());
  event.synthesizeKey(char, {shiftKey: hasShift}, window);
};

/**
 * Send string to the focused element.
 *
 * For now this method only works for English letters (lower and upper
 * case) and the digits 0-9.
 */
event.sendString = function (string, window = undefined) {
  for (let i = 0; i < string.length; ++i) {
    event.sendChar(string.charAt(i), window);
  }
};

/**
 * Send the non-character key to the focused element.
 *
 * The name of the key should be the part that comes after "DOM_VK_"
 * in the nsIDOMKeyEvent constant name for this key.  No modifiers are
 * handled at this point.
 */
event.sendKey = function (key, window = undefined) {
  let keyName = "VK_" + key.toUpperCase();
  event.synthesizeKey(keyName, {shiftKey: false}, window);
};

// TODO(ato): Unexpose this when action.Chain#emitMouseEvent
// no longer emits its own events
event.parseModifiers_ = function (modifiers) {
  let mval = 0;
  if (modifiers.shiftKey) {
    mval |= Ci.nsIDOMNSEvent.SHIFT_MASK;
  }
  if (modifiers.ctrlKey) {
    mval |= Ci.nsIDOMNSEvent.CONTROL_MASK;
  }
  if (modifiers.altKey) {
    mval |= Ci.nsIDOMNSEvent.ALT_MASK;
  }
  if (modifiers.metaKey) {
    mval |= Ci.nsIDOMNSEvent.META_MASK;
  }
  if (modifiers.accelKey) {
    if (navigator.platform.indexOf("Mac") >= 0) {
      mval |= Ci.nsIDOMNSEvent.META_MASK;
    } else {
      mval |= Ci.nsIDOMNSEvent.CONTROL_MASK;
    }
  }
  return mval;
};

/**
 * Synthesise a mouse event on a target.
 *
 * The actual client point is determined by taking the aTarget's client
 * box and offseting it by offsetX and offsetY.  This allows mouse clicks
 * to be simulated by calling this method.
 *
 * If the type is specified, an mouse event of that type is
 * fired. Otherwise, a mousedown followed by a mouse up is performed.
 *
 * @param {Element} element
 *     Element to click.
 * @param {number} offsetX
 *     Horizontal offset to click from the target's bounding box.
 * @param {number} offsetY
 *     Vertical offset to click from the target's bounding box.
 * @param {Object.<string, ?>} opts
 *     Object which may contain the properties "shiftKey", "ctrlKey",
 *     "altKey", "metaKey", "accessKey", "clickCount", "button", and
 *     "type".
 * @param {Window=} window
 *     Window object.  Defaults to the current window.
 */
event.synthesizeMouse = function (
    element, offsetX, offsetY, opts, window = undefined) {
  let rect = element.getBoundingClientRect();
  event.synthesizeMouseAtPoint(
      rect.left + offsetX, rect.top + offsetY, opts, window);
};

/*
 * Synthesize a mouse event at a particular point in a window.
 *
 * If the type of the event is specified, a mouse event of that type is
 * fired. Otherwise, a mousedown followed by a mouse up is performed.
 *
 * @param {number} left
 *     CSS pixels from the left document margin.
 * @param {number} top
 *     CSS pixels from the top document margin.
 * @param {Object.<string, ?>} opts
 *     Object which may contain the properties "shiftKey", "ctrlKey",
 *     "altKey", "metaKey", "accessKey", "clickCount", "button", and
 *     "type".
 * @param {Window=} window
 *     Window object.  Defaults to the current window.
 */
event.synthesizeMouseAtPoint = function (
    left, top, opts, window = undefined) {

  let domutils = getDOMWindowUtils(window);

  let button = opts.button || 0;
  let clickCount = opts.clickCount || 1;
  let modifiers = event.parseModifiers_(opts);
  let pressure = ("pressure" in opts) ? opts.pressure : 0;
  let inputSource = ("inputSource" in opts) ? opts.inputSource :
      Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE;
  let isDOMEventSynthesized =
      ("isSynthesized" in opts) ? opts.isSynthesized : true;
  let isWidgetEventSynthesized =
      ("isWidgetEventSynthesized" in opts) ? opts.isWidgetEventSynthesized : false;
  let buttons = ("buttons" in opts) ? opts.buttons : domutils.MOUSE_BUTTONS_NOT_SPECIFIED;

  if (("type" in opts) && opts.type) {
    domutils.sendMouseEvent(
        opts.type, left, top, button, clickCount, modifiers, false, pressure, inputSource,
        isDOMEventSynthesized, isWidgetEventSynthesized, buttons);
  } else {
    domutils.sendMouseEvent(
        "mousedown", left, top, button, clickCount, modifiers, false, pressure, inputSource,
        isDOMEventSynthesized, isWidgetEventSynthesized, buttons);
    domutils.sendMouseEvent(
        "mouseup", left, top, button, clickCount, modifiers, false, pressure, inputSource,
        isDOMEventSynthesized, isWidgetEventSynthesized, buttons);
  }
};

/**
 * Call event.synthesizeMouse with coordinates at the centre of the
 * target.
 */
event.synthesizeMouseAtCenter = function (element, event, window) {
  let rect = element.getBoundingClientRect();
  event.synthesizeMouse(
      element,
      rect.width / 2,
      rect.height / 2,
      event,
      window);
};

/**
 * Synthesise a mouse scroll event on a target.
 *
 * The actual client point is determined by taking the target's client
 * box and offseting it by |offsetX| and |offsetY|.
 *
 * If the |type| property is specified for the |event| argument, a mouse
 * scroll event of that type is fired.  Otherwise, DOMMouseScroll is used.
 *
 * If the |axis| is specified, it must be one of "horizontal" or
 * "vertical". If not specified, "vertical" is used.
 *
 * |delta| is the amount to scroll by (can be positive or negative).
 * It must be specified.
 *
 * |hasPixels| specifies whether kHasPixels should be set in the
 * |scrollFlags|.
 *
 * |isMomentum| specifies whether kIsMomentum should be set in the
 * |scrollFlags|.
 *
 * @param {Element} target
 * @param {number} offsetY
 * @param {number} offsetY
 * @param {Object.<string, ?>} event
 *     Object which may contain the properties shiftKey, ctrlKey, altKey,
 *     metaKey, accessKey, button, type, axis, delta, and hasPixels.
 * @param {Window=} window
 *     Window object.  Defaults to the current window.
 */
event.synthesizeMouseScroll = function (
    target, offsetX, offsetY, ev, window = undefined) {

  let domutils = getDOMWindowUtils(window);

  // see nsMouseScrollFlags in nsGUIEvent.h
  const kIsVertical = 0x02;
  const kIsHorizontal = 0x04;
  const kHasPixels = 0x08;
  const kIsMomentum = 0x40;

  let button = ev.button || 0;
  let modifiers = event.parseModifiers_(ev);

  let rect = target.getBoundingClientRect();
  let left = rect.left;
  let top = rect.top;

  let type = (("type" in ev) && ev.type) || "DOMMouseScroll";
  let axis = ev.axis || "vertical";
  let scrollFlags = (axis == "horizontal") ? kIsHorizontal : kIsVertical;
  if (ev.hasPixels) {
    scrollFlags |= kHasPixels;
  }
  if (ev.isMomentum) {
    scrollFlags |= kIsMomentum;
  }

  domutils.sendMouseScrollEvent(
      type,
      left + offsetX,
      top + offsetY,
      button,
      scrollFlags,
      ev.delta,
      modifiers);
};

function computeKeyCodeFromChar_(char) {
  if (char.length != 1) {
    return 0;
  }

  if (char >= "a" && char <= "z") {
    return Ci.nsIDOMKeyEvent.DOM_VK_A + char.charCodeAt(0) - "a".charCodeAt(0);
  }
  if (char >= "A" && char <= "Z") {
    return Ci.nsIDOMKeyEvent.DOM_VK_A + char.charCodeAt(0) - "A".charCodeAt(0);
  }
  if (char >= "0" && char <= "9") {
    return Ci.nsIDOMKeyEvent.DOM_VK_0 + char.charCodeAt(0) - "0".charCodeAt(0);
  }

  // returns US keyboard layout's keycode
  switch (char) {
    case "~":
    case "`":
      return Ci.nsIDOMKeyEvent.DOM_VK_BACK_QUOTE;

    case "!":
      return Ci.nsIDOMKeyEvent.DOM_VK_1;

    case "@":
      return Ci.nsIDOMKeyEvent.DOM_VK_2;

    case "#":
      return Ci.nsIDOMKeyEvent.DOM_VK_3;

    case "$":
      return Ci.nsIDOMKeyEvent.DOM_VK_4;

    case "%":
      return Ci.nsIDOMKeyEvent.DOM_VK_5;

    case "^":
      return Ci.nsIDOMKeyEvent.DOM_VK_6;

    case "&":
      return Ci.nsIDOMKeyEvent.DOM_VK_7;

    case "*":
      return Ci.nsIDOMKeyEvent.DOM_VK_8;

    case "(":
      return Ci.nsIDOMKeyEvent.DOM_VK_9;

    case ")":
      return Ci.nsIDOMKeyEvent.DOM_VK_0;

    case "-":
    case "_":
      return Ci.nsIDOMKeyEvent.DOM_VK_SUBTRACT;

    case "+":
    case "=":
      return Ci.nsIDOMKeyEvent.DOM_VK_EQUALS;

    case "{":
    case "[":
      return Ci.nsIDOMKeyEvent.DOM_VK_OPEN_BRACKET;

    case "}":
    case "]":
      return Ci.nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET;

    case "|":
    case "\\":
      return Ci.nsIDOMKeyEvent.DOM_VK_BACK_SLASH;

    case ":":
    case ";":
      return Ci.nsIDOMKeyEvent.DOM_VK_SEMICOLON;

    case "'":
    case "\"":
      return Ci.nsIDOMKeyEvent.DOM_VK_QUOTE;

    case "<":
    case ",":
      return Ci.nsIDOMKeyEvent.DOM_VK_COMMA;

    case ">":
    case ".":
      return Ci.nsIDOMKeyEvent.DOM_VK_PERIOD;

    case "?":
    case "/":
      return Ci.nsIDOMKeyEvent.DOM_VK_SLASH;

    case "\n":
      return Ci.nsIDOMKeyEvent.DOM_VK_RETURN;

    default:
      return 0;
  }
}

/**
 * Returns true if the given key should cause keypress event when widget
 * handles the native key event.  Otherwise, false.
 *
 * The key code should be one of consts of nsIDOMKeyEvent.DOM_VK_*,
 * or a key name begins with "VK_", or a character.
 */
event.isKeypressFiredKey = function (key) {
  if (typeof key == "string") {
    if (key.indexOf("VK_") === 0) {
      key = Ci.nsIDOMKeyEvent["DOM_" + key];
      if (!key) {
        throw new TypeError("Unknown key: " + key);
      }

    // if key generates a character, it must cause a keypress event
    } else {
      return true;
    }
  }

  switch (key) {
    case Ci.nsIDOMKeyEvent.DOM_VK_SHIFT:
    case Ci.nsIDOMKeyEvent.DOM_VK_CONTROL:
    case Ci.nsIDOMKeyEvent.DOM_VK_ALT:
    case Ci.nsIDOMKeyEvent.DOM_VK_CAPS_LOCK:
    case Ci.nsIDOMKeyEvent.DOM_VK_NUM_LOCK:
    case Ci.nsIDOMKeyEvent.DOM_VK_SCROLL_LOCK:
    case Ci.nsIDOMKeyEvent.DOM_VK_META:
      return false;

    default:
      return true;
  }
};

/**
 * Synthesise a key event.
 *
 * It is targeted at whatever would be targeted by an actual keypress
 * by the user, typically the focused element.
 *
 * @param {string} key
 *     Key to synthesise.  Should either be a character or a key code
 *     starting with "VK_" such as VK_RETURN, or a normalized key value.
 * @param {Object.<string, ?>} event
 *     Object which may contain the properties shiftKey, ctrlKey, altKey,
 *     metaKey, accessKey, type.  If the type is specified (keydown or keyup),
 *     a key event of that type is fired.  Otherwise, a keydown, a keypress,
 *     and then a keyup event are fired in sequence.
 * @param {Window=} window
 *     Window object.  Defaults to the current window.
 *
 * @throws {TypeError}
 *     If unknown key.
 */
event.synthesizeKey = function (key, event, win = undefined)
{
  var TIP = getTIP_(win);
  if (!TIP) {
    return;
  }
  var KeyboardEvent = getKeyboardEvent_(win);
  var modifiers = emulateToActivateModifiers_(TIP, event, win);
  var keyEventDict = createKeyboardEventDictionary_(key, event, win);
  var keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
  var dispatchKeydown =
    !("type" in event) || event.type === "keydown" || !event.type;
  var dispatchKeyup =
    !("type" in event) || event.type === "keyup"   || !event.type;

  try {
    if (dispatchKeydown) {
      TIP.keydown(keyEvent, keyEventDict.flags);
      if ("repeat" in event && event.repeat > 1) {
        keyEventDict.dictionary.repeat = true;
        var repeatedKeyEvent = new KeyboardEvent("", keyEventDict.dictionary);
        for (var i = 1; i < event.repeat; i++) {
          TIP.keydown(repeatedKeyEvent, keyEventDict.flags);
        }
      }
    }
    if (dispatchKeyup) {
      TIP.keyup(keyEvent, keyEventDict.flags);
    }
  } finally {
    emulateToInactivateModifiers_(TIP, modifiers, win);
  }
};

var TIPMap = new WeakMap();

function getTIP_(win, callback)
{
  if (!win) {
    win = window;
  }
  var tip;
  if (TIPMap.has(win)) {
    tip = TIPMap.get(win);
  } else {
    tip =
      Cc["@mozilla.org/text-input-processor;1"].
        createInstance(Ci.nsITextInputProcessor);
    TIPMap.set(win, tip);
  }
  if (!tip.beginInputTransactionForTests(win, callback)) {
    tip = null;
    TIPMap.delete(win);
  }
  return tip;
}

function getKeyboardEvent_(win = window)
{
  if (typeof KeyboardEvent != "undefined") {
    try {
      // See if the object can be instantiated; sometimes this yields
      // 'TypeError: can't access dead object' or 'KeyboardEvent is not a constructor'.
      new KeyboardEvent("", {});
      return KeyboardEvent;
    } catch (ex) {}
  }
  if (typeof content != "undefined" && ("KeyboardEvent" in content)) {
    return content.KeyboardEvent;
  }
  return win.KeyboardEvent;
}

function createKeyboardEventDictionary_(key, keyEvent, win = window) {
  var result = { dictionary: null, flags: 0 };
  var keyCodeIsDefined = "keyCode" in keyEvent;
  var keyCode =
    (keyCodeIsDefined && keyEvent.keyCode >= 0 && keyEvent.keyCode <= 255) ?
      keyEvent.keyCode : 0;
  var keyName = "Unidentified";
  if (key.indexOf("KEY_") == 0) {
    keyName = key.substr("KEY_".length);
    result.flags |= Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY;
  } else if (key.indexOf("VK_") == 0) {
    keyCode = Ci.nsIDOMKeyEvent["DOM_" + key];
    if (!keyCode) {
      throw "Unknown key: " + key;
    }
    keyName = guessKeyNameFromKeyCode_(keyCode, win);
    result.flags |= Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY;
  } else if (key != "") {
    keyName = key;
    if (!keyCodeIsDefined) {
      keyCode = computeKeyCodeFromChar_(key.charAt(0));
    }
    if (!keyCode) {
      result.flags |= Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO;
    }
    // keyName was already determined in keyEvent so no fall-back needed
    if (!("key" in keyEvent && keyName == keyEvent.key)) {
      result.flags |= Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY;
    }
  }
  var locationIsDefined = "location" in keyEvent;
  if (locationIsDefined && keyEvent.location === 0) {
    result.flags |= Ci.nsITextInputProcessor.KEY_KEEP_KEY_LOCATION_STANDARD;
  }
  result.dictionary = {
    key: keyName,
    code: "code" in keyEvent ? keyEvent.code : "",
    location: locationIsDefined ? keyEvent.location : 0,
    repeat: "repeat" in keyEvent ? keyEvent.repeat === true : false,
    keyCode: keyCode,
  };
  return result;
}

function emulateToActivateModifiers_(TIP, keyEvent, win = window)
{
  if (!keyEvent) {
    return null;
  }
  var KeyboardEvent = getKeyboardEvent_(win);
  var navigator = getNavigator_(win);

  var modifiers = {
    normal: [
      { key: "Alt",        attr: "altKey" },
      { key: "AltGraph",   attr: "altGraphKey" },
      { key: "Control",    attr: "ctrlKey" },
      { key: "Fn",         attr: "fnKey" },
      { key: "Meta",       attr: "metaKey" },
      { key: "OS",         attr: "osKey" },
      { key: "Shift",      attr: "shiftKey" },
      { key: "Symbol",     attr: "symbolKey" },
      { key: isMac_(win) ? "Meta" : "Control",
                           attr: "accelKey" },
    ],
    lockable: [
      { key: "CapsLock",   attr: "capsLockKey" },
      { key: "FnLock",     attr: "fnLockKey" },
      { key: "NumLock",    attr: "numLockKey" },
      { key: "ScrollLock", attr: "scrollLockKey" },
      { key: "SymbolLock", attr: "symbolLockKey" },
    ]
  }

  for (var i = 0; i < modifiers.normal.length; i++) {
    if (!keyEvent[modifiers.normal[i].attr]) {
      continue;
    }
    if (TIP.getModifierState(modifiers.normal[i].key)) {
      continue; // already activated.
    }
    var event = new KeyboardEvent("", { key: modifiers.normal[i].key });
    TIP.keydown(event,
      TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT);
    modifiers.normal[i].activated = true;
  }
  for (var i = 0; i < modifiers.lockable.length; i++) {
    if (!keyEvent[modifiers.lockable[i].attr]) {
      continue;
    }
    if (TIP.getModifierState(modifiers.lockable[i].key)) {
      continue; // already activated.
    }
    var event = new KeyboardEvent("", { key: modifiers.lockable[i].key });
    TIP.keydown(event,
      TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT);
    TIP.keyup(event,
      TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT);
    modifiers.lockable[i].activated = true;
  }
  return modifiers;
}

function emulateToInactivateModifiers_(TIP, modifiers, win = window)
{
  if (!modifiers) {
    return;
  }
  var KeyboardEvent = getKeyboardEvent_(win);
  for (var i = 0; i < modifiers.normal.length; i++) {
    if (!modifiers.normal[i].activated) {
      continue;
    }
    var event = new KeyboardEvent("", { key: modifiers.normal[i].key });
    TIP.keyup(event,
      TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT);
  }
  for (var i = 0; i < modifiers.lockable.length; i++) {
    if (!modifiers.lockable[i].activated) {
      continue;
    }
    if (!TIP.getModifierState(modifiers.lockable[i].key)) {
      continue; // who already inactivated this?
    }
    var event = new KeyboardEvent("", { key: modifiers.lockable[i].key });
    TIP.keydown(event,
      TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT);
    TIP.keyup(event,
      TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT);
  }
}

function getNavigator_(win = window)
{
  if (typeof navigator != "undefined") {
    return navigator;
  }
  return win.navigator;
}

function isMac_(win = window) {
  if (win) {
    try {
      return win.navigator.platform.indexOf("Mac") > -1;
    } catch (ex) {}
  }
  return navigator.platform.indexOf("Mac") > -1;
}

function guessKeyNameFromKeyCode_(aKeyCode, win = window)
{
  var KeyboardEvent = getKeyboardEvent_(win);
  switch (aKeyCode) {
    case KeyboardEvent.DOM_VK_CANCEL:
      return "Cancel";
    case KeyboardEvent.DOM_VK_HELP:
      return "Help";
    case KeyboardEvent.DOM_VK_BACK_SPACE:
      return "Backspace";
    case KeyboardEvent.DOM_VK_TAB:
      return "Tab";
    case KeyboardEvent.DOM_VK_CLEAR:
      return "Clear";
    case KeyboardEvent.DOM_VK_RETURN:
      return "Enter";
    case KeyboardEvent.DOM_VK_SHIFT:
      return "Shift";
    case KeyboardEvent.DOM_VK_CONTROL:
      return "Control";
    case KeyboardEvent.DOM_VK_ALT:
      return "Alt";
    case KeyboardEvent.DOM_VK_PAUSE:
      return "Pause";
    case KeyboardEvent.DOM_VK_EISU:
      return "Eisu";
    case KeyboardEvent.DOM_VK_ESCAPE:
      return "Escape";
    case KeyboardEvent.DOM_VK_CONVERT:
      return "Convert";
    case KeyboardEvent.DOM_VK_NONCONVERT:
      return "NonConvert";
    case KeyboardEvent.DOM_VK_ACCEPT:
      return "Accept";
    case KeyboardEvent.DOM_VK_MODECHANGE:
      return "ModeChange";
    case KeyboardEvent.DOM_VK_PAGE_UP:
      return "PageUp";
    case KeyboardEvent.DOM_VK_PAGE_DOWN:
      return "PageDown";
    case KeyboardEvent.DOM_VK_END:
      return "End";
    case KeyboardEvent.DOM_VK_HOME:
      return "Home";
    case KeyboardEvent.DOM_VK_LEFT:
      return "ArrowLeft";
    case KeyboardEvent.DOM_VK_UP:
      return "ArrowUp";
    case KeyboardEvent.DOM_VK_RIGHT:
      return "ArrowRight";
    case KeyboardEvent.DOM_VK_DOWN:
      return "ArrowDown";
    case KeyboardEvent.DOM_VK_SELECT:
      return "Select";
    case KeyboardEvent.DOM_VK_PRINT:
      return "Print";
    case KeyboardEvent.DOM_VK_EXECUTE:
      return "Execute";
    case KeyboardEvent.DOM_VK_PRINTSCREEN:
      return "PrintScreen";
    case KeyboardEvent.DOM_VK_INSERT:
      return "Insert";
    case KeyboardEvent.DOM_VK_DELETE:
      return "Delete";
    case KeyboardEvent.DOM_VK_WIN:
      return "OS";
    case KeyboardEvent.DOM_VK_CONTEXT_MENU:
      return "ContextMenu";
    case KeyboardEvent.DOM_VK_SLEEP:
      return "Standby";
    case KeyboardEvent.DOM_VK_F1:
      return "F1";
    case KeyboardEvent.DOM_VK_F2:
      return "F2";
    case KeyboardEvent.DOM_VK_F3:
      return "F3";
    case KeyboardEvent.DOM_VK_F4:
      return "F4";
    case KeyboardEvent.DOM_VK_F5:
      return "F5";
    case KeyboardEvent.DOM_VK_F6:
      return "F6";
    case KeyboardEvent.DOM_VK_F7:
      return "F7";
    case KeyboardEvent.DOM_VK_F8:
      return "F8";
    case KeyboardEvent.DOM_VK_F9:
      return "F9";
    case KeyboardEvent.DOM_VK_F10:
      return "F10";
    case KeyboardEvent.DOM_VK_F11:
      return "F11";
    case KeyboardEvent.DOM_VK_F12:
      return "F12";
    case KeyboardEvent.DOM_VK_F13:
      return "F13";
    case KeyboardEvent.DOM_VK_F14:
      return "F14";
    case KeyboardEvent.DOM_VK_F15:
      return "F15";
    case KeyboardEvent.DOM_VK_F16:
      return "F16";
    case KeyboardEvent.DOM_VK_F17:
      return "F17";
    case KeyboardEvent.DOM_VK_F18:
      return "F18";
    case KeyboardEvent.DOM_VK_F19:
      return "F19";
    case KeyboardEvent.DOM_VK_F20:
      return "F20";
    case KeyboardEvent.DOM_VK_F21:
      return "F21";
    case KeyboardEvent.DOM_VK_F22:
      return "F22";
    case KeyboardEvent.DOM_VK_F23:
      return "F23";
    case KeyboardEvent.DOM_VK_F24:
      return "F24";
    case KeyboardEvent.DOM_VK_NUM_LOCK:
      return "NumLock";
    case KeyboardEvent.DOM_VK_SCROLL_LOCK:
      return "ScrollLock";
    case KeyboardEvent.DOM_VK_VOLUME_MUTE:
      return "AudioVolumeMute";
    case KeyboardEvent.DOM_VK_VOLUME_DOWN:
      return "AudioVolumeDown";
    case KeyboardEvent.DOM_VK_VOLUME_UP:
      return "AudioVolumeUp";
    case KeyboardEvent.DOM_VK_META:
      return "Meta";
    case KeyboardEvent.DOM_VK_ALTGR:
      return "AltGraph";
    case KeyboardEvent.DOM_VK_ATTN:
      return "Attn";
    case KeyboardEvent.DOM_VK_CRSEL:
      return "CrSel";
    case KeyboardEvent.DOM_VK_EXSEL:
      return "ExSel";
    case KeyboardEvent.DOM_VK_EREOF:
      return "EraseEof";
    case KeyboardEvent.DOM_VK_PLAY:
      return "Play";
    default:
      return "Unidentified";
  }
}

/**
 * Indicate that an event with an original target and type is expected
 * to be fired, or not expected to be fired.
 */
function expectEvent_(expectedTarget, expectedEvent, testName) {
  if (!expectedTarget || !expectedEvent) {
    return null;
  }

  seenEvent = false;

  let type;
  if (expectedEvent.charAt(0) == "!") {
    type = expectedEvent.substring(1);
  } else {
    type = expectedEvent;
  }

  let handler = ev => {
    let pass = (!seenEvent && ev.originalTarget == expectedTarget && ev.type == type);
    is(pass, true, `${testName} ${type} event target ${seenEvent ? "twice" : ""}`);
    seenEvent = true;
  };

  expectedTarget.addEventListener(type, handler, false);
  return handler;
}

/**
 * Check if the event was fired or not. The provided event handler will
 * be removed.
 */
function checkExpectedEvent_(
    expectedTarget, expectedEvent, eventHandler, testName) {

  if (eventHandler) {
    let expectEvent = (expectedEvent.charAt(0) != "!");
    let type = expectEvent;
    if (!type) {
      type = expectedEvent.substring(1);
    }
    expectedTarget.removeEventListener(type, eventHandler, false);

    let desc = `${type} event`;
    if (!expectEvent) {
      desc += " not";
    }
    is(seenEvent, expectEvent, `${testName} ${desc} fired`);
  }

  seenEvent = false;
}

/**
 * Similar to event.synthesizeMouse except that a test is performed to
 * see if an event is fired at the right target as a result.
 *
 * To test that an event is not fired, use an expected type preceded by
 * an exclamation mark, such as "!select". This might be used to test that
 * a click on a disabled element doesn't fire certain events for instance.
 *
 * @param {Element} target
 *     Synthesise the mouse event on this target.
 * @param {number} offsetX
 *     Horizontal offset from the target's bounding box.
 * @param {number} offsetY
 *     Vertical offset from the target's bounding box.
 * @param {Object.<string, ?>} ev
 *     Object which may contain the properties shiftKey, ctrlKey, altKey,
 *     metaKey, accessKey, type.
 * @param {Element} expectedTarget
 *     Expected originalTarget of the event.
 * @param {DOMEvent} expectedEvent
 *     Expected type of the event, such as "select".
 * @param {string} testName
 *     Test name when outputing results.
 * @param {Window=} window
 *     Window object.  Defaults to the current window.
 */
event.synthesizeMouseExpectEvent = function (
    target, offsetX, offsetY, ev, expectedTarget, expectedEvent,
    testName, window = undefined) {

  let eventHandler = expectEvent_(
      expectedTarget,
      expectedEvent,
      testName);
  event.synthesizeMouse(target, offsetX, offsetY, ev, window);
  checkExpectedEvent_(
      expectedTarget,
      expectedEvent,
      eventHandler,
      testName);
};

/**
 * Similar to synthesizeKey except that a test is performed to see if
 * an event is fired at the right target as a result.
 *
 * @param {string} key
 *     Key to synthesise.
 * @param {Object.<string, ?>} ev
 *     Object which may contain the properties shiftKey, ctrlKey, altKey,
 *     metaKey, accessKey, type.
 * @param {Element} expectedTarget
 *     Expected originalTarget of the event.
 * @param {DOMEvent} expectedEvent
 *     Expected type of the event, such as "select".
 * @param {string} testName
 *     Test name when outputing results
 * @param {Window=} window
 *     Window object.  Defaults to the current window.
 *
 * To test that an event is not fired, use an expected type preceded by an
 * exclamation mark, such as "!select".
 *
 * aWindow is optional, and defaults to the current window object.
 */
event.synthesizeKeyExpectEvent = function (
    key, ev, expectedTarget, expectedEvent, testName,
    window = undefined) {

  let eventHandler = expectEvent_(
      expectedTarget,
      expectedEvent,
      testName);
  event.synthesizeKey(key, ev, window);
  checkExpectedEvent_(
      expectedTarget,
      expectedEvent,
      eventHandler,
      testName);
};

/**
 * Synthesize a composition event.
 *
 * @param {DOMEvent} ev
 *     The composition event information.  This must have |type|
 *     member.  The value must be "compositionstart", "compositionend" or
 *     "compositionupdate".  And also this may have |data| and |locale|
 *     which would be used for the value of each property of the
 *     composition event.  Note that the data would be ignored if the
 *     event type were "compositionstart".
 * @param {Window=} window
 *     Window object.  Defaults to the current window.
 */
event.synthesizeComposition = function (ev, window = undefined) {
  let domutils = getDOMWindowUtils(window);
  domutils.sendCompositionEvent(ev.type, ev.data || "", ev.locale || "");
};

/**
 * Synthesize a text event.
 *
 * The text event's information, this has |composition| and |caret|
 * members.  |composition| has |string| and |clauses| members. |clauses|
 * must be array object.  Each object has |length| and |attr|.
 * And |caret| has |start| and |length|.  See the following tree image.
 *
 *     ev
 *      +-- composition
 *      |     +-- string
 *      |     +-- clauses[]
 *      |           +-- length
 *      |           +-- attr
 *      +-- caret
 *            +-- start
 *            +-- length
 *
 * Set the composition string to |composition.string|.  Set its clauses
 * information to the |clauses| array.
 *
 * When it's composing, set the each clauses' length
 * to the |composition.clauses[n].length|.  The sum
 * of the all length values must be same as the length of
 * |composition.string|. Set nsIDOMWindowUtils.COMPOSITION_ATTR_* to the
 * |composition.clauses[n].attr|.
 *
 * When it's not composing, set 0 to the |composition.clauses[0].length|
 * and |composition.clauses[0].attr|.
 *
 * Set caret position to the |caret.start|. Its offset from the start of
 * the composition string.  Set caret length to |caret.length|.  If it's
 * larger than 0, it should be wide caret.  However, current nsEditor
 * doesn't support wide caret, therefore, you should always set 0 now.
 *
 * @param {Object.<string, ?>} ev
 *     The text event's information,
 * @param {Window=} window
 *     Window object.  Defaults to the current window.
 */
event.synthesizeText = function (ev, window = undefined) {
  let domutils = getDOMWindowUtils(window);

  if (!ev.composition ||
      !ev.composition.clauses ||
      !ev.composition.clauses[0]) {
    return;
  }

  let firstClauseLength = ev.composition.clauses[0].length;
  let firstClauseAttr   = ev.composition.clauses[0].attr;
  let secondClauseLength = 0;
  let secondClauseAttr = 0;
  let thirdClauseLength = 0;
  let thirdClauseAttr = 0;
  if (ev.composition.clauses[1]) {
    secondClauseLength = ev.composition.clauses[1].length;
    secondClauseAttr   = ev.composition.clauses[1].attr;
    if (event.composition.clauses[2]) {
      thirdClauseLength = ev.composition.clauses[2].length;
      thirdClauseAttr   = ev.composition.clauses[2].attr;
    }
  }

  let caretStart = -1;
  let caretLength = 0;
  if (event.caret) {
    caretStart = ev.caret.start;
    caretLength = ev.caret.length;
  }

  domutils.sendTextEvent(
      ev.composition.string,
      firstClauseLength,
      firstClauseAttr,
      secondClauseLength,
      secondClauseAttr,
      thirdClauseLength,
      thirdClauseAttr,
      caretStart,
      caretLength);
};

/**
 * Synthesize a query selected text event.
 *
 * @param {Window=}
 *     Window object.  Defaults to the current window.
 *
 * @return {(nsIQueryContentEventResult|null)}
 *     Event's result, or null if it failed.
 */
event.synthesizeQuerySelectedText = function (window = undefined) {
  let domutils = getDOMWindowUtils(window);
  return domutils.sendQueryContentEvent(
      domutils.QUERY_SELECTED_TEXT, 0, 0, 0, 0);
};

/**
 * Synthesize a selection set event.
 *
 * @param {number} offset
 *     Character offset.  0 means the first character in the selection
 *     root.
 * @param {number} length
 *     Length of the text.  If the length is too long, the extra length
 *     is ignored.
 * @param {boolean} reverse
 *     If true, the selection is from |aOffset + aLength| to |aOffset|.
 *     Otherwise, from |aOffset| to |aOffset + aLength|.
 * @param {Window=} window
 *     Window object.  Defaults to the current window.
 *
 * @return         True, if succeeded.  Otherwise false.
 */
event.synthesizeSelectionSet = function (
    offset, length, reverse, window = undefined) {
  let domutils = getDOMWindowUtils(window);
  return domutils.sendSelectionSetEvent(offset, length, reverse);
};

const KEYCODES_LOOKUP = {
  "VK_SHIFT": "shiftKey",
  "VK_CONTROL": "ctrlKey",
  "VK_ALT": "altKey",
  "VK_META": "metaKey",
};

const VIRTUAL_KEYCODE_LOOKUP = {
  "\uE001": "VK_CANCEL",
  "\uE002": "VK_HELP",
  "\uE003": "VK_BACK_SPACE",
  "\uE004": "VK_TAB",
  "\uE005": "VK_CLEAR",
  "\uE006": "VK_RETURN",
  "\uE007": "VK_RETURN",
  "\uE008": "VK_SHIFT",
  "\uE009": "VK_CONTROL",
  "\uE00A": "VK_ALT",
  "\uE03D": "VK_META",
  "\uE00B": "VK_PAUSE",
  "\uE00C": "VK_ESCAPE",
  "\uE00D": "VK_SPACE",  // printable
  "\uE00E": "VK_PAGE_UP",
  "\uE00F": "VK_PAGE_DOWN",
  "\uE010": "VK_END",
  "\uE011": "VK_HOME",
  "\uE012": "VK_LEFT",
  "\uE013": "VK_UP",
  "\uE014": "VK_RIGHT",
  "\uE015": "VK_DOWN",
  "\uE016": "VK_INSERT",
  "\uE017": "VK_DELETE",
  "\uE018": "VK_SEMICOLON",
  "\uE019": "VK_EQUALS",
  "\uE01A": "VK_NUMPAD0",
  "\uE01B": "VK_NUMPAD1",
  "\uE01C": "VK_NUMPAD2",
  "\uE01D": "VK_NUMPAD3",
  "\uE01E": "VK_NUMPAD4",
  "\uE01F": "VK_NUMPAD5",
  "\uE020": "VK_NUMPAD6",
  "\uE021": "VK_NUMPAD7",
  "\uE022": "VK_NUMPAD8",
  "\uE023": "VK_NUMPAD9",
  "\uE024": "VK_MULTIPLY",
  "\uE025": "VK_ADD",
  "\uE026": "VK_SEPARATOR",
  "\uE027": "VK_SUBTRACT",
  "\uE028": "VK_DECIMAL",
  "\uE029": "VK_DIVIDE",
  "\uE031": "VK_F1",
  "\uE032": "VK_F2",
  "\uE033": "VK_F3",
  "\uE034": "VK_F4",
  "\uE035": "VK_F5",
  "\uE036": "VK_F6",
  "\uE037": "VK_F7",
  "\uE038": "VK_F8",
  "\uE039": "VK_F9",
  "\uE03A": "VK_F10",
  "\uE03B": "VK_F11",
  "\uE03C": "VK_F12",
};

function getKeyCode(c) {
  if (c in VIRTUAL_KEYCODE_LOOKUP) {
    return VIRTUAL_KEYCODE_LOOKUP[c];
  }
  return c;
}

event.sendKeyDown = function (keyToSend, modifiers, document) {
  modifiers.type = "keydown";
  event.sendSingleKey(keyToSend, modifiers, document);
  // TODO This doesn't do anything since |synthesizeKeyEvent| ignores explicit
  // keypress request, and instead figures out itself when to send keypress
  if (["VK_SHIFT", "VK_CONTROL", "VK_ALT", "VK_META"].indexOf(getKeyCode(keyToSend)) < 0) {
    modifiers.type = "keypress";
    event.sendSingleKey(keyToSend, modifiers, document);
  }
  delete modifiers.type;
};

event.sendKeyUp = function (keyToSend, modifiers, window = undefined) {
  modifiers.type = "keyup";
  event.sendSingleKey(keyToSend, modifiers, window);
  delete modifiers.type;
};

/**
 * Synthesize a key event for a single key.
 *
 * @param {string} keyToSend
 *     Code point or normalized key value
 * @param {?} modifiers
 *     Object with properties used in KeyboardEvent (shiftkey, repeat, ...)
 *     as well as, the event |type| such as keydown. All properties are optional.
 * @param {Window=} window
 *     Window object.  If |window| is undefined, the event is synthesized in
 *     current window.
 */
event.sendSingleKey = function (keyToSend, modifiers, window = undefined) {
  let keyCode = getKeyCode(keyToSend);
  if (keyCode in KEYCODES_LOOKUP) {
    // We assume that if |keyToSend| is a raw code point (like "\uE009") then
    // |modifiers| does not already have correct value for corresponding
    // |modName| attribute (like ctrlKey), so that value needs to be flipped
    let modName = KEYCODES_LOOKUP[keyCode];
    modifiers[modName] = !modifiers[modName];
  } else if (modifiers.shiftKey && keyCode != "Shift") {
    keyCode = keyCode.toUpperCase();
  }
  event.synthesizeKey(keyCode, modifiers, window);
};

/**
 * Focus element and, if a textual input field and no previous selection
 * state exists, move the caret to the end of the input field.
 *
 * @param {Element} element
 *     Element to focus.
 */
function focusElement(element) {
  let t = element.type;
  if (t && (t == "text" || t == "textarea")) {
    if (element.selectionEnd == 0) {
      let len = element.value.length;
      element.setSelectionRange(len, len);
    }
  }
  element.focus();
}

/**
 * @param {Array.<string>} keySequence
 * @param {Element} element
 * @param {Object.<string, boolean>=} opts
 * @param {Window=} window
 */
event.sendKeysToElement = function (
    keySequence, el, opts = {}, window = undefined) {

  if (opts.ignoreVisibility || element.isVisible(el)) {
    focusElement(el);

    // make Object.<modifier, false> map
    let modifiers = Object.create(event.Modifiers);
    for (let modifier in event.Modifiers) {
      modifiers[modifier] = false;
    }

    let value = keySequence.join("");
    for (let i = 0; i < value.length; i++) {
      let c = value.charAt(i);
      event.sendSingleKey(c, modifiers, window);
    }

  } else {
    throw new ElementNotInteractableError("Element is not visible");
  }
};

event.sendEvent = function (eventType, el, modifiers = {}, opts = {}) {
  opts.canBubble = opts.canBubble || true;

  let doc = el.ownerDocument || el.document;
  let ev = doc.createEvent("Event");

  ev.shiftKey = modifiers["shift"];
  ev.metaKey = modifiers["meta"];
  ev.altKey = modifiers["alt"];
  ev.ctrlKey = modifiers["ctrl"];

  ev.initEvent(eventType, opts.canBubble, true);
  el.dispatchEvent(ev);
};

event.focus = function (el, opts = {}) {
  opts.canBubble = opts.canBubble || true;
  let doc = el.ownerDocument || el.document;
  let win = doc.defaultView;

  let ev = new win.FocusEvent(el);
  ev.initEvent("focus", opts.canBubble, true);
  el.dispatchEvent(ev);
};

event.mouseover = function (el, modifiers = {}, opts = {}) {
  return event.sendEvent("mouseover", el, modifiers, opts);
};

event.mousemove = function (el, modifiers = {}, opts = {}) {
  return event.sendEvent("mousemove", el, modifiers, opts);
};

event.mousedown = function (el, modifiers = {}, opts = {}) {
  return event.sendEvent("mousedown", el, modifiers, opts);
};

event.mouseup = function (el, modifiers = {}, opts = {}) {
  return event.sendEvent("mouseup", el, modifiers, opts);
};

event.click = function (el, modifiers = {}, opts = {}) {
  return event.sendEvent("click", el, modifiers, opts);
};

event.change = function (el, modifiers = {}, opts = {}) {
  return event.sendEvent("change", el, modifiers, opts);
};

event.input = function (el, modifiers = {}, opts = {}) {
  return event.sendEvent("input", el, modifiers, opts);
};
