// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; js2-strict-trailing-comma-warning: nil -*-
/* 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/. */

Components.utils.import("resource://gre/modules/Geometry.jsm");

/*
 * Drag scrolling related constants
 */

// maximum drag distance in inches while axis locking can still be reverted
const kAxisLockRevertThreshold = 0.8;

// Same as NS_EVENT_STATE_ACTIVE from mozilla/EventStates.h
const kStateActive = 0x00000001;

// After a drag begins, kinetic panning is stopped if the drag doesn't become
// a pan in 300 milliseconds.
const kStopKineticPanOnDragTimeout = 300;

// Min/max velocity of kinetic panning. This is in pixels/millisecond.
const kMinVelocity = 0.4;
const kMaxVelocity = 6;

/*
 * prefs
 */

// Display rects around selection ranges. Useful in debugging
// selection problems.
const kDebugSelectionDisplayPref = "metro.debug.selection.displayRanges";
// Dump range rect data to the console. Very useful, but also slows
// things down a lot.
const kDebugSelectionDumpPref = "metro.debug.selection.dumpRanges";
// Dump message manager event traffic for selection.
const kDebugSelectionDumpEvents = "metro.debug.selection.dumpEvents";
const kAsyncPanZoomEnabled = "layers.async-pan-zoom.enabled"

/**
 * TouchModule
 *
 * Handles all touch-related input such as dragging and tapping.
 *
 * The Fennec chrome DOM tree has elements that are augmented dynamically with
 * custom JS properties that tell the TouchModule they have custom support for
 * either dragging or clicking.  These JS properties are JS objects that expose
 * an interface supporting dragging or clicking (though currently we only look
 * to drag scrollable elements).
 *
 * A custom dragger is a JS property that lives on a scrollable DOM element,
 * accessible as myElement.customDragger.  The customDragger must support the
 * following interface:  (The `scroller' argument is given for convenience, and
 * is the object reference to the element's scrollbox object).
 *
 *   dragStart(cX, cY, target, scroller)
 *     Signals the beginning of a drag.  Coordinates are passed as
 *     client coordinates. target is copied from the event.
 *
 *   dragStop(dx, dy, scroller)
 *     Signals the end of a drag.  The dx, dy parameters may be non-zero to
 *     indicate one last drag movement.
 *
 *   dragMove(dx, dy, scroller, isKinetic)
 *     Signals an input attempt to drag by dx, dy.
 *
 * There is a default dragger in case a scrollable element is dragged --- see
 * the defaultDragger prototype property.
 */

