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

/* eslint-disable no-restricted-globals */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  keyData: "chrome://remote/content/shared/webdriver/KeyData.sys.mjs",
});

/** Provides functionality for creating and sending DOM events. */
export const event = {};

XPCOMUtils.defineLazyGetter(lazy, "dblclickTimer", () => {
  return Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
});

const _eventUtils = new WeakMap();

function _getEventUtils(win) {
  if (!_eventUtils.has(win)) {
    const eventUtilsObject = {
      window: win,
      parent: win,
      _EU_Ci: Ci,
      _EU_Cc: Cc,
    };
    Services.scriptloader.loadSubScript(
      "chrome://remote/content/external/EventUtils.js",
      eventUtilsObject
    );
    _eventUtils.set(win, eventUtilsObject);
  }
  return _eventUtils.get(win);
}

//  Max interval between two clicks that should result in a dblclick (in ms)
const DBLCLICK_INTERVAL = 640;

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

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

event.MouseButton = {
  isPrimary(button) {
    return button === 0;
  },
  isAuxiliary(button) {
    return button === 1;
  },
  isSecondary(button) {
    return button === 2;
  },
};

event.DoubleClickTracker = {
  firstClick: false,
  isClicked() {
    return event.DoubleClickTracker.firstClick;
  },
  setClick() {
    if (!event.DoubleClickTracker.firstClick) {
      event.DoubleClickTracker.firstClick = true;
      event.DoubleClickTracker.startTimer();
    }
  },
  resetClick() {
    event.DoubleClickTracker.firstClick = false;
    event.DoubleClickTracker.cancelTimer();
  },
  startTimer() {
    lazy.dblclickTimer.initWithCallback(
      event.DoubleClickTracker.resetClick,
      DBLCLICK_INTERVAL,
      Ci.nsITimer.TYPE_ONE_SHOT
    );
  },
  cancelTimer() {
    lazy.dblclickTimer.cancel();
  },
};

/**
 * Synthesise a mouse event at a point.
 *
 * If the type is specified in opts, an mouse event of that type is
 * fired. Otherwise, a mousedown followed by a mouseup is performed.
 *
 * @param {number} left
 *     Offset from viewport left, in CSS pixels
 * @param {number} top
 *     Offset from viewport top, in CSS pixels
 * @param {object} opts
 *     Object which may contain the properties "shiftKey", "ctrlKey",
 *     "altKey", "metaKey", "accessKey", "clickCount", "button", and
 *     "type".
 * @param {Window} win
 *     Window object.
 *
 * @returns {boolean} defaultPrevented
 */
event.synthesizeMouseAtPoint = function (left, top, opts, win) {
  return _getEventUtils(win).synthesizeMouseAtPoint(left, top, opts, win);
};

/**
 * Synthesise a touch event at a point.
 *
 * If the type is specified in opts, a touch event of that type is
 * fired. Otherwise, a touchstart followed by a touchend is performed.
 *
 * @param {number} left
 *     Offset from viewport left, in CSS pixels
 * @param {number} top
 *     Offset from viewport top, in CSS pixels
 * @param {object} opts
 *     Object which may contain the properties "id", "rx", "ry", "angle",
 *     "force", "shiftKey", "ctrlKey", "altKey", "metaKey", "accessKey",
 *     "type".
 * @param {Window} win
 *     Window object.
 *
 * @returns {boolean} defaultPrevented
 */
event.synthesizeTouchAtPoint = function (left, top, opts, win) {
  return _getEventUtils(win).synthesizeTouchAtPoint(left, top, opts, win);
};

/**
 * Synthesise a wheel scroll event at a point.
 *
 * @param {number} left
 *     Offset from viewport left, in CSS pixels
 * @param {number} top
 *     Offset from viewport top, in CSS pixels
 * @param {object} opts
 *     Object which may contain the properties "shiftKey", "ctrlKey",
 *     "altKey", "metaKey", "accessKey", "deltaX", "deltaY", "deltaZ",
 *     "deltaMode", "lineOrPageDeltaX", "lineOrPageDeltaY", "isMomentum",
 *     "isNoLineOrPageDelta", "isCustomizedByPrefs", "expectedOverflowDeltaX",
 *     "expectedOverflowDeltaY"
 * @param {Window} win
 *     Window object.
 */
event.synthesizeWheelAtPoint = function (left, top, opts, win) {
  return _getEventUtils(win).synthesizeWheelAtPoint(left, top, opts, win);
};

event.synthesizeMultiTouch = function (opts, win) {
  const modifiers = _getEventUtils(win)._parseModifiers(opts);
  win.windowUtils.sendTouchEvent(
    opts.type,
    opts.id,
    opts.x,
    opts.y,
    opts.rx,
    opts.ry,
    opts.angle,
    opts.force,
    opts.tiltx,
    opts.tilty,
    opts.twist,
    modifiers
  );
};

/**
 * Synthesize a keydown event for a single key.
 *
 * @param {object} key
 *     Key data as returned by keyData.getData
 * @param {Window} win
 *     Window object.
 */
event.sendKeyDown = function (key, win) {
  event.sendSingleKey(key, win, "keydown");
};

/**
 * Synthesize a keyup event for a single key.
 *
 * @param {object} key
 *     Key data as returned by keyData.getData
 * @param {Window} win
 *     Window object.
 */
event.sendKeyUp = function (key, win) {
  event.sendSingleKey(key, win, "keyup");
};

/**
 * Synthesize a key event for a single key.
 *
 * @param {object} key
 *     Key data as returned by keyData.getData
 * @param {Window} win
 *     Window object.
 * @param {string=} type
 *     Event to emit. By default the full keydown/keypressed/keyup event
 *     sequence is emitted.
 */
event.sendSingleKey = function (key, win, type = null) {
  let keyValue = key.key;
  if (!key.printable) {
    keyValue = `KEY_${keyValue}`;
  }
  const event = {
    code: key.code,
    location: key.location,
    altKey: key.altKey ?? false,
    shiftKey: key.shiftKey ?? false,
    ctrlKey: key.ctrlKey ?? false,
    metaKey: key.metaKey ?? false,
    repeat: key.repeat ?? false,
  };
  if (type) {
    event.type = type;
  }
  _getEventUtils(win).synthesizeKey(keyValue, event, win);
};

/**
 * Send a string as a series of keypresses.
 *
 * @param {string} keyString
 *     Sequence of characters to send as key presses
 * @param {Window} win
 *     Window object
 */
event.sendKeys = function (keyString, win) {
  const modifiers = {};
  for (let modifier in event.Modifiers) {
    modifiers[modifier] = false;
  }

  for (let i = 0; i < keyString.length; i++) {
    let keyValue = keyString.charAt(i);
    if (modifiers.shiftKey) {
      keyValue = lazy.keyData.getShiftedKey(keyValue);
    }
    const data = lazy.keyData.getData(keyValue);
    const key = { ...data, ...modifiers };
    if (data.modifier) {
      // Negating the state of the modifier here is not spec compliant but
      // makes us compatible to Chrome's behavior for now. That's fine unless
      // we know the correct behavior.
      //
      // @see: https://github.com/w3c/webdriver/issues/1734
      modifiers[data.modifier] = !modifiers[data.modifier];
    }
    event.sendSingleKey(key, win);
  }
};

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.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);
};
