// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; js2-strict-trailing-comma-warning: nil -*-
/*
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Mozilla Mobile Browser.
 *
 * The Initial Developer of the Original Code is
 * Mozilla Corporation.
 * Portions created by the Initial Developer are Copyright (C) 2008, 2009
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Stuart Parmenter <stuart@mozilla.com>
 *   Brad Lassey <blassey@mozilla.com>
 *   Mark Finkle <mfinkle@mozilla.com>
 *   Gavin Sharp <gavin.sharp@gmail.com>
 *   Ben Combee <combee@mozilla.com>
 *   Roy Frostig <rfrostig@mozilla.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

// how many msecs elapse before two taps are not a double tap
const kDoubleClickInterval = 400;

// threshold in ms to detect if the click is possibly a dblClick
const kDoubleClickThreshold = 200;

// threshold in pixels for sensing a tap as opposed to a pan
const kTapRadius = 25;

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

/**
 * InputHandler
 *
 * The input handler is an arbiter between the Fennec chrome window inputs and any
 * registered input modules.  It keeps an array of input module objects.  Incoming
 * input events are wrapped in an EventInfo object and passed down to the input modules
 * in the order of the modules array.  Every registed module thus gets called with
 * an EventInfo for each event that the InputHandler is registered to listen for.
 * Currently, the InputHandler listens for the following events by default.
 *
 * On the Fennec global chrome window:
 *   URLChanged
 *   TabSelect
 *   mousedown
 *   mouseup
 *   mousemove
 *   click
 *
 * On the browserViewContainer:
 *   keydown
 *   keyup
 *   DOMMouseScroll
 *
 *
 * When one of the handlers decides it wants to handle the event, it should call
 * grab() on its owner which will cause it to receive all of the events until it
 * calls ungrab().  Calling grab will notify the other handlers via a
 * cancelPending() notification.  This tells them to stop what they're doing and
 * give up hope for being the one to process the events.
 *
 * Input modules must provide the following interface:
 *
 *   handleEvent(evInfo)
 *     Entry point by which InputHandler passes wrapped Fennec chrome window events
 *     to the module.
 *
 *   cancelPending()
 *     Called by the InputHandler as a hint to the module that it may wish to reset
 *     whatever state it might have entered by processing events thus far.  For
 *     instance, a module may have grabbed (cf grab()) focus, in which case the
 *     InputHandler will call cancelPending() on all remaining modules.
 *
 * How grabbing works:
 *   An input module may wish to grab event focus of the InputHandler, which means that it
 * wants to process all incoming events for a while.  When the InputHandler is grabbed
 * by one of its modules, only that module will receive incoming events until it ungrabs
 * the InputHandler.  No other modules' handleEvent() function will be called while the
 * InputHandler is grabbed.  Grabs and ungrabs of the InputHandler require an object reference
 * corresponding to the grabbing object.  That is, a module must call inputHandler.grab(this)
 * and .ungrab(this) in order for the calls to succeed.  The object given as the argument
 * will be that which is given event focus.
 */
function InputHandler(browserViewContainer) {
  /* the list of modules that will handle input */
  this._modules = [];

  /* which module, if any, has all events directed to it */
  this._grabber = null;
  this._grabDepth = 0;

  /* when true, don't process any events */
  this._ignoreEvents = false;

  /* when set to true, next click won't be dispatched */
  this._suppressNextClick = false;

  /* these handle dragging of both chrome elements and content */
  window.addEventListener("mousedown", this, true);
  window.addEventListener("mouseup", this, true);
  window.addEventListener("mousemove", this, true);
  window.addEventListener("click", this, true);

  /* these handle key strokes in the browser view (where page content appears) */
  browserViewContainer.addEventListener("keydown", this, true);
  browserViewContainer.addEventListener("keyup", this, true);
  browserViewContainer.addEventListener("DOMMouseScroll", this, true);
  browserViewContainer.addEventListener("MozMousePixelScroll", this, true);
  browserViewContainer.addEventListener("contextmenu", this, false);

  this.addModule(new MouseModule(this, browserViewContainer));
  this.addModule(new ScrollwheelModule(this, browserViewContainer));
}