var TouchModule = {
  _debugEvents: false,
  _isCancelled: false,
  _isCancellable: false,

  init: function init() {
    this._dragData = new DragData();

    this._dragger = null;

    this._targetScrollbox = null;
    this._targetScrollInterface = null;

    this._kinetic = new KineticController(this._dragBy.bind(this),
                                          this._kineticStop.bind(this));

    // capture phase events
    window.addEventListener("CancelTouchSequence", this, true);
    window.addEventListener("keydown", this, true);
    window.addEventListener("MozMouseHittest", this, true);

    // bubble phase
    window.addEventListener("contextmenu", this, false);
    window.addEventListener("touchstart", this, false);
    window.addEventListener("touchmove", this, false);
    window.addEventListener("touchend", this, false);

    Services.obs.addObserver(this, "Gesture:SingleTap", false);
    Services.obs.addObserver(this, "Gesture:DoubleTap", false);
  },

  /*
   * Events
   */

  handleEvent: function handleEvent(aEvent) {
    switch (aEvent.type) {
      case "contextmenu":
        this._onContextMenu(aEvent);
        break;

      case "CancelTouchSequence":
        this.cancelPending();
        break;

      default: {
        if (this._debugEvents) {
          if (aEvent.type != "touchmove")
            Util.dumpLn("TouchModule:", aEvent.type, aEvent.target);
        }

        switch (aEvent.type) {
          case "touchstart":
            this._onTouchStart(aEvent);
            break;
          case "touchmove":
            this._onTouchMove(aEvent);
            break;
          case "touchend":
            this._onTouchEnd(aEvent);
            break;
          case "keydown":
            this._handleKeyDown(aEvent);
            break;
          case "MozMouseHittest":
            // Used by widget to hit test chrome vs content. Make sure the XUl scrollbars
            // are counted as "chrome". Since the XUL scrollbars have sub-elements we walk
            // the parent chain to ensure we catch all of those as well.
            let onScrollbar = false;
            for (let node = aEvent.originalTarget; node instanceof XULElement; node = node.parentNode) {
              if (node.tagName == 'scrollbar') {
                onScrollbar = true;
                break;
              }
            }
            if (onScrollbar || aEvent.target.ownerDocument == document) {
              aEvent.preventDefault();
            }
            aEvent.stopPropagation();
            break;
        }
      }
    }
  },

  _handleKeyDown: function _handleKeyDown(aEvent) {
    const TABKEY = 9;
    if (aEvent.keyCode == TABKEY && !InputSourceHelper.isPrecise) {
      if (Util.isEditable(aEvent.target) &&
          aEvent.target.selectionStart != aEvent.target.selectionEnd) {
        SelectionHelperUI.closeEditSession(false);
      }
      setTimeout(function() {
        let element = Browser.selectedBrowser.contentDocument.activeElement;
        // We only want to attach monocles if we have an input, text area,
        // there is selection, and the target element changed.
        // Sometimes the target element won't change even though selection is
        // cleared because of focus outside the browser.
        if (Util.isEditable(element) &&
            !SelectionHelperUI.isActive &&
            element.selectionStart != element.selectionEnd &&
            // not e10s friendly
            aEvent.target != element) {
              let rect = element.getBoundingClientRect();
              SelectionHelperUI.attachEditSession(Browser.selectedBrowser,
                                                  rect.left + rect.width / 2,
                                                  rect.top + rect.height / 2);
        }
      }, 50);
    }
  },

  observe: function BrowserUI_observe(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "Gesture:SingleTap":
      case "Gesture:DoubleTap":
        Browser.selectedBrowser.messageManager.sendAsyncMessage(aTopic, JSON.parse(aData));
        break;
    }
  },


  sample: function sample(aTimeStamp) {
    this._waitingForPaint = false;
  },

  /**
   * This gets invoked by the input handler if another module grabs.  We should
   * reset our state or something here.  This is probably doing the wrong thing
   * in its current form.
   */
  cancelPending: function cancelPending() {
    this._doDragStop();

    // Kinetic panning may have already been active or drag stop above may have
    // made kinetic panning active.
    this._kinetic.end();

    this._targetScrollbox = null;
    this._targetScrollInterface = null;
  },

  _onContextMenu: function _onContextMenu(aEvent) {
    // bug 598965 - chrome UI should stop to be pannable once the
    // context menu has appeared.
    if (ContextMenuUI.popupState) {
      this.cancelPending();
    }
  },

  /** Begin possible pan and send tap down event. */
  _onTouchStart: function _onTouchStart(aEvent) {
    if (aEvent.touches.length > 1)
      return;

    this._isCancelled = false;
    this._isCancellable = true;

    if (aEvent.defaultPrevented) {
      this._isCancelled = true;
      return;
    }

    let dragData = this._dragData;
    if (dragData.dragging) {
      // Somehow a mouse up was missed.
      this._doDragStop();
    }
    dragData.reset();
    this.dX = 0;
    this.dY = 0;

    // walk up the DOM tree in search of nearest scrollable ancestor.  nulls are
    // returned if none found.
    let [targetScrollbox, targetScrollInterface, dragger]
      = ScrollUtils.getScrollboxFromElement(aEvent.originalTarget);

    // stop kinetic panning if targetScrollbox has changed
    if (this._kinetic.isActive() && this._dragger != dragger)
      this._kinetic.end();

    this._targetScrollbox = targetScrollInterface ? targetScrollInterface.element : targetScrollbox;
    this._targetScrollInterface = targetScrollInterface;

    if (!this._targetScrollbox) {
      return;
    }

    // Don't allow kinetic panning if APZC is enabled and the pan element is the deck
    let deck = document.getElementById("browsers");
    if (Services.prefs.getBoolPref(kAsyncPanZoomEnabled) &&
        this._targetScrollbox == deck) {
      return;
    }

    // XXX shouldn't dragger always be valid here?
    if (dragger) {
      let draggable = dragger.isDraggable(targetScrollbox, targetScrollInterface);
      dragData.locked = !draggable.x || !draggable.y;
      if (draggable.x || draggable.y) {
        this._dragger = dragger;
        if (dragger.freeDrag)
          dragData.alwaysFreeDrag = dragger.freeDrag();
        this._doDragStart(aEvent, draggable);
      }
    }
  },

  /** Send tap up event and any necessary full taps. */
  _onTouchEnd: function _onTouchEnd(aEvent) {
    if (aEvent.touches.length > 0 || this._isCancelled || !this._targetScrollbox)
      return;

    // onMouseMove will not record the delta change if we are waiting for a
    // paint. Since this is the last input for this drag, we override the flag.
    this._waitingForPaint = false;
    this._onTouchMove(aEvent);

    let dragData = this._dragData;
    this._doDragStop();
  },

  /**
   * If we're in a drag, do what we have to do to drag on.
   */
  _onTouchMove: function _onTouchMove(aEvent) {
    if (aEvent.touches.length > 1)
      return;

    if (this._isCancellable) {
      // only the first touchmove is cancellable.
      this._isCancellable = false;
      if (aEvent.defaultPrevented) {
        this._isCancelled = true;
      }
    }

    if (this._isCancelled)
      return;

    let touch = aEvent.changedTouches[0];
    if (!this._targetScrollbox) {
      return;
    }

    let dragData = this._dragData;

    if (dragData.dragging) {
      let oldIsPan = dragData.isPan();
      dragData.setDragPosition(touch.screenX, touch.screenY);
      dragData.setMousePosition(touch);

      // Kinetic panning is sensitive to time. It is more stable if it receives
      // the mousemove events as they come. For dragging though, we only want
      // to call _dragBy if we aren't waiting for a paint (so we don't spam the
      // main browser loop with a bunch of redundant paints).
      //
      // Here, we feed kinetic panning drag differences for mouse events as
      // come; for dragging, we build up a drag buffer in this.dX/this.dY and
      // release it when we are ready to paint.
      //
      let [sX, sY] = dragData.panPosition();
      this.dX += dragData.prevPanX - sX;
      this.dY += dragData.prevPanY - sY;

      if (dragData.isPan()) {
        // Only pan when mouse event isn't part of a click. Prevent jittering on tap.
        this._kinetic.addData(sX - dragData.prevPanX, sY - dragData.prevPanY);

        // dragBy will reset dX and dY values to 0
        this._dragBy(this.dX, this.dY);

        // Let everyone know when mousemove begins a pan
        if (!oldIsPan && dragData.isPan()) {
          let event = document.createEvent("Events");
          event.initEvent("PanBegin", true, false);
          this._targetScrollbox.dispatchEvent(event);

          Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:PanBegin", {});
        }
      }
    }
  },

  /**
   * Inform our dragger of a dragStart.
   */
  _doDragStart: function _doDragStart(aEvent, aDraggable) {
    let touch = aEvent.changedTouches[0];
    let dragData = this._dragData;
    dragData.setDragStart(touch.screenX, touch.screenY, aDraggable);
    this._kinetic.addData(0, 0);
    this._dragStartTime = Date.now();
    if (!this._kinetic.isActive()) {
      this._dragger.dragStart(touch.clientX, touch.clientY, touch.target, this._targetScrollInterface);
    }
  },

  /** Finish a drag. */
  _doDragStop: function _doDragStop() {
    let dragData = this._dragData;
    if (!dragData.dragging)
      return;

    dragData.endDrag();

    // Note: it is possible for kinetic scrolling to be active from a
    // mousedown/mouseup event previous to this one. In this case, we
    // want the kinetic panner to tell our drag interface to stop.

    if (dragData.isPan()) {
      if (Date.now() - this._dragStartTime > kStopKineticPanOnDragTimeout)
        this._kinetic._velocity.set(0, 0);

      // Start kinetic pan if we aren't using async pan zoom or the scroll
      // element is not browsers.
      let deck = document.getElementById("browsers");
      if (!Services.prefs.getBoolPref(kAsyncPanZoomEnabled) ||
          this._targetScrollbox != deck) {
        this._kinetic.start();
      }
    } else {
      this._kinetic.end();
      if (this._dragger)
        this._dragger.dragStop(0, 0, this._targetScrollInterface);
      this._dragger = null;
    }
  },

  /**
   * Used by _onTouchMove() above and by KineticController's timer to do the
   * actual dragMove signalling to the dragger.  We'd put this in _onTouchMove()
   * but then KineticController would be adding to its own data as it signals
   * the dragger of dragMove()s.
   */
  _dragBy: function _dragBy(dX, dY, aIsKinetic) {
    let dragged = true;
    let dragData = this._dragData;
    if (!this._waitingForPaint || aIsKinetic) {
      let dragData = this._dragData;
      dragged = this._dragger.dragMove(dX, dY, this._targetScrollInterface, aIsKinetic,
                                       dragData._mouseX, dragData._mouseY);
      if (dragged && !this._waitingForPaint) {
        this._waitingForPaint = true;
        mozRequestAnimationFrame(this);
      }
      this.dX = 0;
      this.dY = 0;
    }
    if (!dragData.isPan())
      this._kinetic.pause();

    return dragged;
  },

  /** Callback for kinetic scroller. */
  _kineticStop: function _kineticStop() {
    // Kinetic panning could finish while user is panning, so don't finish
    // the pan just yet.
    let dragData = this._dragData;
    if (!dragData.dragging) {
      if (this._dragger)
        this._dragger.dragStop(0, 0, this._targetScrollInterface);
      this._dragger = null;

      let event = document.createEvent("Events");
      event.initEvent("PanFinished", true, false);
      this._targetScrollbox.dispatchEvent(event);
    }
  },

  toString: function toString() {
    return '[TouchModule] {'
      + '\n\tdragData=' + this._dragData + ', '
      + 'dragger=' + this._dragger + ', '
      + '\n\ttargetScroller=' + this._targetScrollInterface + '}';
  },
};

