/**
 * Copyright 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 *Module ResponderEventPlugin
 */

'use strict';

var EventConstants = require('./EventConstants')
const {
  topMouseMove,
  topMouseUp,
  topMouseDown,
} = EventConstants.topLevelTypes
const touchSupport = EventConstants.touchSupport

var EventPluginUtils = require('./EventPluginUtils');
var EventPropagators = require('./EventPropagators');
var ResponderSyntheticEvent = require('./ResponderSyntheticEvent');
var ResponderTouchHistoryStore = require('./ResponderTouchHistoryStore');

var accumulate = require('./accumulate');

var isStartish = EventPluginUtils.isStartish;
var isMoveish = EventPluginUtils.isMoveish;
var isEndish = EventPluginUtils.isEndish;
var executeDirectDispatch = EventPluginUtils.executeDirectDispatch;
var hasDispatches = EventPluginUtils.hasDispatches;
var executeDispatchesInOrderStopAtTrue = EventPluginUtils.executeDispatchesInOrderStopAtTrue;

/**
 * Instance of element that should respond to touch/move types of interactions,
 * as indicated explicitly by relevant callbacks.
 */
var responderInst = null;

/**
 * Count of current touches. A textInput should become responder iff the
 * selection changes while there is a touch on the screen.
 */
var trackedTouchCount = 0;

/**
 * Last reported number of active touches.
 */
var previousActiveTouches = 0;

var changeResponder = function(nextResponderInst, blockHostResponder) {
    var oldResponderInst = responderInst;
    responderInst = nextResponderInst;
    if (ResponderEventPlugin.GlobalResponderHandler !== null) {
        ResponderEventPlugin.GlobalResponderHandler.onChange(oldResponderInst, nextResponderInst, blockHostResponder);
    }
};

var eventTypes = {
    /**
     * On a `touchStart`/`mouseDown`, is it desired that this element become the
     * responder?
     */
    startShouldSetResponder: {
        phasedRegistrationNames: {
            bubbled: 'onStartShouldSetResponder',
            captured: 'onStartShouldSetResponderCapture'
        }
    },

    /**
     * On a `scroll`, is it desired that this element become the responder? This
     * is usually not needed, but should be used to retroactively infer that a
     * `touchStart` had occurred during momentum scroll. During a momentum scroll,
     * a touch start will be immediately followed by a scroll event if the view is
     * currently scrolling.
     *
     * TODO: This shouldn't bubble.
     */
    scrollShouldSetResponder: {
        phasedRegistrationNames: {
            bubbled: 'onScrollShouldSetResponder',
            captured: 'onScrollShouldSetResponderCapture'
        }
    },

    /**
     * On text selection change, should this element become the responder? This
     * is needed for text inputs or other views with native selection, so the
     * JS view can claim the responder.
     *
     * TODO: This shouldn't bubble.
     */
    selectionChangeShouldSetResponder: {
        phasedRegistrationNames: {
            bubbled: 'onSelectionChangeShouldSetResponder',
            captured: 'onSelectionChangeShouldSetResponderCapture',
        }
    },

    /**
     * On a `touchMove`/`mouseMove`, is it desired that this element become the
     * responder?
     */
    moveShouldSetResponder: {
        phasedRegistrationNames: {
            bubbled: 'onMoveShouldSetResponder',
            captured: 'onMoveShouldSetResponderCapture'
        }
    },

    /**
     * Direct responder events dispatched directly to responder. Do not bubble.
     */
    responderStart: { registrationName: 'onResponderStart' },
    responderMove: { registrationName: 'onResponderMove' },
    responderEnd: { registrationName: 'onResponderEnd' },
    responderRelease: { registrationName: 'onResponderRelease' },
    responderTerminationRequest: {
        registrationName: 'onResponderTerminationRequest'
    },
    responderGrant: { registrationName: 'onResponderGrant' },
    responderReject: { registrationName: 'onResponderReject' },
    responderTerminate: { registrationName: 'onResponderTerminate' }
};