InputHandler.prototype = {
  /**
   * Add a module.  Module priority is first come, first served, so modules
   * added later have lower priority.
   */
  addModule: function addModule(m) {
    this._modules.push(m);
  },

  /**
   * Have the InputHandler begin/resume listening for events.
   */
  startListening: function startListening() {
    this._ignoreEvents = false;
  },

  /**
   * Stop/pause the InputHandler from listening for events.
   */
  stopListening: function stopListening() {
    this._ignoreEvents = true;
  },

  /**
   * A module calls grab(this) to grab event focus from the input
   * handler.  In grabbed state, the input handler forwards all events
   * directly to the grabber module, and not to any other modules.
   * The this reference passed is essentially a ceritificate to the
   * input handler --- collateral for the grab.
   *
   * Other modules cannot grab a grabbed input handler, and only the
   * grabber module can ungrab the input handler.
   */
  grab: function grab(grabber) {
    if (!this._grabber || this._grabber == grabber) {
      if (!this._grabber) {
        // call cancel on all modules
        let mods = this._modules;
        for (let i = 0, len = mods.length; i < len; ++i)
          if (mods[i] != grabber)
            mods[i].cancelPending();
      }
      this._grabber = grabber;
      return true;
    }
    return false;
  },

  /**
   * A grabber module should ungrab the input handler by calling ungrab(this).
   * Of course, a module other than the original grabber may spoof the ungrab
   * if it has our reference to that module.
   *
   * @param grabber The grabber's object reference, as grabber proof.
   */
  ungrab: function ungrab(grabber) {
    if (this._grabber == grabber) {  // only grabber can ungrab
      this._grabber = null;
    }
  },

  /**
   * Sometimes a module will swallow a mousedown and mouseup, which (when found
   * in sequence) should be followed by a click.  Ideally, this module would
   * listen for the click as well, and ignore it, but this is a convenience method
   * for the module to do so via the InputHandler.  Hopefully the module is doing
   * this under grab (that is, hopefully the module was grabbing while the mousedown
   * and mouseup events came in, *not* just grabbing for making this call).
   */
  suppressNextClick: function suppressNextClick() {
    this._suppressNextClick = true;
  },

  /**
   * Undoes any suppression caused by calling suppressNextClick(), unless the click
   * already happened.
   */
  allowClicks: function allowClicks() {
    this._suppressNextClick = false;
  },

  /**
   * InputHandler's DOM event handler.
   */
  handleEvent: function handleEvent(aEvent) {
    if (this._ignoreEvents)
      return;

    /* ignore all events that belong to other windows or documents (e.g. content events) */
    if (aEvent.view != window)
      return;

    if (this._suppressNextClick && aEvent.type == "click") {
      this._suppressNextClick = false;
      aEvent.stopPropagation();
      aEvent.preventDefault();
      return;
    }

    this._passToModules(new InputHandler.EventInfo(aEvent));
  },

  /**
   * Utility method for passing an EventInfo to the handlers of all modules beginning
   * with the module at index skipToIndex and increasing (==> decreasing in priority).
   */
  _passToModules: function _passToModules(evInfo, skipToIndex) {
    if (this._grabber) {
      this._grabber.handleEvent(evInfo);
    } else {
      let mods = this._modules;
      let i = skipToIndex || 0;

      for (let len = mods.length; i < len; ++i) {
        mods[i].handleEvent(evInfo);  // event focus could get grabbed in this invocation
        if (this._grabber)            // so don't pass the event to the rest of modules
          break;
      }
    }
  }
};

/**
 * Helper class to InputHandler.  Wraps a DOM event with some additional data.
 */
InputHandler.EventInfo = function EventInfo(aEvent) {
  this.event = aEvent;
  this.time = Date.now();
};

InputHandler.EventInfo.prototype = {
  toString: function toString() {
    return '[EventInfo] { event=' + this.event + 'time=' + this.time + ' }';
  }
};