var ScrollUtils = {
  // threshold in pixels for sensing a tap as opposed to a pan
  get tapRadius() {
    let dpi = Util.displayDPI;
    delete this.tapRadius;
    return this.tapRadius = Services.prefs.getIntPref("ui.dragThresholdX") / 240 * dpi;
  },

  /**
   * Walk up (parentward) the DOM tree from elem in search of a scrollable element.
   * Return the element and its scroll interface if one is found, two nulls otherwise.
   *
   * This function will cache the pointer to the scroll interface on the element itself,
   * so it is safe to call it many times without incurring the same XPConnect overhead
   * as in the initial call.
   */
  getScrollboxFromElement: function getScrollboxFromElement(elem) {
    let scrollbox = null;
    let qinterface = null;

    // if element is content or the startui page, get the browser scroll interface
    if (elem.ownerDocument == Browser.selectedBrowser.contentDocument) {
      elem = Browser.selectedBrowser;
    }
    for (; elem; elem = elem.parentNode) {
      try {
        if (elem.anonScrollBox) {
          scrollbox = elem.anonScrollBox;
          qinterface = scrollbox.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
        } else if (elem.scrollBoxObject) {
          scrollbox = elem;
          qinterface = elem.scrollBoxObject;
          break;
        } else if (elem.customDragger) {
          scrollbox = elem;
          break;
        } else if (elem.boxObject) {
          let qi = (elem._cachedSBO) ? elem._cachedSBO
                                     : elem.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
          if (qi) {
            scrollbox = elem;
            scrollbox._cachedSBO = qinterface = qi;
            break;
          }
        }
      } catch (e) { /* we aren't here to deal with your exceptions, we'll just keep
                       traversing until we find something more well-behaved, as we
                       prefer default behaviour to whiny scrollers. */ }
    }
    return [scrollbox, qinterface, (scrollbox ? (scrollbox.customDragger || this._defaultDragger) : null)];
  },

  /** Determine if the distance moved can be considered a pan */
  isPan: function isPan(aPoint, aPoint2) {
    return (Math.abs(aPoint.x - aPoint2.x) > this.tapRadius ||
            Math.abs(aPoint.y - aPoint2.y) > this.tapRadius);
  },

  /**
   * The default dragger object used by TouchModule when dragging a scrollable
   * element that provides no customDragger.  Simply performs the expected
   * regular scrollBy calls on the scroller.
   */
  _defaultDragger: {
    isDraggable: function isDraggable(target, scroller) {
      let sX = {}, sY = {},
          pX = {}, pY = {};
      scroller.getPosition(pX, pY);
      scroller.getScrolledSize(sX, sY);
      let rect = target.getBoundingClientRect();
      return { x: (sX.value > rect.width  || pX.value != 0),
               y: (sY.value > rect.height || pY.value != 0) };
    },

    dragStart: function dragStart(cx, cy, target, scroller) {
      scroller.element.addEventListener("PanBegin", this._showScrollbars, false);
    },

    dragStop: function dragStop(dx, dy, scroller) {
      scroller.element.removeEventListener("PanBegin", this._showScrollbars, false);
      return this.dragMove(dx, dy, scroller);
    },

    dragMove: function dragMove(dx, dy, scroller) {
      if (scroller.getPosition) {
        try {
          let oldX = {}, oldY = {};
          scroller.getPosition(oldX, oldY);

          scroller.scrollBy(dx, dy);

          let newX = {}, newY = {};
          scroller.getPosition(newX, newY);

          return (newX.value != oldX.value) || (newY.value != oldY.value);

        } catch (e) { /* we have no time for whiny scrollers! */ }
      }

      return false;
    },

    _showScrollbars: function _showScrollbars(aEvent) {
      let scrollbox = aEvent.target;
      scrollbox.setAttribute("panning", "true");

      let hideScrollbars = function() {
        scrollbox.removeEventListener("PanFinished", hideScrollbars, false);
        scrollbox.removeEventListener("CancelTouchSequence", hideScrollbars, false);
        scrollbox.removeAttribute("panning");
      }

      // Wait for panning to be completely finished before removing scrollbars
      scrollbox.addEventListener("PanFinished", hideScrollbars, false);
      scrollbox.addEventListener("CancelTouchSequence", hideScrollbars, false);
    }
  }
};