/**
 *
 * Responder System:
 * ----------------
 *
 * - A global, solitary "interaction lock" on a view.
 * - If a node becomes the responder, it should convey visual feedback
 *   immediately to indicate so, either by highlighting or moving accordingly.
 * - To be the responder means, that touches are exclusively important to that
 *   responder view, and no other view.
 * - While touches are still occurring, the responder lock can be transferred to
 *   a new view, but only to increasingly "higher" views (meaning ancestors of
 *   the current responder).
 *
 * Responder being granted:
 * ------------------------
 *
 * - Touch starts, moves, and scrolls can cause an ID to become the responder.
 * - We capture/bubble `startShouldSetResponder`/`moveShouldSetResponder` to
 *   the "appropriate place".
 * - If nothing is currently the responder, the "appropriate place" is the
 *   initiating event's `targetID`.
 * - If something *is* already the responder, the "appropriate place" is the
 *   first common ancestor of the event target and the current `responderInst`.
 * - Some negotiation happens: See the timing diagram below.
 * - Scrolled views automatically become responder. The reasoning is that a
 *   platform scroll view that isn't built on top of the responder system has
 *   began scrolling, and the active responder must now be notified that the
 *   interaction is no longer locked to it - the system has taken over.
 *
 * - Responder being released:
 *   As soon as no more touches that *started* inside of descendants of the
 *   *current* responderInst, an `onResponderRelease` event is dispatched to the
 *   current responder, and the responder lock is released.
 *
 * TODO:
 * - on "end", a callback hook for `onResponderEndShouldRemainResponder` that
 *   determines if the responder lock should remain.
 * - If a view shouldn't "remain" the responder, any active touches should by
 *   default be considered "dead" and do not influence future negotiations or
 *   bubble paths. It should be as if those touches do not exist.
 * -- For multitouch: Usually a translate-z will choose to "remain" responder
 *  after one out of many touches ended. For translate-y, usually the view
 *  doesn't wish to "remain" responder after one of many touches end.
 * - Consider building this on top of a `stopPropagation` model similar to
 *   `W3C` events.
 * - Ensure that `onResponderTerminate` is called on touch cancels, whether or
 *   not `onResponderTerminationRequest` returns `true` or `false`.
 *
 */

/*                                             Negotiation Performed
                                             +-----------------------+
                                            /                         \
Process low level events to    +     Current Responder      +   wantsResponderID
determine who to perform negot-|   (if any exists at all)   |
iation/transition              | Otherwise just pass through|
-------------------------------+----------------------------+------------------+
Bubble to find first ID        |                            |
to return true:wantsResponderID|                            |
                               |                            |
     +-------------+           |                            |
     | onTouchStart|           |                            |
     +------+------+     none  |                            |
            |            return|                            |
+-----------v-------------+true| +------------------------+ |
|onStartShouldSetResponder|----->|onResponderStart (cur)  |<-----------+
+-----------+-------------+    | +------------------------+ |          |
            |                  |                            | +--------+-------+
            | returned true for|       false:REJECT +-------->|onResponderReject
            | wantsResponderID |                    |       | +----------------+
            | (now attempt     | +------------------+-----+ |
            |  handoff)        | |   onResponder          | |
            +------------------->|      TerminationRequest| |
                               | +------------------+-----+ |
                               |                    |       | +----------------+
                               |         true:GRANT +-------->|onResponderGrant|
                               |                            | +--------+-------+
                               | +------------------------+ |          |
                               | |   onResponderTerminate |<-----------+
                               | +------------------+-----+ |
                               |                    |       | +----------------+
                               |                    +-------->|onResponderStart|
                               |                            | +----------------+
Bubble to find first ID        |                            |
to return true:wantsResponderID|                            |
                               |                            |
     +-------------+           |                            |
     | onTouchMove |           |                            |
     +------+------+     none  |                            |
            |            return|                            |
+-----------v-------------+true| +------------------------+ |
|onMoveShouldSetResponder |----->|onResponderMove (cur)   |<-----------+
+-----------+-------------+    | +------------------------+ |          |
            |                  |                            | +--------+-------+
            | returned true for|       false:REJECT +-------->|onResponderRejec|
            | wantsResponderID |                    |       | +----------------+
            | (now attempt     | +------------------+-----+ |
            |  handoff)        | |   onResponder          | |
            +------------------->|      TerminationRequest| |
                               | +------------------+-----+ |
                               |                    |       | +----------------+
                               |         true:GRANT +-------->|onResponderGrant|
                               |                            | +--------+-------+
                               | +------------------------+ |          |
                               | |   onResponderTerminate |<-----------+
                               | +------------------+-----+ |
                               |                    |       | +----------------+
                               |                    +-------->|onResponderMove |
                               |                            | +----------------+
                               |                            |
                               |                            |
      Some active touch started|                            |
      inside current responder | +------------------------+ |
      +------------------------->|      onResponderEnd    | |
      |                        | +------------------------+ |
  +---+---------+              |                            |
  | onTouchEnd  |              |                            |
  +---+---------+              |                            |
      |                        | +------------------------+ |
      +------------------------->|     onResponderEnd     | |
      No active touches started| +-----------+------------+ |
      inside current responder |             |              |
                               |             v              |
                               | +------------------------+ |
                               | |    onResponderRelease  | |
                               | +------------------------+ |
                               |                            |
                               +                            + */