/**
 * MouseModule
 *
 * Input handler module that handles all mouse-related input such as dragging and
 * clicking.
 *
 * The Fennec chrome DOM tree has elements that are augmented dynamically with
 * custom JS properties that tell the MouseModule 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).
 *
 * The MouseModule grabs event focus of the input handler on mousedown, at which
 * point it will attempt to find such custom draggers/clickers by walking up the
 * DOM tree from the event target.  It ungrabs event focus on mouseup.  It
 * redispatches the swallowed mousedown, mouseup events back to chrome, so that
 * chrome elements still get their events.
 *
 * The mousedown and mouseup events happening in the main context are
 * redispatched as soon as they get caught, contrary to events happening on web
 * content which are swallowed before being redispatched as a triple at the end
 * of the mouseup handling.
 *
 * 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)
 *     Signals an input attempt to drag by dx, dy.
 *
 * Between mousedown and mouseup, MouseModule incrementally drags and updates
 * the dragger accordingly, and also determines whether a [double-]click occured
 * (based on whether the input moves have moved outside of a certain drag disk
 * centered at the mousedown position).  If a [double-]click happened, any
 * customClicker will be notified.  The customClicker must support the following
 * interface:
 *
 *   singleClick(cx, cy, modifiers)
 *     Signals a single (as opposed to double) click occured at client
 *     coordinates cx, cy.  Specify optional modifiers to include
 *     shift-keys with click.
 *
 *   doubleClick(cx1, cy1, cx2, cy2)
 *     Signals a doubleclick occured, with the first click at client coordinates
 *     cx1, cy1, and second click at client coordinates cx2, cy2.
 *
 * There is a default dragger in case a scrollable element is dragged --- see
 * the defaultDragger prototype property.  There is no default clicker.
 */
function MouseModule(owner, browserViewContainer) {
  this._owner = owner;
  this._browserViewContainer = browserViewContainer;
  this._dragData = new DragData(this, kTapRadius);

  this._dragger = null;
  this._clicker = null;

  this._downUpEvents = [];
  this._targetScrollInterface = null;

  var self = this;
  this._kinetic = new KineticController(Util.bind(this._dragBy, this),
                                        Util.bind(this._kineticStop, this));
}