/**
 * DragData handles processing drags on the screen, handling both
 * locking of movement on one axis, and click detection.
 */
function DragData() {
  this._domUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
  this._lockRevertThreshold = Util.displayDPI * kAxisLockRevertThreshold;
  this.reset();
};

DragData.prototype = {
  reset: function reset() {
    this.dragging = false;
    this.sX = null;
    this.sY = null;
    this.locked = false;
    this.stayLocked = false;
    this.alwaysFreeDrag = false;
    this.lockedX = null;
    this.lockedY = null;
    this._originX = null;
    this._originY = null;
    this.prevPanX = null;
    this.prevPanY = null;
    this._isPan = false;
  },

  /** Depending on drag data, locks sX,sY to X-axis or Y-axis of start position. */
  _lockAxis: function _lockAxis(sX, sY) {
    if (this.locked) {
      if (this.lockedX !== null)
        sX = this.lockedX;
      else if (this.lockedY !== null)
        sY = this.lockedY;
      return [sX, sY];
    }
    else {
      return [this._originX, this._originY];
    }
  },

  setMousePosition: function setMousePosition(aEvent) {
    this._mouseX = aEvent.clientX;
    this._mouseY = aEvent.clientY;
  },

  setDragPosition: function setDragPosition(sX, sY) {
    // Check if drag is now a pan.
    if (!this._isPan) {
      this._isPan = ScrollUtils.isPan(new Point(this._originX, this._originY), new Point(sX, sY));
      if (this._isPan) {
        this._resetActive();
      }
    }

    // If now a pan, mark previous position where panning was.
    if (this._isPan) {
      let absX = Math.abs(this._originX - sX);
      let absY = Math.abs(this._originY - sY);

      if (absX > this._lockRevertThreshold || absY > this._lockRevertThreshold)
        this.stayLocked = true;

      // After the first lock, see if locking decision should be reverted.
      if (!this.stayLocked) {
        if (this.lockedX && absX > 3 * absY)
          this.lockedX = null;
        else if (this.lockedY && absY > 3 * absX)
          this.lockedY = null;
      }

      if (!this.locked) {
        // look at difference from origin coord to lock movement, but only
        // do it if initial movement is sufficient to detect intent

        // divide possibilty space into eight parts.  Diagonals will allow
        // free movement, while moving towards a cardinal will lock that
        // axis.  We pick a direction if you move more than twice as far
        // on one axis than another, which should be an angle of about 30
        // degrees from the axis

        if (absX > 2.5 * absY)
          this.lockedY = sY;
        else if (absY > absX)
          this.lockedX = sX;

        this.locked = true;
      }
    }

    // Never lock if the dragger requests it
    if (this.alwaysFreeDrag) {
      this.lockedY = null;
      this.lockedX = null;
    }

    // After pan lock, figure out previous panning position. Base it on last drag
    // position so there isn't a jump in panning.
    let [prevX, prevY] = this._lockAxis(this.sX, this.sY);
    this.prevPanX = prevX;
    this.prevPanY = prevY;

    this.sX = sX;
    this.sY = sY;
  },

  setDragStart: function setDragStart(screenX, screenY, aDraggable) {
    this.sX = this._originX = screenX;
    this.sY = this._originY = screenY;
    this.dragging = true;

    // If the target area is pannable only in one direction lock it early
    // on the right axis
    this.lockedX = !aDraggable.x ? screenX : null;
    this.lockedY = !aDraggable.y ? screenY : null;
    this.stayLocked = this.lockedX || this.lockedY;
    this.locked = this.stayLocked;
  },

  endDrag: function endDrag() {
    this._resetActive();
    this.dragging = false;
  },

  /** Returns true if drag should pan scrollboxes.*/
  isPan: function isPan() {
    return this._isPan;
  },

  /** Return true if drag should be parsed as a click. */
  isClick: function isClick() {
    return !this._isPan;
  },

  /**
   * Returns the screen position for a pan. This factors in axis locking.
   * @return Array of screen X and Y coordinates
   */
  panPosition: function panPosition() {
    return this._lockAxis(this.sX, this.sY);
  },

  /** dismiss the active state of the pan element */
  _resetActive: function _resetActive() {
    let target = document.documentElement;
    // If the target is active, toggle (turn off) the active flag. Otherwise do nothing.
    if (this._domUtils.getContentState(target) & kStateActive)
      this._domUtils.setContentState(target, kStateActive);
  },

  toString: function toString() {
    return '[DragData] { sX,sY=' + this.sX + ',' + this.sY + ', dragging=' + this.dragging + ' }';
  }
};