/**
 * A note about event ordering in the `EventPluginHub`.
 *
 * Suppose plugins are injected in the following order:
 *
 * `[R, S, C]`
 *
 * To help illustrate the example, assume `S` is `SimpleEventPlugin` (for
 * `onClick` etc) and `R` is `ResponderEventPlugin`.
 *
 * "Deferred-Dispatched Events":
 *
 * - The current event plugin system will traverse the list of injected plugins,
 *   in order, and extract events by collecting the plugin's return value of
 *   `extractEvents()`.
 * - These events that are returned from `extractEvents` are "deferred
 *   dispatched events".
 * - When returned from `extractEvents`, deferred-dispatched events contain an
 *   "accumulation" of deferred dispatches.
 * - These deferred dispatches are accumulated/collected before they are
 *   returned, but processed at a later time by the `EventPluginHub` (hence the
 *   name deferred).
 *
 * In the process of returning their deferred-dispatched events, event plugins
 * themselves can dispatch events on-demand without returning them from
 * `extractEvents`. Plugins might want to do this, so that they can use event
 * dispatching as a tool that helps them decide which events should be extracted
 * in the first place.
 *
 * "On-Demand-Dispatched Events":
 *
 * - On-demand-dispatched events are not returned from `extractEvents`.
 * - On-demand-dispatched events are dispatched during the process of returning
 *   the deferred-dispatched events.
 * - They should not have side effects.
 * - They should be avoided, and/or eventually be replaced with another
 *   abstraction that allows event plugins to perform multiple "rounds" of event
 *   extraction.
 *
 * Therefore, the sequence of event dispatches becomes:
 *
 * - `R`s on-demand events (if any)   (dispatched by `R` on-demand)
 * - `S`s on-demand events (if any)   (dispatched by `S` on-demand)
 * - `C`s on-demand events (if any)   (dispatched by `C` on-demand)
 * - `R`s extracted events (if any)   (dispatched by `EventPluginHub`)
 * - `S`s extracted events (if any)   (dispatched by `EventPluginHub`)
 * - `C`s extracted events (if any)   (dispatched by `EventPluginHub`)
 *
 * In the case of `ResponderEventPlugin`: If the `startShouldSetResponder`
 * on-demand dispatch returns `true` (and some other details are satisfied) the
 * `onResponderGrant` deferred dispatched event is returned from
 * `extractEvents`. The sequence of dispatch executions in this case
 * will appear as follows:
 *
 * - `startShouldSetResponder` (`ResponderEventPlugin` dispatches on-demand)
 * - `touchStartCapture`       (`EventPluginHub` dispatches as usual)
 * - `touchStart`              (`EventPluginHub` dispatches as usual)
 * - `responderGrant/Reject`   (`EventPluginHub` dispatches as usual)
 */

