/**
 * Copyright (c) 2013-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * This class listens to events on the document and then updates a react
 * component through callbacks.
 * Please note that captureMouseMove must be called in
 * order to initialize listeners on mousemove and mouseup.
 * releaseMouseMove must be called to remove them. It is important to
 * call releaseMouseMoves since mousemove is expensive to listen to.
 *
 * @providesModule DOMMouseMoveTracker
 * @typechecks
 */

'use strict';

const EventListener = require('EventListener');

const cancelAnimationFramePolyfill = require('cancelAnimationFramePolyfill');
const requestAnimationFramePolyfill = require('requestAnimationFramePolyfill');

class DOMMouseMoveTracker {
  /**
   * onMove is the callback that will be called on every mouse move.
   * onMoveEnd is called on mouse up when movement has ended.
   */
  constructor(
    /*function*/ onMove,
    /*function*/ onMoveEnd,
    /*DOMElement*/ domNode) {
    this._isDragging = false;
    this._animationFrameID = null;
    this._domNode = domNode;
    this._onMove = onMove;
    this._onMoveEnd = onMoveEnd;
    this._onMouseMove = this._onMouseMove.bind(this);
    this._onMouseUp = this._onMouseUp.bind(this);
    this._didMouseMove = this._didMouseMove.bind(this);
  }

  /**
   * This is to set up the listeners for listening to mouse move
   * and mouse up signaling the movement has ended. Please note that these
   * listeners are added at the document.body level. It takes in an event
   * in order to grab inital state.
   */
  captureMouseMoves(/*object*/ event) {
    if (!this._eventMoveToken && !this._eventUpToken) {
      this._eventMoveToken = EventListener.listen(
        this._domNode,
        'mousemove',
        this._onMouseMove
      );
      this._eventUpToken = EventListener.listen(
        this._domNode,
        'mouseup',
        this._onMouseUp
      );
    }

    if (!this._isDragging) {
      this._deltaX = 0;
      this._deltaY = 0;
      this._isDragging = true;
      this._x = event.clientX;
      this._y = event.clientY;
    }
    event.preventDefault();
  }

  /**
   * These releases all of the listeners on document.body.
   */
  releaseMouseMoves() {
    if (this._eventMoveToken && this._eventUpToken) {
      this._eventMoveToken.remove();
      this._eventMoveToken = null;
      this._eventUpToken.remove();
      this._eventUpToken = null;
    }

    if (this._animationFrameID !== null) {
      cancelAnimationFramePolyfill(this._animationFrameID);
      this._animationFrameID = null;
    }

    if (this._isDragging) {
      this._isDragging = false;
      this._x = null;
      this._y = null;
    }
  }

  /**
   * Returns whether or not if the mouse movement is being tracked.
   */
  isDragging() /*boolean*/{
    return this._isDragging;
  }

  /**
   * Calls onMove passed into constructor and updates internal state.
   */
  _onMouseMove(/*object*/ event) {
    const x = event.clientX;
    const y = event.clientY;

    this._deltaX += (x - this._x);
    this._deltaY += (y - this._y);

    if (this._animationFrameID === null) {
      // The mouse may move faster then the animation frame does.
      // Use `requestAnimationFramePolyfill` to avoid over-updating.
      this._animationFrameID =
        requestAnimationFramePolyfill(this._didMouseMove);
    }

    this._x = x;
    this._y = y;
    event.preventDefault();
  }

  _didMouseMove() {
    this._animationFrameID = null;
    this._onMove(this._deltaX, this._deltaY);
    this._deltaX = 0;
    this._deltaY = 0;
  }

  /**
   * Calls onMoveEnd passed into constructor and updates internal state.
   */
  _onMouseUp() {
    if (this._animationFrameID) {
      this._didMouseMove();
    }
    this._onMoveEnd();
  }
}

module.exports = DOMMouseMoveTracker;
