/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 * @flow
 */

'use strict';

const React = require('react');
const Dimensions = require('../Utilities/Dimensions');
const FrameRateLogger = require('../Interaction/FrameRateLogger');
const Keyboard = require('./Keyboard/Keyboard');
const ReactNative = require('../Renderer/shims/ReactNative');
const TextInputState = require('./TextInput/TextInputState');
const UIManager = require('../ReactNative/UIManager');
const Platform = require('../Utilities/Platform');
import Commands from './ScrollView/ScrollViewCommands';

const invariant = require('invariant');
const performanceNow = require('fbjs/lib/performanceNow');

import type {PressEvent, ScrollEvent} from '../Types/CoreEventTypes';
import typeof ScrollView from './ScrollView/ScrollView';
import type {Props as ScrollViewProps} from './ScrollView/ScrollView';
import type {KeyboardEvent} from './Keyboard/Keyboard';
import type EmitterSubscription from '../vendor/emitter/EmitterSubscription';
import type {HostComponent} from '../Renderer/shims/ReactNativeTypes';

/**
 * Mixin that can be integrated in order to handle scrolling that plays well
 * with `ResponderEventPlugin`. Integrate with your platform specific scroll
 * views, or even your custom built (every-frame animating) scroll views so that
 * all of these systems play well with the `ResponderEventPlugin`.
 *
 * iOS scroll event timing nuances:
 * ===============================
 *
 *
 * Scrolling without bouncing, if you touch down:
 * -------------------------------
 *
 * 1. `onMomentumScrollBegin` (when animation begins after letting up)
 *    ... physical touch starts ...
 * 2. `onTouchStartCapture`   (when you press down to stop the scroll)
 * 3. `onTouchStart`          (same, but bubble phase)
 * 4. `onResponderRelease`    (when lifting up - you could pause forever before * lifting)
 * 5. `onMomentumScrollEnd`
 *
 *
 * Scrolling with bouncing, if you touch down:
 * -------------------------------
 *
 * 1. `onMomentumScrollBegin` (when animation begins after letting up)
 *    ... bounce begins ...
 *    ... some time elapses ...
 *    ... physical touch during bounce ...
 * 2. `onMomentumScrollEnd`   (Makes no sense why this occurs first during bounce)
 * 3. `onTouchStartCapture`   (immediately after `onMomentumScrollEnd`)
 * 4. `onTouchStart`          (same, but bubble phase)
 * 5. `onTouchEnd`            (You could hold the touch start for a long time)
 * 6. `onMomentumScrollBegin` (When releasing the view starts bouncing back)
 *
 * So when we receive an `onTouchStart`, how can we tell if we are touching
 * *during* an animation (which then causes the animation to stop)? The only way
 * to tell is if the `touchStart` occurred immediately after the
 * `onMomentumScrollEnd`.
 *
 * This is abstracted out for you, so you can just call this.scrollResponderIsAnimating() if
 * necessary
 *
 * `ScrollResponder` also includes logic for blurring a currently focused input
 * if one is focused while scrolling. The `ScrollResponder` is a natural place
 * to put this logic since it can support not dismissing the keyboard while
 * scrolling, unless a recognized "tap"-like gesture has occurred.
 *
 * The public lifecycle API includes events for keyboard interaction, responder
 * interaction, and scrolling (among others). The keyboard callbacks
 * `onKeyboardWill/Did/*` are *global* events, but are invoked on scroll
 * responder's props so that you can guarantee that the scroll responder's
 * internal state has been updated accordingly (and deterministically) by
 * the time the props callbacks are invoke. Otherwise, you would always wonder
 * if the scroll responder is currently in a state where it recognizes new
 * keyboard positions etc. If coordinating scrolling with keyboard movement,
 * *always* use these hooks instead of listening to your own global keyboard
 * events.
 *
 * Public keyboard lifecycle API: (props callbacks)
 *
 * Standard Keyboard Appearance Sequence:
 *
 *   this.props.onKeyboardWillShow
 *   this.props.onKeyboardDidShow
 *
 * `onScrollResponderKeyboardDismissed` will be invoked if an appropriate
 * tap inside the scroll responder's scrollable region was responsible
 * for the dismissal of the keyboard. There are other reasons why the
 * keyboard could be dismissed.
 *
 *   this.props.onScrollResponderKeyboardDismissed
 *
 * Standard Keyboard Hide Sequence:
 *
 *   this.props.onKeyboardWillHide
 *   this.props.onKeyboardDidHide
 */

