// Copyright (c) 2011, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/**
 * Touch Handler. Class that handles all touch events and
 * uses them to interpret higher level gestures and behaviors. TouchEvent is a
 * built in mobile safari type:
 * [http://developer.apple.com/safari/library/documentation/UserExperience/Reference/TouchEventClassReference/TouchEvent/TouchEvent.html].
 *
 * Examples of higher level gestures this class is intended to support
 * - click, double click, long click
 * - dragging, swiping, zooming
 *
 * Touch Behavior:
 *      Use this class to make your elements 'touchable' (see Touchable.dart).
 *      Intended to work with all webkit browsers.
 *
 * Drag Behavior:
 *      Use this class to make your elements 'draggable' (see draggable.js).
 *      This behavior will handle all of the required events and report the
 *      properties of the drag to you while the touch is happening and at the
 *      end of the drag sequence. This behavior will NOT perform the actual
 *      dragging (redrawing the element) for you, this responsibility is left to
 *      the client code. This behavior contains a work around for a mobile
 *      safari bug where the 'touchend' event is not dispatched when the touch
 *      goes past the bottom of the browser window.
 *      This is intended to work well in iframes.
 *      Intended to work with all webkit browsers, tested only on iPhone 3.x so
 *      far.
 *
 * Click Behavior:
 *      Not yet implemented.
 *
 * Zoom Behavior:
 *      Not yet implemented.
 *
 * Swipe Behavior:
 *      Not yet implemented.
 */
class TouchHandler {
  Touchable _touchable;
  Element _element;

  /** The absolute sum of all touch y deltas. */
  int _totalMoveY;

  /** The absolute sum of all touch x deltas. */
  int _totalMoveX;

  /**
   * A list of tuples where the first item is the horizontal component of a
   * recent relevant touch and the second item is the touch's time stamp. Old
   * touches are removed based on the max tracking time and when direction
   * changes.
   */
  List<int> _recentTouchesX;

  /**
   * A list of tuples where the first item is the vertical component of a
   * recent relevant touch and the second item is the touch's time stamp. Old
   * touches are removed based on the max tracking time and when direction
   * changes.
   */
  List<int> _recentTouchesY;

  // TODO(jacobr): make customizable by passing optional parameters to the
  // TouchHandler constructor.
  /**
   * Minimum movement of touch required to be considered a drag.
   */
  static final _MIN_TRACKING_FOR_DRAG = 2;

  /**
   * The maximum number of ms to track a touch event. After an event is older
   * than this value, it will be ignored in velocity calculations.
   */
  static final _MAX_TRACKING_TIME = 250;

  /** The maximum number of touches to track. */
  static final _MAX_TRACKING_TOUCHES = 5;

  /**
   * The maximum velocity to return, in pixels per millisecond, that is used to
   * guard against errors in calculating end velocity of a drag. This is a very
   * fast drag velocity.
   */
  static final _MAXIMUM_VELOCITY = 5;

  /**
   * The velocity to return, in pixel per millisecond, when the time stamps on
   * the events are erroneous. The browser can return bad time stamps if the
   * thread is blocked for the duration of the drag. This is a low velocity to
   * prevent the content from moving quickly after a slow drag. It is less
   * jarring if the content moves slowly after a fast drag.
   */
  static final _VELOCITY_FOR_INCORRECT_EVENTS = 1;

  Draggable _draggable;
  bool _tracking;
  bool _dragging;
  bool _touching;
  int _startTouchX;
  int _startTouchY;
  int _startTime;
  TouchEvent _lastEvent;
  int _lastTouchX;
  int _lastTouchY;
  int _lastMoveX;
  int _lastMoveY;
  int _endTime;
  int _endTouchX;
  int _endTouchY;

  TouchHandler(Touchable touchable, [Element element = null])
      : _touchable = touchable,
        _totalMoveY = 0,
        _totalMoveX = 0,
        _recentTouchesX = new List<int>(),
        _recentTouchesY = new List<int>(),
        // TODO(jmesserly): I don't like having to initialize all booleans here
        // See b/5045736
        _dragging = false,
        _tracking = false,
        _touching = false {
    _element = element != null ? element : touchable.getElement();
  }

  /**
   * Begin tracking the touchable element, it is eligible for dragging.
   */
  void _beginTracking() {
    _tracking = true;
  }

  /**
   * Stop tracking the touchable element, it is no longer dragging.
   */
  void _endTracking() {
    _tracking = false;
    _dragging = false;
    _totalMoveY = 0;
    _totalMoveX = 0;
  }

  /**
   * Correct erroneous velocities by capping the velocity if we think it's too
   * high, or setting it to a default velocity if know that the event data is
   * bad. Returns the corrected velocity.
   */
  num _correctVelocity(num velocity) {
    num absVelocity = velocity.abs();
    if (absVelocity > _MAXIMUM_VELOCITY) {
      absVelocity = _recentTouchesY.length < 6 ?
          _VELOCITY_FOR_INCORRECT_EVENTS : _MAXIMUM_VELOCITY;
    }
    return absVelocity * (velocity < 0 ? -1 : 1);
  }

  /**
   * Start listenting for events.
   * If [capture] is True the TouchHandler should listen during the capture
   * phase.
   */
  void enable([bool capture = false]) {
    Function onEnd = (e) { _onEnd(e.timeStamp, e); };
    _addEventListeners(
        _element,
        (e) { _onStart(e); },
        (e) { _onMove(e); }, onEnd, onEnd, capture);
  }

  /**
   * Get the current horizontal drag delta. Drag delta is defined as the deltaX
   * of the start touch position and the last touch position.
   */
  int getDragDeltaX() {
    return _lastTouchX - _startTouchX;
  }

  /**
   * Get the current vertical drag delta. Drag delta is defined as the deltaY of
   * the start touch position and the last touch position.
   */
  int getDragDeltaY() {
    return _lastTouchY - _startTouchY;
  }

  /**
   * Get end velocity of the drag. This method is specific to drag behavior, so
   * if touch behavior and drag behavior is split then this should go with drag
   * behavior. End velocity is defined as deltaXY / deltaTime where deltaXY is
   * the difference between endPosition and the oldest recent position, and
   * deltaTime is the difference between endTime and the oldest recent time
   * stamp.
   */
  Coordinate getEndVelocity() {
    num velocityX = 0;
    num velocityY = 0;

    if (_recentTouchesX.length > 0) {
      num timeDeltaX = Math.max(1, _endTime - _recentTouchesX[1]);
      velocityX = (_endTouchX - _recentTouchesX[0]) / timeDeltaX;
    }

    if (_recentTouchesY.length > 0) {
      num timeDeltaY = Math.max(1, _endTime - _recentTouchesY[1]);
      velocityY = (_endTouchY - _recentTouchesY[0]) / timeDeltaY;
    }
    velocityX = _correctVelocity(velocityX);
    velocityY = _correctVelocity(velocityY);
    return new Coordinate(velocityX, velocityY);
  }

  /**
   * Return the touch of the last event.
   */
  Touch _getLastTouch() {
    assert (_lastEvent != null); // Last event not set
    return _lastEvent.touches[0];
  }

  /**
   * Is the touch manager currently tracking touch moves to detect a drag?
   */
  bool isTracking() {
    return _tracking;
  }

  /**
   * Touch end handler.
   */
  void _onEnd(int timeStamp, [TouchEvent e = null]) {
    _touching = false;
    _touchable.onTouchEnd();
    if (!_tracking || _draggable === null) {
      return;
    }
    Touch touch = _getLastTouch();
    int clientX = touch.clientX;
    int clientY = touch.clientY;
    if (_dragging) {
      _endTime = timeStamp;
      _endTouchX = clientX;
      _endTouchY = clientY;
      _recentTouchesX = _removeOldTouches(_recentTouchesX, timeStamp);
      _recentTouchesY = _removeOldTouches(_recentTouchesY, timeStamp);
      _draggable.onDragEnd();
      if (e !== null) {
        e.preventDefault();
      }
      ClickBuster.preventGhostClick(_startTouchX, _startTouchY);
    }
    _endTracking();
  }

  /**
   * Touch move handler.
   */
  void _onMove(TouchEvent e) {
    if (!_tracking || _draggable === null) {
      return;
    }
    final touch = e.touches[0];
    int clientX = touch.clientX;
    int clientY = touch.clientY;
    int moveX = _lastTouchX - clientX;
    int moveY = _lastTouchY - clientY;
    _totalMoveX += moveX.abs();
    _totalMoveY += moveY.abs();
    _lastTouchX = clientX;
    _lastTouchY = clientY;
    if (!_dragging &&
        ((_totalMoveY > _MIN_TRACKING_FOR_DRAG && _draggable.verticalEnabled) ||
         (_totalMoveX > _MIN_TRACKING_FOR_DRAG &&
          _draggable.horizontalEnabled))) {
      _dragging = _draggable.onDragStart(e);
      if (!_dragging) {
        _endTracking();
      } else {
        _startTouchX = clientX;
        _startTouchY = clientY;
        _startTime = e.timeStamp;
      }
    }
    if (_dragging) {
      _draggable.onDragMove();
      _lastEvent = e;
      e.preventDefault();
      _recentTouchesX =
          _removeTouchesInWrongDirection(_recentTouchesX, _lastMoveX, moveX);
      _recentTouchesY =
          _removeTouchesInWrongDirection(_recentTouchesY, _lastMoveY, moveY);
      _recentTouchesX = _removeOldTouches(_recentTouchesX, e.timeStamp);
      _recentTouchesY = _removeOldTouches(_recentTouchesY, e.timeStamp);
      _recentTouchesX.add(clientX);
      _recentTouchesX.add(e.timeStamp);
      _recentTouchesY.add(clientY);
      _recentTouchesY.add(e.timeStamp);
    }
    _lastMoveX = moveX;
    _lastMoveY = moveY;
  }

  /**
   * Touch start handler.
   */
  void _onStart(TouchEvent e) {
    if (_touching) {
      return;
    }
    _touching = true;
    if (!_touchable.onTouchStart(e) || _draggable === null) {
      return;
    }
    final touch = e.touches[0];
    _startTouchX = _lastTouchX = touch.clientX;
    _startTouchY = _lastTouchY = touch.clientY;
    _startTime = e.timeStamp;
    // TODO(jacobr): why don't we just clear the lists?
    _recentTouchesX = new List<int>();
    _recentTouchesY = new List<int>();
    _recentTouchesX.add(touch.clientX);
    _recentTouchesX.add(e.timeStamp);
    _recentTouchesY.add(touch.clientY);
    _recentTouchesY.add(e.timeStamp);
    _lastEvent = e;
    _beginTracking();
  }

  /**
   * Filters the provided recent touches list to remove all touches older than
   * the max tracking time or the 5th most recent touch.
   * [recentTouches] specifies a list of tuples where the first item is the x
   * or y component of the recent touch and the second item is the touch time
   * stamp. The time of the most recent event is specified by [recentTime].
   */
  List<int> _removeOldTouches(List<int> recentTouches,
                         int recentTime) {
    int count = 0;
    final len = recentTouches.length;
    assert (len % 2 == 0);
    while (count < len &&
           recentTime - recentTouches[count + 1] > _MAX_TRACKING_TIME ||
           (len - count) > _MAX_TRACKING_TOUCHES * 2) {
      count += 2;
    }
    return count == 0 ? recentTouches : _removeFirstN(recentTouches, count);
  }

  static List<int> _removeFirstN(List<int> list, int n) {
    return new List<int>.fromList(list, n, list.length);
  }

  /**
   * Filters the provided recent touches list to remove all touches except the
   * last if the move direction has changed.
   * [recentTouches] specifies a list of tuples where the first item is the x
   * or y component of the recent touch and the second item is the touch time
   * stamp. The x or y component of the most recent move is specified by
   * [recentMove].
   */
  List<int> _removeTouchesInWrongDirection(List<int> recentTouches,
                                            int lastMove, int recentMove) {
    if (lastMove !=0 && recentMove != 0 && recentTouches.length > 2 &&
        _xor(lastMove > 0, recentMove > 0)) {
      return _removeFirstN(recentTouches, recentTouches.length - 2);
    }
    return recentTouches;
  }

  // TODO(jacobr): why doesn't bool implement the xor operator directly?
  static bool _xor(bool a, bool b) {
    return (a === true || b === true) && !(a === true && b === true);
  }

  /**
   * Reset the touchable element.
   */
  void reset() {
    _endTracking();
    _touching = false;
  }

  /**
   * Call this method to enable drag behavior on a draggable delegate.
   * The [draggable] object can be the same as the [_touchable] object, they are
   * assigned to different members to allow for strong typing with interfaces.
   */
  void setDraggable(Draggable draggable) {
    _draggable = draggable;
  }
}