MouseModule.prototype = {
  handleEvent: function handleEvent(evInfo) {
    let evt = evInfo.event;
    if (evt.button !== 0 && evt.type != "contextmenu")
      return;

    switch (evt.type) {
      case "mousedown":
        this._onMouseDown(evInfo);
        break;
      case "mousemove":
        this._onMouseMove(evInfo);
        break;
      case "mouseup":
        this._onMouseUp(evInfo);
        break;
      case "contextmenu":
        // TODO: Make "contextmenu" a first class part of InputHandler
        // Bug 554639
        if (ContextHelper.popupNode) {
          if (this._clicker)
            this._clicker.panBegin();
          if (this._dragger)
            this._dragger.dragStop(0, 0, this._targetScrollInterface);
          this.cancelPending();
        }
        break;
    }
  },

  /**
   * 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() {
    if (this._kinetic.isActive())
      this._kinetic.end();

    this._dragData.reset();
    this._targetScrollInterface = null;

    if (this._clickTimeout)
      window.clearTimeout(this._clickTimeout);

    this._cleanClickBuffer();
  },

  /**
   * Handle a mousedown by stopping any lingering kinetic drag, walking DOM tree
   * in search of a scrollable element (and its custom dragger if available) and
   * a clicker, and initiating a drag if we have said scrollable element.  The
   * mousedown event is entirely swallowed but is saved for later redispatching,
   * once we know right and proper what the input is trying to do to us.
   *
   * We grab() in here.
   */
  _onMouseDown: function _onMouseDown(evInfo) {
    this._owner.allowClicks();

    let dragData = this._dragData;
    if (dragData.dragging) {
      // Somehow a mouse up was missed.
      let [sX, sY] = dragData.panPosition();
      this._doDragStop(sX, sY, !dragData.isPan());
    }
    dragData.reset();

    // walk up the DOM tree in search of nearest scrollable ancestor.  nulls are
    // returned if none found.
    let [targetScrollbox, targetScrollInterface]
      = this.getScrollboxFromElement(evInfo.event.target);

    // stop kinetic panning if targetScrollbox has changed
    let oldInterface = this._targetScrollInterface;
    if (this._kinetic.isActive() && targetScrollInterface != oldInterface)
      this._kinetic.end();

    let targetClicker = this.getClickerFromElement(evInfo.event.target);

    this._targetScrollInterface = targetScrollInterface;
    this._dragger = (targetScrollInterface) ? (targetScrollbox.customDragger || this._defaultDragger)
                                            : null;
    this._clicker = (targetClicker) ? targetClicker.customClicker : null;

    if (this._clicker)
      this._clicker.mouseDown(evInfo.event.clientX, evInfo.event.clientY);

    if (targetScrollInterface && this._dragger.isDraggable(targetScrollbox, targetScrollInterface))
      this._doDragStart(evInfo.event);

    if (this._targetIsContent(evInfo.event)) {
      this._recordEvent(evInfo);
    }
    else {
      if (this._clickTimeout) {
        // cancel all pending content clicks
        window.clearTimeout(this._clickTimeout);
        this._cleanClickBuffer();
      }

      if (targetScrollInterface) {
        // do not allow axis locking if panning is only possible in one direction
        let cX = {}, cY = {};
        targetScrollInterface.getScrolledSize(cX, cY);
        let rect = targetScrollbox.getBoundingClientRect();
        dragData.locked = ((cX.value > rect.width) != (cY.value > rect.height));
      }
    }
  },

  /**
   * Handle a mouseup by swallowing the event (just as we did the mousedown) as
   * well as the possible DOM click event that follows, making one last drag
   * (which, do note, might just be the beginning of a kinetic drag that will
   * linger long after we are gone), and recording the mousedown for later
   * redispatching.
   *
   * We ungrab() in here.
   */
  _onMouseUp: function _onMouseUp(evInfo) {
    let dragData = this._dragData;
    let oldIsPan = dragData.isPan();
    if (dragData.dragging) {
      dragData.setDragPosition(evInfo.event.screenX, evInfo.event.screenY);
      let [sX, sY] = dragData.panPosition();
      this._doDragStop(sX, sY, !dragData.isPan());
    }

    if (this._targetIsContent(evInfo.event)) {
      // User possibly clicked on something in content
      this._recordEvent(evInfo);
      let commitToClicker = this._clicker && dragData.isClick() && (this._downUpEvents.length > 1);
      if (commitToClicker)
        // commit this click to the doubleclick timewait buffer
        this._commitAnotherClick();
      else
        // clean the click buffer ourselves, since there was no clicker
        // to commit to.  when there is one, the path taken through
        // _commitAnotherClick takes care of this.
        this._cleanClickBuffer();
    }
    else if (dragData.isPan()) {
      // User was panning around, do not allow chrome click
      // XXX Instead of having suppressNextClick, we could grab until click is seen
      // and THEN ungrab so that owner does not need to know anything about clicking.
      let generatesClick = evInfo.event.detail;
      if (generatesClick)
        this._owner.suppressNextClick();
    }

    let clicker = this._clicker;
    if (clicker) {
      // Let clicker know when mousemove begins a pan
      if (!oldIsPan && dragData.isPan())
        clicker.panBegin();
      clicker.mouseUp(evInfo.event.clientX, evInfo.event.clientY);
    }

    this._owner.ungrab(this);
  },

  /**
   * If we're in a drag, do what we have to do to drag on.
   */
  _onMouseMove: function _onMouseMove(evInfo) {
    let dragData = this._dragData;

    if (dragData.dragging) {
      let oldIsPan = dragData.isPan();
      dragData.setDragPosition(evInfo.event.screenX, evInfo.event.screenY);
      evInfo.event.stopPropagation();
      evInfo.event.preventDefault();
      if (dragData.isPan()) {
        this._owner.grab(this);
        // Only pan when mouse event isn't part of a click. Prevent jittering on tap.
        let [sX, sY] = dragData.panPosition();
        this._doDragMove(sX, sY);

        // Let clicker know when mousemove begins a pan
        let clicker = this._clicker;
        if (!oldIsPan && clicker)
          clicker.panBegin();
      }
    }
  },

  /**
   * Check if the event concern the browser content
   */
  _targetIsContent: function _targetIsContent(aEvent) {
    let target = aEvent.target;
    while (target) {
      if (target === window)
        return false;
      if (target === this._browserViewContainer)
        return true;

      target = target.parentNode;
    }
    return false;
  },

  /**
   * Inform our dragger of a dragStart.
   */
  _doDragStart: function _doDragStart(event) {
    let dragData = this._dragData;
    dragData.setDragStart(event.screenX, event.screenY);
    this._kinetic.addData(0, 0);
    if (!this._kinetic.isActive())
      this._dragger.dragStart(event.clientX, event.clientY, event.target, this._targetScrollInterface);
  },

  /**
   * Finish a drag.  The third parameter is a secret one used to distinguish
   * between the supposed end of drag caused by a mouseup and the real end
   * of drag which happens when KineticController::end() is called.
   */
  _doDragStop: function _doDragStop(sX, sY, kineticStop) {
    let dragData = this._dragData;
    dragData.endDrag();

    if (!kineticStop) {
      // we're not really done, since now it is kinetic's turn to scroll about
      let dX = dragData.prevPanX - sX;
      let dY = dragData.prevPanY - sY;
      this._kinetic.addData(-dX, -dY);
      this._kinetic.start();
    } else {
      // now we're done, says our secret 3rd argument
      this._dragger.dragStop(0, 0, this._targetScrollInterface);
    }
  },

  /**
   * Update kinetic with new data and drag.
   */
  _doDragMove: function _doDragMove(sX, sY) {
    let dragData = this._dragData;
    let dX = dragData.prevPanX - sX;
    let dY = dragData.prevPanY - sY;
    this._kinetic.addData(-dX, -dY);
    this._dragBy(dX, dY);
  },

  /**
   * Used by _doDragMove() above and by KineticController's timer to do the
   * actual dragMove signalling to the dragger.  We'd put this in _doDragMove()
   * but then KineticController would be adding to its own data as it signals
   * the dragger of dragMove()s.
   */
  _dragBy: function _dragBy(dX, dY) {
    let dragData = this._dragData;
    return this._dragger.dragMove(dX, dY, this._targetScrollInterface);
  },

  /** Callback for kinetic scroller. */
  _kineticStop: function _kineticStop() {
    let dragData = this._dragData;
    if (!dragData.dragging)
      this._doDragStop(0, 0, true);
  },

  /**
   * Commit another click event to our click buffer.  The `click buffer' is a
   * timeout initiated by the first click.  If the timeout is still alive when
   * another click is committed, then the click buffer forms a double click, and
   * the timeout is cancelled.  Otherwise, the timeout issues a single click to
   * the clicker.
   */
  _commitAnotherClick: function _commitAnotherClick() {
    if (this._clickTimeout) {   // we're waiting for a second click for double
      window.clearTimeout(this._clickTimeout);
      this._doDoubleClick();
    } else {
      let time = this._downUpEvents[1].time - this._downUpEvents[0].time;
      if (time >= kDoubleClickThreshold)
        this._doSingleClick();
      else
        this._clickTimeout = window.setTimeout(function _clickTimeout(self) { self._doSingleClick(); },
                                               kDoubleClickInterval, this);
    }
  },

  /**
   * Endpoint of _commitAnotherClick().  Finalize a single click and tell the clicker.
   */
  _doSingleClick: function _doSingleClick() {
    let ev = this._downUpEvents[1].event;
    this._cleanClickBuffer(2);

    // borrowed from nsIDOMNSEvent.idl
    let modifiers =
      (ev.altKey   ? Ci.nsIDOMNSEvent.ALT_MASK     : 0) |
      (ev.ctrlKey  ? Ci.nsIDOMNSEvent.CONTROL_MASK : 0) |
      (ev.shiftKey ? Ci.nsIDOMNSEvent.SHIFT_MASK   : 0) |
      (ev.metaKey  ? Ci.nsIDOMNSEvent.META_MASK    : 0);
    this._clicker.singleClick(ev.clientX, ev.clientY, modifiers);
  },

  /**
   * Endpoint of _commitAnotherClick().  Finalize a double click and tell the clicker.
   */
  _doDoubleClick: function _doDoubleClick() {
    let mouseUp1 = this._downUpEvents[1].event;
    let mouseUp2 = this._downUpEvents[3].event;
    this._cleanClickBuffer(4);
    this._clicker.doubleClick(mouseUp1.clientX, mouseUp1.clientY,
                              mouseUp2.clientX, mouseUp2.clientY);
  },

  /**
   * Record a mousedown/mouseup event for later redispatch via
   * _redispatchDownUpEvents()
   */
  _recordEvent: function _recordEvent(evInfo) {
    this._downUpEvents.push(evInfo);
  },

  /**
   * Clean out the click buffer.  Should be called after a single, double, or
   * non-click has been processed and all relevant (re)dispatches of events in
   * the recorded down/up event queue have been issued out.
   *
   * @param [optional] the number of events to remove from the front of the
   * recorded events queue.
   */
  _cleanClickBuffer: function _cleanClickBuffer(howMany) {
    delete this._clickTimeout;

    if (howMany == undefined)
      howMany = this._downUpEvents.length;

    this._downUpEvents.splice(0, howMany);
  },

  /**
   * The default dragger object used by MouseModule 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 = {};
      scroller.getScrolledSize(sX, sY);
      let rect = target.getBoundingClientRect();
      return sX.value > rect.width || sY.value > rect.height;
    },

    dragStart: function dragStart(cx, cy, target, scroller) {},

    dragStop : function dragStop(dx, dy, scroller) {
      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;
    }
  },

  // -----------------------------------------------------------
  // -- Utility functions

  /**
   * 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;
    let prev = null;

    for (; elem; elem = elem.parentNode) {
      try {
        if (elem.ignoreDrag) {
          prev = elem;
          break;
        }

        if (elem.scrollBoxObject) {
          scrollbox = elem;
          qinterface = elem.scrollBoxObject;
          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. */ }
      prev = elem;
    }
    return [scrollbox, qinterface, prev];
  },

  /**
   * Walk up (parentward) the DOM tree from elem in search of an element with
   * a customClicker.  Return the element if found, null elsewise.
   */
  getClickerFromElement: function getClickerFromElement(elem) {
    for (; elem; elem = elem.parentNode)
      if (elem.customClicker)
        break;

    return (elem) ? elem : null;
  },

  toString: function toString() {
    return '[MouseModule] {'
      + '\n\tdragData=' + this._dragData + ', '
      + 'dragger=' + this._dragger + ', '
      + 'clicker=' + this._clicker + ', '
      + '\n\tdownUpEvents=' + this._downUpEvents + ', '
      + 'length=' + this._downUpEvents.length + ', '
      + '\n\ttargetScroller=' + this._targetScrollInterface + ', '
      + '\n\tclickTimeout=' + this._clickTimeout + '\n  }';
  }
};

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