const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16;

export type State = {|
  isTouching: boolean,
  lastMomentumScrollBeginTime: number,
  lastMomentumScrollEndTime: number,
  observedScrollSinceBecomingResponder: boolean,
  becameResponderWhileAnimating: boolean,
|};

const ScrollResponderMixin = {
  _subscriptionKeyboardWillShow: (null: ?EmitterSubscription),
  _subscriptionKeyboardWillHide: (null: ?EmitterSubscription),
  _subscriptionKeyboardDidShow: (null: ?EmitterSubscription),
  _subscriptionKeyboardDidHide: (null: ?EmitterSubscription),
  scrollResponderMixinGetInitialState: function(): State {
    return {
      isTouching: false,
      lastMomentumScrollBeginTime: 0,
      lastMomentumScrollEndTime: 0,

      // Reset to false every time becomes responder. This is used to:
      // - Determine if the scroll view has been scrolled and therefore should
      // refuse to give up its responder lock.
      // - Determine if releasing should dismiss the keyboard when we are in
      // tap-to-dismiss mode (this.props.keyboardShouldPersistTaps !== 'always').
      observedScrollSinceBecomingResponder: false,
      becameResponderWhileAnimating: false,
    };
  },

  /**
   * Invoke this from an `onScroll` event.
   */
  scrollResponderHandleScrollShouldSetResponder: function(): boolean {
    // Allow any event touch pass through if the default pan responder is disabled
    if (this.props.disableScrollViewPanResponder === true) {
      return false;
    }
    return this.state.isTouching;
  },

  /**
   * Merely touch starting is not sufficient for a scroll view to become the
   * responder. Being the "responder" means that the very next touch move/end
   * event will result in an action/movement.
   *
   * Invoke this from an `onStartShouldSetResponder` event.
   *
   * `onStartShouldSetResponder` is used when the next move/end will trigger
   * some UI movement/action, but when you want to yield priority to views
   * nested inside of the view.
   *
   * There may be some cases where scroll views actually should return `true`
   * from `onStartShouldSetResponder`: Any time we are detecting a standard tap
   * that gives priority to nested views.
   *
   * - If a single tap on the scroll view triggers an action such as
   *   recentering a map style view yet wants to give priority to interaction
   *   views inside (such as dropped pins or labels), then we would return true
   *   from this method when there is a single touch.
   *
   * - Similar to the previous case, if a two finger "tap" should trigger a
   *   zoom, we would check the `touches` count, and if `>= 2`, we would return
   *   true.
   *
   */
  scrollResponderHandleStartShouldSetResponder: function(
    e: PressEvent,
  ): boolean {
    // Allow any event touch pass through if the default pan responder is disabled
    if (this.props.disableScrollViewPanResponder === true) {
      return false;
    }

    const currentlyFocusedInput = TextInputState.currentlyFocusedInput();

    if (
      this.props.keyboardShouldPersistTaps === 'handled' &&
      currentlyFocusedInput != null &&
      e.target !== currentlyFocusedInput
    ) {
      return true;
    }
    return false;
  },

  /**
   * There are times when the scroll view wants to become the responder
   * (meaning respond to the next immediate `touchStart/touchEnd`), in a way
   * that *doesn't* give priority to nested views (hence the capture phase):
   *
   * - Currently animating.
   * - Tapping anywhere that is not a text input, while the keyboard is
   *   up (which should dismiss the keyboard).
   *
   * Invoke this from an `onStartShouldSetResponderCapture` event.
   */
  scrollResponderHandleStartShouldSetResponderCapture: function(
    e: PressEvent,
  ): boolean {
    // The scroll view should receive taps instead of its descendants if:
    // * it is already animating/decelerating
    if (this.scrollResponderIsAnimating()) {
      return true;
    }

    // Allow any event touch pass through if the default pan responder is disabled
    if (this.props.disableScrollViewPanResponder === true) {
      return false;
    }

    // * the keyboard is up, keyboardShouldPersistTaps is 'never' (the default),
    // and a new touch starts with a non-textinput target (in which case the
    // first tap should be sent to the scroll view and dismiss the keyboard,
    // then the second tap goes to the actual interior view)
    const currentlyFocusedTextInput = TextInputState.currentlyFocusedInput();
    const {keyboardShouldPersistTaps} = this.props;
    const keyboardNeverPersistTaps =
      !keyboardShouldPersistTaps || keyboardShouldPersistTaps === 'never';

    if (typeof e.target === 'number') {
      if (__DEV__) {
        console.error(
          'Did not expect event target to be a number. Should have been a native component',
        );
      }

      return false;
    }

    if (
      keyboardNeverPersistTaps &&
      currentlyFocusedTextInput != null &&
      e.target != null &&
      !TextInputState.isTextInput(e.target)
    ) {
      return true;
    }

    return false;
  },

  /**
   * Invoke this from an `onResponderReject` event.
   *
   * Some other element is not yielding its role as responder. Normally, we'd
   * just disable the `UIScrollView`, but a touch has already began on it, the
   * `UIScrollView` will not accept being disabled after that. The easiest
   * solution for now is to accept the limitation of disallowing this
   * altogether. To improve this, find a way to disable the `UIScrollView` after
   * a touch has already started.
   */
  scrollResponderHandleResponderReject: function() {},

  /**
   * We will allow the scroll view to give up its lock iff it acquired the lock
   * during an animation. This is a very useful default that happens to satisfy
   * many common user experiences.
   *
   * - Stop a scroll on the left edge, then turn that into an outer view's
   *   backswipe.
   * - Stop a scroll mid-bounce at the top, continue pulling to have the outer
   *   view dismiss.
   * - However, without catching the scroll view mid-bounce (while it is
   *   motionless), if you drag far enough for the scroll view to become
   *   responder (and therefore drag the scroll view a bit), any backswipe
   *   navigation of a swipe gesture higher in the view hierarchy, should be
   *   rejected.
   */
  scrollResponderHandleTerminationRequest: function(): boolean {
    return !this.state.observedScrollSinceBecomingResponder;
  },

  /**
   * Invoke this from an `onTouchEnd` event.
   *
   * @param {PressEvent} e Event.
   */
  scrollResponderHandleTouchEnd: function(e: PressEvent) {
    const nativeEvent = e.nativeEvent;
    this.state.isTouching = nativeEvent.touches.length !== 0;
    this.props.onTouchEnd && this.props.onTouchEnd(e);
  },

  /**
   * Invoke this from an `onTouchCancel` event.
   *
   * @param {PressEvent} e Event.
   */
  scrollResponderHandleTouchCancel: function(e: PressEvent) {
    this.state.isTouching = false;
    this.props.onTouchCancel && this.props.onTouchCancel(e);
  },

  /**
   * Invoke this from an `onResponderRelease` event.
   */
  scrollResponderHandleResponderRelease: function(e: PressEvent) {
    this.props.onResponderRelease && this.props.onResponderRelease(e);

    if (typeof e.target === 'number') {
      if (__DEV__) {
        console.error(
          'Did not expect event target to be a number. Should have been a native component',
        );
      }

      return;
    }

    // By default scroll views will unfocus a textField
    // if another touch occurs outside of it
    const currentlyFocusedTextInput = TextInputState.currentlyFocusedInput();
    if (
      this.props.keyboardShouldPersistTaps !== true &&
      this.props.keyboardShouldPersistTaps !== 'always' &&
      currentlyFocusedTextInput != null &&
      e.target !== currentlyFocusedTextInput &&
      !this.state.observedScrollSinceBecomingResponder &&
      !this.state.becameResponderWhileAnimating
    ) {
      this.props.onScrollResponderKeyboardDismissed &&
        this.props.onScrollResponderKeyboardDismissed(e);
      TextInputState.blurTextInput(currentlyFocusedTextInput);
    }
  },

  scrollResponderHandleScroll: function(e: ScrollEvent) {
    (this: any).state.observedScrollSinceBecomingResponder = true;
    (this: any).props.onScroll && (this: any).props.onScroll(e);
  },

  /**
   * Invoke this from an `onResponderGrant` event.
   */
  scrollResponderHandleResponderGrant: function(e: ScrollEvent) {
    this.state.observedScrollSinceBecomingResponder = false;
    this.props.onResponderGrant && this.props.onResponderGrant(e);
    this.state.becameResponderWhileAnimating = this.scrollResponderIsAnimating();
  },

  /**
   * Unfortunately, `onScrollBeginDrag` also fires when *stopping* the scroll
   * animation, and there's not an easy way to distinguish a drag vs. stopping
   * momentum.
   *
   * Invoke this from an `onScrollBeginDrag` event.
   */
  scrollResponderHandleScrollBeginDrag: function(e: ScrollEvent) {
    FrameRateLogger.beginScroll(); // TODO: track all scrolls after implementing onScrollEndAnimation
    this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e);
  },

  /**
   * Invoke this from an `onScrollEndDrag` event.
   */
  scrollResponderHandleScrollEndDrag: function(e: ScrollEvent) {
    const {velocity} = e.nativeEvent;
    // - If we are animating, then this is a "drag" that is stopping the scrollview and momentum end
    //   will fire.
    // - If velocity is non-zero, then the interaction will stop when momentum scroll ends or
    //   another drag starts and ends.
    // - If we don't get velocity, better to stop the interaction twice than not stop it.
    if (
      !this.scrollResponderIsAnimating() &&
      (!velocity || (velocity.x === 0 && velocity.y === 0))
    ) {
      FrameRateLogger.endScroll();
    }
    this.props.onScrollEndDrag && this.props.onScrollEndDrag(e);
  },

  /**
   * Invoke this from an `onMomentumScrollBegin` event.
   */
  scrollResponderHandleMomentumScrollBegin: function(e: ScrollEvent) {
    this.state.lastMomentumScrollBeginTime = performanceNow();
    this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e);
  },

  /**
   * Invoke this from an `onMomentumScrollEnd` event.
   */
  scrollResponderHandleMomentumScrollEnd: function(e: ScrollEvent) {
    FrameRateLogger.endScroll();
    this.state.lastMomentumScrollEndTime = performanceNow();
    this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e);
  },

  /**
   * Invoke this from an `onTouchStart` event.
   *
   * Since we know that the `SimpleEventPlugin` occurs later in the plugin
   * order, after `ResponderEventPlugin`, we can detect that we were *not*
   * permitted to be the responder (presumably because a contained view became
   * responder). The `onResponderReject` won't fire in that case - it only
   * fires when a *current* responder rejects our request.
   *
   * @param {PressEvent} e Touch Start event.
   */
  scrollResponderHandleTouchStart: function(e: PressEvent) {
    this.state.isTouching = true;
    this.props.onTouchStart && this.props.onTouchStart(e);
  },

  /**
   * Invoke this from an `onTouchMove` event.
   *
   * Since we know that the `SimpleEventPlugin` occurs later in the plugin
   * order, after `ResponderEventPlugin`, we can detect that we were *not*
   * permitted to be the responder (presumably because a contained view became
   * responder). The `onResponderReject` won't fire in that case - it only
   * fires when a *current* responder rejects our request.
   *
   * @param {PressEvent} e Touch Start event.
   */
  scrollResponderHandleTouchMove: function(e: PressEvent) {
    this.props.onTouchMove && this.props.onTouchMove(e);
  },

  /**
   * A helper function for this class that lets us quickly determine if the
   * view is currently animating. This is particularly useful to know when
   * a touch has just started or ended.
   */
  scrollResponderIsAnimating: function(): boolean {
    const now = performanceNow();
    const timeSinceLastMomentumScrollEnd =
      now - this.state.lastMomentumScrollEndTime;
    const isAnimating =
      timeSinceLastMomentumScrollEnd < IS_ANIMATING_TOUCH_START_THRESHOLD_MS ||
      this.state.lastMomentumScrollEndTime <
        this.state.lastMomentumScrollBeginTime;
    return isAnimating;
  },

  /**
   * Returns the node that represents native view that can be scrolled.
   * Components can pass what node to use by defining a `getScrollableNode`
   * function otherwise `this` is used.
   */
  scrollResponderGetScrollableNode: function(): ?number {
    return this.getScrollableNode
      ? this.getScrollableNode()
      : ReactNative.findNodeHandle(this);
  },

  /**
   * A helper function to scroll to a specific point in the ScrollView.
   * This is currently used to help focus child TextViews, but can also
   * be used to quickly scroll to any element we want to focus. Syntax:
   *
   * `scrollResponderScrollTo(options: {x: number = 0; y: number = 0; animated: boolean = true})`
   *
   * Note: The weird argument signature is due to the fact that, for historical reasons,
   * the function also accepts separate arguments as as alternative to the options object.
   * This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED.
   */
  scrollResponderScrollTo: function(
    x?:
      | number
      | {
          x?: number,
          y?: number,
          animated?: boolean,
          ...
        },
    y?: number,
    animated?: boolean,
  ) {
    if (typeof x === 'number') {
      console.warn(
        '`scrollResponderScrollTo(x, y, animated)` is deprecated. Use `scrollResponderScrollTo({x: 5, y: 5, animated: true})` instead.',
      );
    } else {
      ({x, y, animated} = x || {});
    }

    const that: React.ElementRef<ScrollView> = (this: any);
    invariant(
      that.getNativeScrollRef != null,
      'Expected scrollTo to be called on a scrollViewRef. If this exception occurs it is likely a bug in React Native',
    );
    const nativeScrollRef = that.getNativeScrollRef();
    if (nativeScrollRef == null) {
      return;
    }
    Commands.scrollTo(nativeScrollRef, x || 0, y || 0, animated !== false);
  },

  /**
   * Scrolls to the end of the ScrollView, either immediately or with a smooth
   * animation.
   *
   * Example:
   *
   * `scrollResponderScrollToEnd({animated: true})`
   */
  scrollResponderScrollToEnd: function(options?: {animated?: boolean, ...}) {
    // Default to true
    const animated = (options && options.animated) !== false;

    const that: React.ElementRef<ScrollView> = (this: any);
    invariant(
      that.getNativeScrollRef != null,
      'Expected scrollToEnd to be called on a scrollViewRef. If this exception occurs it is likely a bug in React Native',
    );
    const nativeScrollRef = that.getNativeScrollRef();
    if (nativeScrollRef == null) {
      return;
    }

    Commands.scrollToEnd(nativeScrollRef, animated);
  },

  /**
   * A helper function to zoom to a specific rect in the scrollview. The argument has the shape
   * {x: number; y: number; width: number; height: number; animated: boolean = true}
   *
   * @platform ios
   */
  scrollResponderZoomTo: function(
    rect: {|
      x: number,
      y: number,
      width: number,
      height: number,
      animated?: boolean,
    |},
    animated?: boolean, // deprecated, put this inside the rect argument instead
  ) {
    invariant(Platform.OS === 'ios', 'zoomToRect is not implemented');
    if ('animated' in rect) {
      animated = rect.animated;
      delete rect.animated;
    } else if (typeof animated !== 'undefined') {
      console.warn(
        '`scrollResponderZoomTo` `animated` argument is deprecated. Use `options.animated` instead',
      );
    }

    const that: React.ElementRef<ScrollView> = this;
    invariant(
      that.getNativeScrollRef != null,
      'Expected zoomToRect to be called on a scrollViewRef. If this exception occurs it is likely a bug in React Native',
    );
    const nativeScrollRef = that.getNativeScrollRef();
    if (nativeScrollRef == null) {
      return;
    }
    Commands.zoomToRect(nativeScrollRef, rect, animated !== false);
  },

  /**
   * Displays the scroll indicators momentarily.
   */
  scrollResponderFlashScrollIndicators: function() {
    const that: React.ElementRef<ScrollView> = (this: any);
    invariant(
      that.getNativeScrollRef != null,
      'Expected flashScrollIndicators to be called on a scrollViewRef. If this exception occurs it is likely a bug in React Native',
    );
    const nativeScrollRef = that.getNativeScrollRef();
    if (nativeScrollRef == null) {
      return;
    }
    Commands.flashScrollIndicators(nativeScrollRef);
  },

  /**
   * This method should be used as the callback to onFocus in a TextInputs'
   * parent view. Note that any module using this mixin needs to return
   * the parent view's ref in getScrollViewRef() in order to use this method.
   * @param {number} nodeHandle The TextInput node handle
   * @param {number} additionalOffset The scroll view's bottom "contentInset".
   *        Default is 0.
   * @param {bool} preventNegativeScrolling Whether to allow pulling the content
   *        down to make it meet the keyboard's top. Default is false.
   */
  scrollResponderScrollNativeHandleToKeyboard: function<T>(
    nodeHandle: number | React.ElementRef<HostComponent<T>>,
    additionalOffset?: number,
    preventNegativeScrollOffset?: boolean,
  ) {
    this.additionalScrollOffset = additionalOffset || 0;
    this.preventNegativeScrollOffset = !!preventNegativeScrollOffset;

    if (typeof nodeHandle === 'number') {
      UIManager.measureLayout(
        nodeHandle,
        ReactNative.findNodeHandle(this.getInnerViewNode()),
        this.scrollResponderTextInputFocusError,
        this.scrollResponderInputMeasureAndScrollToKeyboard,
      );
    } else {
      const innerRef = this.getInnerViewRef();

      if (innerRef == null) {
        return;
      }

      nodeHandle.measureLayout(
        innerRef,
        this.scrollResponderInputMeasureAndScrollToKeyboard,
        this.scrollResponderTextInputFocusError,
      );
    }
  },

  /**
   * The calculations performed here assume the scroll view takes up the entire
   * screen - even if has some content inset. We then measure the offsets of the
   * keyboard, and compensate both for the scroll view's "contentInset".
   *
   * @param {number} left Position of input w.r.t. table view.
   * @param {number} top Position of input w.r.t. table view.
   * @param {number} width Width of the text input.
   * @param {number} height Height of the text input.
   */
  scrollResponderInputMeasureAndScrollToKeyboard: function(
    left: number,
    top: number,
    width: number,
    height: number,
  ) {
    let keyboardScreenY = Dimensions.get('window').height;
    if (this.keyboardWillOpenTo) {
      keyboardScreenY = this.keyboardWillOpenTo.endCoordinates.screenY;
    }
    let scrollOffsetY =
      top - keyboardScreenY + height + this.additionalScrollOffset;

    // By default, this can scroll with negative offset, pulling the content
    // down so that the target component's bottom meets the keyboard's top.
    // If requested otherwise, cap the offset at 0 minimum to avoid content
    // shifting down.
    if (this.preventNegativeScrollOffset) {
      scrollOffsetY = Math.max(0, scrollOffsetY);
    }
    this.scrollResponderScrollTo({x: 0, y: scrollOffsetY, animated: true});

    this.additionalOffset = 0;
    this.preventNegativeScrollOffset = false;
  },

  scrollResponderTextInputFocusError: function(msg: string) {
    console.error('Error measuring text field: ', msg);
  },

  /**
   * `componentWillMount` is the closest thing to a  standard "constructor" for
   * React components.
   *
   * The `keyboardWillShow` is called before input focus.
   */
  UNSAFE_componentWillMount: function() {
    const {keyboardShouldPersistTaps} = ((this: any).props: ScrollViewProps);
    if (typeof keyboardShouldPersistTaps === 'boolean') {
      console.warn(
        `'keyboardShouldPersistTaps={${
          keyboardShouldPersistTaps === true ? 'true' : 'false'
        }}' is deprecated. ` +
          `Use 'keyboardShouldPersistTaps="${
            keyboardShouldPersistTaps ? 'always' : 'never'
          }"' instead`,
      );
    }

    (this: any).keyboardWillOpenTo = null;
    (this: any).additionalScrollOffset = 0;
    this._subscriptionKeyboardWillShow = Keyboard.addListener(
      'keyboardWillShow',
      this.scrollResponderKeyboardWillShow,
    );

    this._subscriptionKeyboardWillHide = Keyboard.addListener(
      'keyboardWillHide',
      this.scrollResponderKeyboardWillHide,
    );
    this._subscriptionKeyboardDidShow = Keyboard.addListener(
      'keyboardDidShow',
      this.scrollResponderKeyboardDidShow,
    );
    this._subscriptionKeyboardDidHide = Keyboard.addListener(
      'keyboardDidHide',
      this.scrollResponderKeyboardDidHide,
    );
  },

  componentWillUnmount: function() {
    if (this._subscriptionKeyboardWillShow != null) {
      this._subscriptionKeyboardWillShow.remove();
    }
    if (this._subscriptionKeyboardWillHide != null) {
      this._subscriptionKeyboardWillHide.remove();
    }
    if (this._subscriptionKeyboardDidShow != null) {
      this._subscriptionKeyboardDidShow.remove();
    }
    if (this._subscriptionKeyboardDidHide != null) {
      this._subscriptionKeyboardDidHide.remove();
    }
  },

  /**
   * Warning, this may be called several times for a single keyboard opening.
   * It's best to store the information in this method and then take any action
   * at a later point (either in `keyboardDidShow` or other).
   *
   * Here's the order that events occur in:
   * - focus
   * - willShow {startCoordinates, endCoordinates} several times
   * - didShow several times
   * - blur
   * - willHide {startCoordinates, endCoordinates} several times
   * - didHide several times
   *
   * The `ScrollResponder` module callbacks for each of these events.
   * Even though any user could have easily listened to keyboard events
   * themselves, using these `props` callbacks ensures that ordering of events
   * is consistent - and not dependent on the order that the keyboard events are
   * subscribed to. This matters when telling the scroll view to scroll to where
   * the keyboard is headed - the scroll responder better have been notified of
   * the keyboard destination before being instructed to scroll to where the
   * keyboard will be. Stick to the `ScrollResponder` callbacks, and everything
   * will work.
   *
   * WARNING: These callbacks will fire even if a keyboard is displayed in a
   * different navigation pane. Filter out the events to determine if they are
   * relevant to you. (For example, only if you receive these callbacks after
   * you had explicitly focused a node etc).
   */
  scrollResponderKeyboardWillShow: function(e: KeyboardEvent) {
    this.keyboardWillOpenTo = e;
    this.props.onKeyboardWillShow && this.props.onKeyboardWillShow(e);
  },

  scrollResponderKeyboardWillHide: function(e: KeyboardEvent) {
    this.keyboardWillOpenTo = null;
    this.props.onKeyboardWillHide && this.props.onKeyboardWillHide(e);
  },

  scrollResponderKeyboardDidShow: function(e: KeyboardEvent) {
    // TODO(7693961): The event for DidShow is not available on iOS yet.
    // Use the one from WillShow and do not assign.
    if (e) {
      this.keyboardWillOpenTo = e;
    }
    this.props.onKeyboardDidShow && this.props.onKeyboardDidShow(e);
  },

  scrollResponderKeyboardDidHide: function(e: KeyboardEvent) {
    this.keyboardWillOpenTo = null;
    this.props.onKeyboardDidHide && this.props.onKeyboardDidHide(e);
  },
};

const ScrollResponder = {
  Mixin: ScrollResponderMixin,
};

module.exports = ScrollResponder;