/**
 * KineticController - a class to take drag position data and use it
 * to do kinetic panning of a scrollable object.
 *
 * aPanBy is a function that will be called with the dx and dy
 * generated by the kinetic algorithm.  It should return true if the
 * object was panned, false if there was no movement.
 *
 * There are two complicated things done here.  One is calculating the
 * initial velocity of the movement based on user input.  Two is
 * calculating the distance to move every frame.
 */
function KineticController(aPanBy, aEndCallback) {
  this._panBy = aPanBy;
  this._beforeEnd = aEndCallback;

  // These are used to calculate the position of the scroll panes during kinetic panning. Think of
  // these points as vectors that are added together and multiplied by scalars.
  this._position = new Point(0, 0);
  this._velocity = new Point(0, 0);
  this._acceleration = new Point(0, 0);
  this._time = 0;
  this._timeStart = 0;

  // How often do we change the position of the scroll pane?  Too often and panning may jerk near
  // the end. Too little and panning will be choppy. In milliseconds.
  this._updateInterval = Services.prefs.getIntPref("browser.ui.kinetic.updateInterval");
  // Constants that affect the "friction" of the scroll pane.
  this._exponentialC = Services.prefs.getIntPref("browser.ui.kinetic.exponentialC");
  this._polynomialC = Services.prefs.getIntPref("browser.ui.kinetic.polynomialC") / 1000000;
  // Number of milliseconds that can contain a swipe. Movements earlier than this are disregarded.
  this._swipeLength = Services.prefs.getIntPref("browser.ui.kinetic.swipeLength");

  this._reset();
}

