/**
 * mouse_event_shim.js: generate mouse events from touch events.
 *
 * This library listens for touch events and generates mousedown, mousemove
 * mouseup, and click events to match them. It captures and dicards any
 * real mouse events (non-synthetic events with isTrusted true) that are
 * send by gecko so that there are not duplicates.
 *
 * This library does emit mouseover/mouseout and mouseenter/mouseleave
 * events. You can turn them off by setting MouseEventShim.trackMouseMoves to
 * false. This means that mousemove events will always have the same target
 * as the mousedown even that began the series. You can also call
 * MouseEventShim.setCapture() from a mousedown event handler to prevent
 * mouse tracking until the next mouseup event.
 *
 * This library does not support multi-touch but should be sufficient
 * to do drags based on mousedown/mousemove/mouseup events.
 *
 * This library does not emit dblclick events or contextmenu events
 */

"use strict";

(function () {
  // Make sure we don't run more than once
  if (MouseEventShim) {
    return;
  }

  // Bail if we're not on running on a platform that sends touch
  // events. We don't need the shim code for mouse events.
  try {
    document.createEvent("TouchEvent");
  } catch (e) {
    return;
  }

  let starttouch; // The Touch object that we started with
  let target; // The element the touch is currently over
  let emitclick; // Will we be sending a click event after mouseup?

  // Use capturing listeners to discard all mouse events from gecko
  window.addEventListener("mousedown", discardEvent, true);
  window.addEventListener("mouseup", discardEvent, true);
  window.addEventListener("mousemove", discardEvent, true);
  window.addEventListener("click", discardEvent, true);

  function discardEvent(e) {
    if (e.isTrusted) {
      e.stopImmediatePropagation(); // so it goes no further
      if (e.type === "click") {
        e.preventDefault();
      } // so it doesn't trigger a change event
    }
  }

  // Listen for touch events that bubble up to the window.
  // If other code has called stopPropagation on the touch events
  // then we'll never see them. Also, we'll honor the defaultPrevented
  // state of the event and will not generate synthetic mouse events
  window.addEventListener("touchstart", handleTouchStart);
  window.addEventListener("touchmove", handleTouchMove);
  window.addEventListener("touchend", handleTouchEnd);
  window.addEventListener("touchcancel", handleTouchEnd); // Same as touchend

  function handleTouchStart(e) {
    // If we're already handling a touch, ignore this one
    if (starttouch) {
      return;
    }

    // Ignore any event that has already been prevented
    if (e.defaultPrevented) {
      return;
    }

    // Sometimes an unknown gecko bug causes us to get a touchstart event
    // for an iframe target that we can't use because it is cross origin.
    // Don't start handling a touch in that case
    try {
      e.changedTouches[0].target.ownerDocument;
    } catch (e) {
      // Ignore the event if we can't see the properties of the target
      return;
    }

    // If there is more than one simultaneous touch, ignore all but the first
    starttouch = e.changedTouches[0];
    target = starttouch.target;
    emitclick = true;

    // Move to the position of the touch
    emitEvent("mousemove", target, starttouch);

    // Now send a synthetic mousedown
    let result = emitEvent("mousedown", target, starttouch);

    // If the mousedown was prevented, pass that on to the touch event.
    // And remember not to send a click event
    if (!result) {
      e.preventDefault();
      emitclick = false;
    }
  }

  function handleTouchEnd(e) {
    if (!starttouch) {
      return;
    }

    // End a MouseEventShim.setCapture() call
    if (MouseEventShim.capturing) {
      MouseEventShim.capturing = false;
      MouseEventShim.captureTarget = null;
    }

    for (let i = 0; i < e.changedTouches.length; i++) {
      let touch = e.changedTouches[i];
      // If the ended touch does not have the same id, skip it
      if (touch.identifier !== starttouch.identifier) {
        continue;
      }

      emitEvent("mouseup", target, touch);

      // If target is still the same element we started and the touch did not
      // move more than the threshold and if the user did not prevent
      // the mousedown, then send a click event, too.
      if (emitclick) {
        emitEvent("click", starttouch.target, touch);
      }

      starttouch = null;
      return;
    }
  }

  function handleTouchMove(e) {
    if (!starttouch) {
      return;
    }

    for (let i = 0; i < e.changedTouches.length; i++) {
      let touch = e.changedTouches[i];
      // If the ended touch does not have the same id, skip it
      if (touch.identifier !== starttouch.identifier) {
        continue;
      }

      // Don't send a mousemove if the touchmove was prevented
      if (e.defaultPrevented) {
        return;
      }

      // See if we've moved too much to emit a click event
      let dx = Math.abs(touch.screenX - starttouch.screenX);
      let dy = Math.abs(touch.screenY - starttouch.screenY);
      if (
        dx > MouseEventShim.dragThresholdX ||
        dy > MouseEventShim.dragThresholdY
      ) {
        emitclick = false;
      }

      let tracking =
        MouseEventShim.trackMouseMoves && !MouseEventShim.capturing;

      let oldtarget;
      let newtarget;
      if (tracking) {
        // If the touch point moves, then the element it is over
        // may have changed as well. Note that calling elementFromPoint()
        // forces a layout if one is needed.
        // XXX: how expensive is it to do this on each touchmove?
        // Can we listen for (non-standard) touchleave events instead?
        oldtarget = target;
        newtarget = document.elementFromPoint(touch.clientX, touch.clientY);
        if (newtarget === null) {
          // this can happen as the touch is moving off of the screen, e.g.
          newtarget = oldtarget;
        }
        if (newtarget !== oldtarget) {
          leave(oldtarget, newtarget, touch); // mouseout, mouseleave
          target = newtarget;
        }
      } else if (MouseEventShim.captureTarget) {
        target = MouseEventShim.captureTarget;
      }

      emitEvent("mousemove", target, touch);

      if (tracking && newtarget !== oldtarget) {
        enter(newtarget, oldtarget, touch); // mouseover, mouseenter
      }
    }
  }

  // Return true if element a contains element b
  function contains(a, b) {
    return (a.compareDocumentPosition(b) & 16) !== 0;
  }

  // A touch has left oldtarget and entered newtarget
  // Send out all the events that are required
  function leave(oldtarget, newtarget, touch) {
    emitEvent("mouseout", oldtarget, touch, newtarget);

    // If the touch has actually left oldtarget (and has not just moved
    // into a child of oldtarget) send a mouseleave event. mouseleave
    // events don't bubble, so we have to repeat this up the hierarchy.
    for (let e = oldtarget; !contains(e, newtarget); e = e.parentNode) {
      emitEvent("mouseleave", e, touch, newtarget);
    }
  }

  // A touch has entered newtarget from oldtarget
  // Send out all the events that are required.
  function enter(newtarget, oldtarget, touch) {
    emitEvent("mouseover", newtarget, touch, oldtarget);

    // Emit non-bubbling mouseenter events if the touch actually entered
    // newtarget and wasn't already in some child of it
    for (let e = newtarget; !contains(e, oldtarget); e = e.parentNode) {
      emitEvent("mouseenter", e, touch, oldtarget);
    }
  }

  function emitEvent(type, target, touch, relatedTarget) {
    let synthetic = document.createEvent("MouseEvents");
    let bubbles = type !== "mouseenter" && type !== "mouseleave";
    let count =
      type === "mousedown" || type === "mouseup" || type === "click" ? 1 : 0;

    synthetic.initMouseEvent(
      type,
      bubbles, // canBubble
      true, // cancelable
      window,
      count, // detail: click count
      touch.screenX,
      touch.screenY,
      touch.clientX,
      touch.clientY,
      false, // ctrlKey: we don't have one
      false, // altKey: we don't have one
      false, // shiftKey: we don't have one
      false, // metaKey: we don't have one
      0, // we're simulating the left button
      relatedTarget || null
    );

    try {
      return target.dispatchEvent(synthetic);
    } catch (e) {
      console.warn("Exception calling dispatchEvent", type, e);
      return true;
    }
  }
})();

const MouseEventShim = {
  // It is a known gecko bug that synthetic events have timestamps measured
  // in microseconds while regular events have timestamps measured in
  // milliseconds. This utility function returns a the timestamp converted
  // to milliseconds, if necessary.
  getEventTimestamp(e) {
    if (e.isTrusted) {
      // XXX: Are real events always trusted?
      return e.timeStamp;
    }
    return e.timeStamp / 1000;
  },

  // Set this to false if you don't care about mouseover/out events
  // and don't want the target of mousemove events to follow the touch
  trackMouseMoves: true,

  // Call this function from a mousedown event handler if you want to guarantee
  // that the mousemove and mouseup events will go to the same element
  // as the mousedown even if they leave the bounds of the element. This is
  // like setting trackMouseMoves to false for just one drag. It is a
  // substitute for event.target.setCapture(true)
  setCapture(target) {
    this.capturing = true; // Will be set back to false on mouseup
    if (target) {
      this.captureTarget = target;
    }
  },

  capturing: false,

  // Keep these in sync with ui.dragThresholdX and ui.dragThresholdY prefs.
  // If a touch ever moves more than this many pixels from its starting point
  // then we will not synthesize a click event when the touch ends.
  dragThresholdX: 25,
  dragThresholdY: 25,
};