function setResponderAndExtractTransfer(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
    // for Responder topMouseMove only triggering after topMouseDown should listen
    if (topLevelType === topMouseMove && !ResponderEventPlugin.isMouseDown) return null
    var shouldSetEventType = isStartish(topLevelType) ? eventTypes.startShouldSetResponder : isMoveish(topLevelType) ? eventTypes.moveShouldSetResponder : topLevelType === 'topSelectionChange' ?
        eventTypes.selectionChangeShouldSetResponder : eventTypes.scrollShouldSetResponder;

    // TODO: stop one short of the current responder.
    var bubbleShouldSetFrom = !responderInst ? targetInst : EventPluginUtils.getLowestCommonAncestor(responderInst, targetInst);

    // When capturing/bubbling the "shouldSet" event, we want to skip the target
    // (deepest ID) if it happens to be the current responder. The reasoning:
    // It's strange to get an `onMoveShouldSetResponder` when you're *already*
    // the responder.
    var skipOverBubbleShouldSetFrom = bubbleShouldSetFrom === responderInst;
    var shouldSetEvent = ResponderSyntheticEvent.getPooled(shouldSetEventType, bubbleShouldSetFrom, nativeEvent, nativeEventTarget);
    shouldSetEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
    if (skipOverBubbleShouldSetFrom) {
        EventPropagators.accumulateTwoPhaseDispatchesSkipTarget(shouldSetEvent);
    } else {
        EventPropagators.accumulateTwoPhaseDispatches(shouldSetEvent);
    }
    var wantsResponderInst = executeDispatchesInOrderStopAtTrue(shouldSetEvent);
    if (!shouldSetEvent.isPersistent()) {
        shouldSetEvent.constructor.release(shouldSetEvent);
    }

    if (!wantsResponderInst || wantsResponderInst === responderInst) {
        return null;
    }
    var extracted;
    var grantEvent = ResponderSyntheticEvent.getPooled(eventTypes.responderGrant, wantsResponderInst, nativeEvent, nativeEventTarget);
    grantEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;

    EventPropagators.accumulateDirectDispatches(grantEvent);
    var blockHostResponder = executeDirectDispatch(grantEvent) === true;
    if (responderInst) {

        var terminationRequestEvent = ResponderSyntheticEvent.getPooled(eventTypes.responderTerminationRequest, responderInst, nativeEvent, nativeEventTarget);
        terminationRequestEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
        EventPropagators.accumulateDirectDispatches(terminationRequestEvent);
        var shouldSwitch = !hasDispatches(terminationRequestEvent) || executeDirectDispatch(terminationRequestEvent);
        if (!terminationRequestEvent.isPersistent()) {
            terminationRequestEvent.constructor.release(terminationRequestEvent);
        }

        if (shouldSwitch) {
            var terminateEvent = ResponderSyntheticEvent.getPooled(eventTypes.responderTerminate, responderInst, nativeEvent, nativeEventTarget);
            terminateEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
            EventPropagators.accumulateDirectDispatches(terminateEvent);
            extracted = accumulate(extracted, [grantEvent, terminateEvent]);
            changeResponder(wantsResponderInst, blockHostResponder);
        } else {
            var rejectEvent = ResponderSyntheticEvent.getPooled(eventTypes.responderReject, wantsResponderInst, nativeEvent, nativeEventTarget);
            rejectEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
            EventPropagators.accumulateDirectDispatches(rejectEvent);
            extracted = accumulate(extracted, rejectEvent);
        }
    } else {
        extracted = accumulate(extracted, grantEvent);
        changeResponder(wantsResponderInst, blockHostResponder);
    }
    return extracted;
}

/**
 * A transfer is a negotiation between a currently set responder and the next
 * element to claim responder status. Any start event could trigger a transfer
 * of responderInst. Any move event could trigger a transfer.
 *
 * @param {string} topLevelType Record from `EventConstants`.
 * @return {boolean} True if a transfer of responder could possibly occur.
 */
function canTriggerTransfer(topLevelType, topLevelInst, nativeEvent) {
    return topLevelInst && (
        // responderIgnoreScroll: We are trying to migrate away from specifically
        // tracking native scroll events here and responderIgnoreScroll indicates we
        // will send topTouchCancel to handle canceling touch events instead
        topLevelType === 'topScroll' && !nativeEvent.responderIgnoreScroll || trackedTouchCount > 0 && topLevelType === 'topSelectionChange' || isStartish(topLevelType) || isMoveish(topLevelType));
}

/**
 * Returns whether or not this touch end event makes it such that there are no
 * longer any touches that started inside of the current `responderInst`.
 *
 * @param {NativeEvent} nativeEvent Native touch end event.
 * @return {boolean} Whether or not this touch end event ends the responder.
 */