KineticController.prototype = {
  _reset: function _reset() {
    this._active = false;
    this._paused = false;
    this.momentumBuffer = [];
    this._velocity.set(0, 0);
  },

  isActive: function isActive() {
    return this._active;
  },

  _startTimer: function _startTimer() {
    let self = this;

    let lastp = this._position;  // track last position vector because pan takes deltas
    let v0 = this._velocity;  // initial velocity
    let a = this._acceleration;  // acceleration
    let c = this._exponentialC;
    let p = new Point(0, 0);
    let dx, dy, t, realt;

    function calcP(v0, a, t) {
      // Important traits for this function:
      //   p(t=0) is 0
      //   p'(t=0) is v0
      //
      // We use exponential to get a smoother stop, but by itself exponential
      // is too smooth at the end. Adding a polynomial with the appropriate
      // weight helps to balance
      return v0 * Math.exp(-t / c) * -c + a * t * t + v0 * c;
    }

    this._calcV = function(v0, a, t) {
      return v0 * Math.exp(-t / c) + 2 * a * t;
    }

    let callback = {
      sample: function kineticHandleEvent(timeStamp) {
        // Someone called end() on us between timer intervals
        // or we are paused.
        if (!self.isActive() || self._paused)
          return;

        // To make animation end fast enough but to keep smoothness, average the ideal
        // time frame (smooth animation) with the actual time lapse (end fast enough).
        // Animation will never take longer than 2 times the ideal length of time.
        realt = timeStamp - self._initialTime;
        self._time += self._updateInterval;
        t = (self._time + realt) / 2;

        // Calculate new position.
        p.x = calcP(v0.x, a.x, t);
        p.y = calcP(v0.y, a.y, t);
        dx = Math.round(p.x - lastp.x);
        dy = Math.round(p.y - lastp.y);

        // Test to see if movement is finished for each component.
        if (dx * a.x > 0) {
          dx = 0;
          lastp.x = 0;
          v0.x = 0;
          a.x = 0;
        }
        // Symmetric to above case.
        if (dy * a.y > 0) {
          dy = 0;
          lastp.y = 0;
          v0.y = 0;
          a.y = 0;
        }

        if (v0.x == 0 && v0.y == 0) {
          self.end();
        } else {
          let panStop = false;
          if (dx != 0 || dy != 0) {
            try { panStop = !self._panBy(-dx, -dy, true); } catch (e) {}
            lastp.add(dx, dy);
          }

          if (panStop)
            self.end();
          else
            mozRequestAnimationFrame(this);
        }
      }
    };

    this._active = true;
    this._paused = false;
    mozRequestAnimationFrame(callback);
  },

  start: function start() {
    function sign(x) {
      return x ? ((x > 0) ? 1 : -1) : 0;
    }

    function clampFromZero(x, closerToZero, furtherFromZero) {
      if (x >= 0)
        return Math.max(closerToZero, Math.min(furtherFromZero, x));
      return Math.min(-closerToZero, Math.max(-furtherFromZero, x));
    }

    let mb = this.momentumBuffer;
    let mblen = this.momentumBuffer.length;

    let lastTime = mb[mblen - 1].t;
    let distanceX = 0;
    let distanceY = 0;
    let swipeLength = this._swipeLength;

    // determine speed based on recorded input
    let me;
    for (let i = 0; i < mblen; i++) {
      me = mb[i];
      if (lastTime - me.t < swipeLength) {
        distanceX += me.dx;
        distanceY += me.dy;
      }
    }

    let currentVelocityX = 0;
    let currentVelocityY = 0;

    if (this.isActive()) {
      // If active, then we expect this._calcV to be defined.
      let currentTime = Date.now() - this._initialTime;
      currentVelocityX = Util.clamp(this._calcV(this._velocity.x, this._acceleration.x, currentTime), -kMaxVelocity, kMaxVelocity);
      currentVelocityY = Util.clamp(this._calcV(this._velocity.y, this._acceleration.y, currentTime), -kMaxVelocity, kMaxVelocity);
    }

    if (currentVelocityX * this._velocity.x <= 0)
      currentVelocityX = 0;
    if (currentVelocityY * this._velocity.y <= 0)
      currentVelocityY = 0;

    let swipeTime = Math.min(swipeLength, lastTime - mb[0].t);
    this._velocity.x = clampFromZero((distanceX / swipeTime) + currentVelocityX, Math.abs(currentVelocityX), kMaxVelocity);
    this._velocity.y = clampFromZero((distanceY / swipeTime) + currentVelocityY, Math.abs(currentVelocityY), kMaxVelocity);

    if (Math.abs(this._velocity.x) < kMinVelocity)
      this._velocity.x = 0;
    if (Math.abs(this._velocity.y) < kMinVelocity)
      this._velocity.y = 0;

    // Set acceleration vector to opposite signs of velocity
    this._acceleration.set(this._velocity.clone().map(sign).scale(-this._polynomialC));

    this._position.set(0, 0);
    this._initialTime = mozAnimationStartTime;
    this._time = 0;
    this.momentumBuffer = [];

    if (!this.isActive() || this._paused)
      this._startTimer();

    return true;
  },

  pause: function pause() {
    this._paused = true;
  },

  end: function end() {
    if (this.isActive()) {
      if (this._beforeEnd)
        this._beforeEnd();
      this._reset();
    }
  },

  addData: function addData(dx, dy) {
    let mbLength = this.momentumBuffer.length;
    let now = Date.now();

    if (this.isActive()) {
      // Stop active movement when dragging in other direction.
      if (dx * this._velocity.x < 0 || dy * this._velocity.y < 0)
        this.end();
    }

    this.momentumBuffer.push({'t': now, 'dx' : dx, 'dy' : dy});
  }
};