DragData.prototype = {
  reset: function reset() {
    this.dragging = false;
    this.sX = null;
    this.sY = null;
    this.locked = 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];
    }
  },

  setDragPosition: function setDragPosition(sX, sY) {
    // Check if drag is now a pan.
    if (!this._isPan) {
      let distanceSquared = (Math.pow(sX - this._originX, 2) + Math.pow(sY - this._originY, 2));
      this._isPan = (distanceSquared > Math.pow(this._dragRadius, 2));
      if (this._isPan) {
        // dismiss the active state of the pan element
        let target = document.documentElement;
        let state = this._domUtils.getContentState(target);
        this._domUtils.setContentState(target, state & kStateActive);
      }
    }

    // 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);

      // After the first lock, see if locking decision should be reverted.
      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;
      }

      // 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) {
    this.sX = this._originX = screenX;
    this.sY = this._originY = screenY;
    this.dragging = true;
    this.locked = false;
  },

  endDrag: function endDrag() {
    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);
  },

  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._timer = null;
  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 = gPrefService.getIntPref("browser.ui.kinetic.updateInterval");
  // "Friction" of the scroll pane. The lower, the less friction and the further distance traveled.
  this._decelerationRate = gPrefService.getIntPref("browser.ui.kinetic.decelerationRate") / 10000;
  // A multiplier for the initial velocity of the movement.
  this._speedSensitivity = gPrefService.getIntPref("browser.ui.kinetic.speedSensitivity") / 100;
  // Number of milliseconds that can contain a swipe. Movements earlier than this are disregarded.
  this._swipeLength = gPrefService.getIntPref("browser.ui.kinetic.swipeLength");

  this._reset();
}