function noResponderTouches(nativeEvent) {
    var touches = nativeEvent.touches;
    if (!touches || touches.length === 0) {
        return true;
    }
    for (var i = 0; i < touches.length; i++) {
        var activeTouch = touches[i];
        var target = activeTouch.target;
        if (target !== null && target !== undefined && target !== 0) {
            // Is the original touch location inside of the current responder?
            var targetInst = EventPluginUtils.getInstanceFromNode(target);
            if (EventPluginUtils.isAncestor(responderInst, targetInst)) {
                return false;
            }
        }
    }
    return true;
}
var ResponderEventPlugin = {
    isMouseDown: touchSupport, // to support PC
    /* For unit testing only */
    _getResponderID: function() {
        return responderInst ? responderInst._rootNodeID : null;
    },

    eventTypes: eventTypes,

    /**
     * We must be resilient to `targetInst` being `null` on `touchMove` or
     * `touchEnd`. On certain platforms, this means that a native scroll has
     * assumed control and the original touch targets are destroyed.
     */
    extractEvents: function(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
        switch (topLevelType) {
          case topMouseDown: ResponderEventPlugin.isMouseDown = true;break;
          // while using debugger tool detecting ele, only mouseup will trigger
          case topMouseUp: if (!ResponderEventPlugin.isMouseDown) return null;ResponderEventPlugin.isMouseDown = false;break;
        }
        if (isStartish(topLevelType)) {
            trackedTouchCount += 1;
        } else if (isEndish(topLevelType)) {
            if (trackedTouchCount >= 0) {
                trackedTouchCount -= 1;
            } else {
                console.error('Ended a touch event which was not counted in `trackedTouchCount`.');
                return null;
            }
        }
        ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent);

        var extracted = canTriggerTransfer(topLevelType, targetInst, nativeEvent) ? setResponderAndExtractTransfer(topLevelType, targetInst, nativeEvent, nativeEventTarget) : null;
        // Responder may or may not have transferred on a new touch start/move.
        // Regardless, whoever is the responder after any potential transfer, we
        // direct all touch start/move/ends to them in the form of
        // `onResponderMove/Start/End`. These will be called for *every* additional
        // finger that move/start/end, dispatched directly to whoever is the
        // current responder at that moment, until the responder is "released".
        //
        // These multiple individual change touch events are are always bookended
        // by `onResponderGrant`, and one of
        // (`onResponderRelease/onResponderTerminate`).
        var isResponderTouchStart = responderInst && isStartish(topLevelType);
        var isResponderTouchMove = responderInst && isMoveish(topLevelType) && ResponderEventPlugin.isMouseDown;
        var isResponderTouchEnd = responderInst && isEndish(topLevelType);
        var incrementalTouch = isResponderTouchStart ? eventTypes.responderStart : isResponderTouchMove ? eventTypes.responderMove : isResponderTouchEnd ? eventTypes.responderEnd : null;

        if (incrementalTouch) {
            var gesture = ResponderSyntheticEvent.getPooled(incrementalTouch, responderInst, nativeEvent, nativeEventTarget);
            gesture.touchHistory = ResponderTouchHistoryStore.touchHistory;
            EventPropagators.accumulateDirectDispatches(gesture);
            extracted = accumulate(extracted, gesture);
        }

        var isResponderTerminate = responderInst && topLevelType === 'topTouchCancel';
        var isResponderRelease = responderInst && !isResponderTerminate && isEndish(topLevelType) && noResponderTouches(nativeEvent);
        var finalTouch = isResponderTerminate ? eventTypes.responderTerminate : isResponderRelease ? eventTypes.responderRelease : null;
        if (finalTouch) {
            var finalEvent = ResponderSyntheticEvent.getPooled(finalTouch, responderInst, nativeEvent, nativeEventTarget);
            finalEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
            EventPropagators.accumulateDirectDispatches(finalEvent);
            extracted = accumulate(extracted, finalEvent);
            changeResponder(null);
        }

        var numberActiveTouches = ResponderTouchHistoryStore.touchHistory.numberActiveTouches;
        if (ResponderEventPlugin.GlobalInteractionHandler && numberActiveTouches !== previousActiveTouches) {
            ResponderEventPlugin.GlobalInteractionHandler.onChange(numberActiveTouches);
        }
        previousActiveTouches = numberActiveTouches;

        return extracted;
    },

    GlobalResponderHandler: null,
    GlobalInteractionHandler: null,

    injection: {
        /**
         * @param {{onChange: (ReactID, ReactID) => void} GlobalResponderHandler
         * Object that handles any change in responder. Use this to inject
         * integration with an existing touch handling system etc.
         */
        injectGlobalResponderHandler: function(GlobalResponderHandler) {
            ResponderEventPlugin.GlobalResponderHandler = GlobalResponderHandler;
        },

        /**
         * @param {{onChange: (numberActiveTouches) => void} GlobalInteractionHandler
         * Object that handles any change in the number of active touches.
         */
        injectGlobalInteractionHandler: function(GlobalInteractionHandler) {
            ResponderEventPlugin.GlobalInteractionHandler = GlobalInteractionHandler;
        }
    }
};

module.exports = require('qreact').ResponderEventPlugin = ResponderEventPlugin;

