/* @flow */

import * as React from 'react';
import PropTypes from 'prop-types';
import {
  Animated,
  I18nManager,
  PanResponder,
  StyleSheet,
  View,
  Platform,
} from 'react-native';
import { PagerRendererPropType } from './PropTypes';
import type { PagerRendererProps } from './TypeDefinitions';

type GestureEvent = {
  nativeEvent: {
    changedTouches: Array<*>,
    identifier: number,
    locationX: number,
    locationY: number,
    pageX: number,
    pageY: number,
    target: number,
    timestamp: number,
    touches: Array<*>,
  },
};

type GestureState = {
  stateID: number,
  moveX: number,
  moveY: number,
  x0: number,
  y0: number,
  dx: number,
  dy: number,
  vx: number,
  vy: number,
  numberActiveTouches: number,
};

type Props<T> = PagerRendererProps<T> & {
  swipeDistanceThreshold?: number,
  swipeVelocityThreshold?: number,
};

const DEAD_ZONE = 12;

const DefaultTransitionSpec = {
  timing: Animated.spring,
  tension: 300,
  friction: 35,
};

export default class PagerPan<T: *> extends React.Component<Props<T>> {
  static propTypes = {
    ...PagerRendererPropType,
    swipeDistanceThreshold: PropTypes.number,
    swipeVelocityThreshold: PropTypes.number,
  };

  static defaultProps = {
    canJumpToTab: () => true,
    initialLayout: {
      height: 0,
      width: 0,
    },
  };

  componentDidUpdate(prevProps: Props<T>) {
    this._currentIndex = this.props.navigationState.index;

    if (
      prevProps.navigationState.routes.length !==
        this.props.navigationState.routes.length ||
      prevProps.layout.width !== this.props.layout.width
    ) {
      this._transitionTo(this.props.navigationState.index, false);
    } else if (
      prevProps.navigationState.index !== this.props.navigationState.index
    ) {
      this._transitionTo(this.props.navigationState.index);
    }
  }

  _currentIndex = this.props.navigationState.index;
  _pendingIndex: ?number;

  _isMovingHorizontally = (evt: GestureEvent, gestureState: GestureState) => {
    return (
      Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 2) &&
      Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 2)
    );
  };

  _canMoveScreen = (evt: GestureEvent, gestureState: GestureState) => {
    if (this.props.swipeEnabled === false) {
      return false;
    }

    const {
      navigationState: { routes },
    } = this.props;

    return (
      this._isMovingHorizontally(evt, gestureState) &&
      ((gestureState.dx >= DEAD_ZONE && this._currentIndex > 0) ||
        (gestureState.dx <= -DEAD_ZONE &&
          this._currentIndex < routes.length - 1))
    );
  };

  _startGesture = () => {
    this.props.onSwipeStart && this.props.onSwipeStart();
    this.props.panX.stopAnimation();
  };

  _respondToGesture = (evt: GestureEvent, gestureState: GestureState) => {
    const {
      navigationState: { routes, index },
    } = this.props;

    if (
      // swiping left
      (gestureState.dx > 0 && index <= 0) ||
      // swiping right
      (gestureState.dx < 0 && index >= routes.length - 1)
    ) {
      return;
    }

    this.props.panX.setValue(gestureState.dx);
  };

  _finishGesture = (evt: GestureEvent, gestureState: GestureState) => {
    const {
      navigationState,
      layout,
      swipeDistanceThreshold = layout.width / 1.75,
    } = this.props;

    let { swipeVelocityThreshold = 0.15 } = this.props;

    this.props.onSwipeEnd && this.props.onSwipeEnd();

    if (Platform.OS === 'android') {
      // on Android, velocity is way lower due to timestamp being in nanosecond
      // normalize it to have the same velocity on both iOS and Android
      swipeVelocityThreshold /= 1000000;
    }

    const currentIndex =
      typeof this._pendingIndex === 'number'
        ? this._pendingIndex
        : this._currentIndex;

    let nextIndex = currentIndex;

    if (
      Math.abs(gestureState.dx) > Math.abs(gestureState.dy) &&
      Math.abs(gestureState.vx) > Math.abs(gestureState.vy) &&
      (Math.abs(gestureState.dx) > swipeDistanceThreshold ||
        Math.abs(gestureState.vx) > swipeVelocityThreshold)
    ) {
      nextIndex = Math.round(
        Math.min(
          Math.max(
            0,
            currentIndex - gestureState.dx / Math.abs(gestureState.dx)
          ),
          navigationState.routes.length - 1
        )
      );

      this._currentIndex = nextIndex;
    }

    if (
      !isFinite(nextIndex) ||
      !this.props.canJumpToTab({
        route: this.props.navigationState.routes[nextIndex],
      })
    ) {
      nextIndex = currentIndex;
    }

    this._transitionTo(nextIndex);
  };

  _transitionTo = (index: number, animated: boolean = true) => {
    const offset = -index * this.props.layout.width;
    const route = this.props.navigationState.routes[index];

    if (this.props.animationEnabled === false || animated === false) {
      this.props.panX.setValue(0);
      this.props.offsetX.setValue(offset);
      this.props.jumpTo(route.key);
      return;
    }

    const { timing, ...transitionConfig } = DefaultTransitionSpec;

    Animated.parallel([
      timing(this.props.panX, {
        ...transitionConfig,
        toValue: 0,
      }),
      timing(this.props.offsetX, {
        ...transitionConfig,
        toValue: offset,
      }),
    ]).start(({ finished }) => {
      if (finished) {
        this.props.jumpTo(route.key);
        this.props.onAnimationEnd && this.props.onAnimationEnd();
        this._pendingIndex = null;
      }
    });

    this._pendingIndex = index;
  };

  _panResponder = PanResponder.create({
    onMoveShouldSetPanResponder: this._canMoveScreen,
    onMoveShouldSetPanResponderCapture: this._canMoveScreen,
    onPanResponderGrant: this._startGesture,
    onPanResponderMove: this._respondToGesture,
    onPanResponderTerminate: this._finishGesture,
    onPanResponderRelease: this._finishGesture,
    onPanResponderTerminationRequest: () => true,
  });

  render() {
    const { panX, offsetX, navigationState, layout, children } = this.props;
    const { width } = layout;
    const { routes } = navigationState;
    const maxTranslate = width * (routes.length - 1);
    const translateX = Animated.multiply(
      Animated.add(panX, offsetX).interpolate({
        inputRange: [-maxTranslate, 0],
        outputRange: [-maxTranslate, 0],
        extrapolate: 'clamp',
      }),
      I18nManager.isRTL ? -1 : 1
    );

    return (
      <Animated.View
        style={[
          styles.sheet,
          width
            ? {
                width: routes.length * width,
                transform: [{ translateX }],
              }
            : null,
        ]}
        {...this._panResponder.panHandlers}
      >
        {React.Children.map(children, (child, i) => {
          const route = navigationState.routes[i];
          const focused = i === navigationState.index;

          return (
            <View
              key={route.key}
              testID={this.props.getTestID({ route })}
              style={
                width ? { width } : focused ? StyleSheet.absoluteFill : null
              }
            >
              {focused || width ? child : null}
            </View>
          );
        })}
      </Animated.View>
    );
  }
}

const styles = StyleSheet.create({
  sheet: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'stretch',
  },
});