KineticController.prototype = {
  _reset: function _reset() {
    if (this._timer != null) {
      this._timer.cancel();
      this._timer = null;
    }

    this.momentumBuffer = [];
    this._velocity.set(0, 0);
  },

  isActive: function isActive() {
    return (this._timer != null);
  },

  _startTimer: function _startTimer() {
    // Use closed form of a parabola to calculate each position for panning.
    // x(t) = v0*t + .5*t^2*a
    // where: v0 is initial velocity
    //        a is acceleration
    //        t is time elapsed
    //
    // x(t)
    //  ^
    //  |                |
    //  |
    //  |                |
    //  |           ....^^^^....
    //  |      ...^^     |      ^^...
    //  |  ...^                      ^...
    //  |..              |               ..
    //   -----------------------------------> t
    //  t0             tf=-v0/a
    //
    // Using this formula, distance moved is independent of the time between each frame, unlike time
    // step approaches. Once the time is up, set the position to x(tf) and stop the timer.

    let lastx = this._position;  // track last position vector because pan takes differences
    let v0 = this._velocity;  // initial velocity
    let a = this._acceleration;  // acceleration

    // Temporary "bins" so that we don't create new objects during pan.
    let aBin = new Point(0, 0);
    let v0Bin = new Point(0, 0);

    let callback = {
      _self: this,
      notify: function kineticTimerCallback(timer) {
        let self = this._self;

        if (!self.isActive())  // someone called end() on us between timer intervals
          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.
        let realt = Date.now() - self._initialTime;
        self._time += self._updateInterval;
        let t = (self._time + realt) / 2;

        // Calculate new position using x(t) formula.
        let x = v0Bin.set(v0).scale(t).add(aBin.set(a).scale(0.5 * t * t));
        let dx = x.x - lastx.x;
        let dy = x.y - lastx.y;
        lastx.set(x);

        // Test to see if movement is finished for each component. As seen in graph, we want the
        // final position to be at tf.
        if (t >= -v0.x / a.x) {
          // Plug in t=-v0/a into x(t) to get final position.
          dx = -v0.x * v0.x / 2 / a.x - lastx.x;
          // Reset components. Next frame: a's component will be 0 and t >= NaN will be false.
          lastx.x = 0;
          v0.x = 0;
          a.x = 0;
        }
        // Symmetric to above case.
        if (t >= -v0.y / a.y) {
          dy = -v0.y * v0.y / 2 / a.y - lastx.y;
          lastx.y = 0;
          v0.y = 0;
          a.y = 0;
        }

        let panned = false;
        try { panned = self._panBy(Math.round(-dx), Math.round(-dy)); } catch (e) {}
        if (!panned)
          self.end();
      }
    };

    this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    //initialize our timer with updateInterval
    this._timer.initWithCallback(callback,
                                 this._updateInterval,
                                 this._timer.TYPE_REPEATING_SLACK);
  },

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

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

    // Only allow kinetic scrolling to speed up if kinetic scrolling is active.
    this._velocity.x = (distanceX < 0 ? Math.min : Math.max)((distanceX / swipeLength) * this._speedSensitivity, this._velocity.x);
    this._velocity.y = (distanceY < 0 ? Math.min : Math.max)((distanceY / swipeLength) * this._speedSensitivity, this._velocity.y);

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

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

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

    return true;
  },

  end: function end() {
    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});
  }
};

/**
 * Input module for basic scrollwheel input.  Currently just zooms the browser
 * view accordingly.
 */
function ScrollwheelModule(owner, browserViewContainer) {
  this._owner = owner;
  this._browserViewContainer = browserViewContainer;
}

ScrollwheelModule.prototype = {
  pendingEvent : 0,
  handleEvent: function handleEvent(evInfo) {
    if (evInfo.event.type == "DOMMouseScroll" || evInfo.event.type == "MozMousePixelScroll") {
      /*
      * If events come too fast we don't want their handling to lag the zoom in/zoom out execution.
      * With the timeout the zoom is executed as we scroll.
      */
      if (this.pendingEvent)
        clearTimeout(this.pendingEvent);

      this.pendingEvent = setTimeout(function handleEventImpl(self) {
        self.pendingEvent = 0;
        Browser.zoom(evInfo.event.detail);
      }, 0, this);

      evInfo.event.stopPropagation();
      evInfo.event.preventDefault();
    }
  },

  /* We don't have much state to reset if we lose event focus */
  cancelPending: function cancelPending() {}
};