/*
 * Simple gestures support
 */

var GestureModule = {
  _debugEvents: false,

  init: function init() {
    window.addEventListener("MozSwipeGesture", this, true);
  },

  /*
   * Events
   *
   * Dispatch events based on the type of mouse gesture event. For now, make
   * sure to stop propagation of every gesture event so that web content cannot
   * receive gesture events.
   *
   * @param nsIDOMEvent information structure
   */

  handleEvent: function handleEvent(aEvent) {
    try {
      aEvent.stopPropagation();
      aEvent.preventDefault();
      if (this._debugEvents) Util.dumpLn("GestureModule:", aEvent.type);
      switch (aEvent.type) {
        case "MozSwipeGesture":
          if (this._onSwipe(aEvent)) {
            let event = document.createEvent("Events");
            event.initEvent("CancelTouchSequence", true, true);
            aEvent.target.dispatchEvent(event);
          }
          break;
      }
    } catch (e) {
      Util.dumpLn("Error while handling gesture event", aEvent.type,
                  "\nPlease report error at:", e);
      Cu.reportError(e);
    }
  },

  _onSwipe: function _onSwipe(aEvent) {
    switch (aEvent.direction) {
      case Ci.nsIDOMSimpleGestureEvent.DIRECTION_LEFT:
        return this._tryCommand("cmd_forward");
      case Ci.nsIDOMSimpleGestureEvent.DIRECTION_RIGHT:
        return this._tryCommand("cmd_back");
    }
    return false;
  },

  _tryCommand: function _tryCommand(aId) {
     if (document.getElementById(aId).getAttribute("disabled") == "true")
       return false;
     CommandUpdater.doCommand(aId);
     return true;
  },
};

/**
 * Helper to track when the user is using a precise pointing device (pen/mouse)
 * versus an imprecise one (touch).
 */
var InputSourceHelper = {
  isPrecise: false,

  init: function ish_init() {
    Services.obs.addObserver(this, "metro_precise_input", false);
    Services.obs.addObserver(this, "metro_imprecise_input", false);
  },

  _precise: function () {
    if (!this.isPrecise) {
      this.isPrecise = true;
      this._fire("MozPrecisePointer");
    }
  },

  _imprecise: function () {
    if (this.isPrecise) {
      this.isPrecise = false;
      this._fire("MozImprecisePointer");
    }
  },

  observe: function BrowserUI_observe(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "metro_precise_input":
        this._precise();
        break;
      case "metro_imprecise_input":
        this._imprecise();
        break;
    }
  },

  fireUpdate: function fireUpdate() {
    if (this.isPrecise) {
      this._fire("MozPrecisePointer");
    } else {
      this._fire("MozImprecisePointer");
    }
  },

  _fire: function (name) {
    let event = document.createEvent("Events");
    event.initEvent(name, true, true);
    window.dispatchEvent(event);
  }
};
