import React, { Component } from "react";
import PropTypes from "prop-types"
import {
  View,
  Animated,
  PanResponder,
  Dimensions,
  Text,
  Platform
} from "react-native";

const { height, width } = Dimensions.get("window");

class CardStack extends Component {
  static distance(x, y) {
    return Math.hypot(x, y);
  }

  constructor(props) {
    super(props);
    this.state = {
      drag: new Animated.ValueXY({ x: 0, y: 0 }),
      dragDistance: new Animated.Value(0),
      sindex: 0, // index to the next card to be renderd mod card.length
      cardA: null,
      cardB: null,
      topCard: "cardA",
      cards: [],
      touchStart: 0,
    };
    this.distance = this.constructor.distance;
    this._panResponder = PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => false,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
      onMoveShouldSetPanResponder: (evt, gestureState) => {
        const isVerticalSwipe = Math.sqrt(
          Math.pow(gestureState.dx, 2) < Math.pow(gestureState.dy, 2)
        )
        if (!this.props.verticalSwipe && isVerticalSwipe) {
          return false
        }
        return Math.sqrt(Math.pow(gestureState.dx, 2) + Math.pow(gestureState.dy, 2)) > 10
      },
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
        const isVerticalSwipe = Math.sqrt(
          Math.pow(gestureState.dx, 2) < Math.pow(gestureState.dy, 2)
        )
        if (!this.props.verticalSwipe && isVerticalSwipe) {
          return false
        }
        return Math.sqrt(Math.pow(gestureState.dx, 2) + Math.pow(gestureState.dy, 2)) > 10
      },
      onPanResponderGrant: (evt, gestureState) => {
        this.props.onSwipeStart();
        this.setState({ touchStart: new Date().getTime() });
      },
      onPanResponderMove: (evt, gestureState) => {
        const movedX = gestureState.moveX - gestureState.x0;
        const movedY = gestureState.moveY - gestureState.y0;
        this.props.onSwipe(movedX, movedY);
        const { verticalSwipe, horizontalSwipe } = this.props;
        const dragDistance = this.distance((horizontalSwipe) ? gestureState.dx : 0, (verticalSwipe) ? gestureState.dy : 0);
        this.state.dragDistance.setValue(dragDistance);
        this.state.drag.setValue({ x: (horizontalSwipe) ? gestureState.dx : 0, y: (verticalSwipe) ? gestureState.dy : 0 });
      },
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {
        this.props.onSwipeEnd();
        const currentTime = new Date().getTime();
        const swipeDuration = currentTime - this.state.touchStart;
        const {
          verticalThreshold,
          horizontalThreshold,
          disableTopSwipe,
          disableLeftSwipe,
          disableRightSwipe,
          disableBottomSwipe,
        } = this.props;

        if (((Math.abs(gestureState.dx) > horizontalThreshold) ||
          (Math.abs(gestureState.dx) > horizontalThreshold * 0.6 &&
            swipeDuration < 150)
        ) && this.props.horizontalSwipe) {

          const swipeDirection = (gestureState.dx < 0) ? width * -1.5 : width * 1.5;
          if (swipeDirection < 0 && !disableLeftSwipe) {
            this._nextCard("left", swipeDirection, gestureState.dy, this.props.duration);
          }
          else if (swipeDirection > 0 && !disableRightSwipe) {
            this._nextCard("right", swipeDirection, gestureState.dy, this.props.duration);
          }
          else {
            this._resetCard();
          }
        } else if (((Math.abs(gestureState.dy) > verticalThreshold) ||
          (Math.abs(gestureState.dy) > verticalThreshold * 0.8 &&
            swipeDuration < 150)
        ) && this.props.verticalSwipe) {

          const swipeDirection = (gestureState.dy < 0) ? height * -1 : height;
          if (swipeDirection < 0 && !disableTopSwipe) {

            this._nextCard("top", gestureState.dx, swipeDirection, this.props.duration);
          }
          else if (swipeDirection > 0 && !disableBottomSwipe) {
            this._nextCard("bottom", gestureState.dx, swipeDirection, this.props.duration);
          }
          else {
            this._resetCard();
          }
        }
        else {
          this._resetCard();
        }
      },
      onPanResponderTerminate: (evt, gestureState) => {
      },
      onShouldBlockNativeResponder: (evt, gestureState) => {
        return true;
      },
    });
  }

  componentDidUpdate(prevProps) {
    if (typeof this.props.children === "undefined") return;
    if (!this._isSameChildren(this.props.children, prevProps.children)) {
      const children = Array.isArray(this.props.children) ? this.props.children : [this.props.children];
      const aIndex = (this.state.topCard == "cardA") ?
        this._getIndex(this.state.sindex - 2, children.length) :
        this._getIndex(this.state.sindex - 1, children.length);
      const bIndex = (this.state.topCard == "cardB") ?
        this._getIndex(this.state.sindex - 2, children.length) :
        this._getIndex(this.state.sindex - 1, children.length);
      this.setState({
        cards: children,
        cardA: children[aIndex] || null,
        cardB: children[bIndex] || null
      });
    }
  }

  _getIndex(index, cards){
    return this.props.loop ? 
      this.mod(index, cards):
      index;
  }

  componentDidMount() {
    this.initDeck();
  }

  _isSameChildren(a, b) {
    if (typeof a != typeof b) return false;
    if (typeof a === "undefined") return false;
    if (Array.isArray(a) && Array.isArray(b)) {
      if (a.length != b.length) return false;
      for (const i in a) {
        if (a[i].key != b[i].key) { return false }
      }
      return true;
    }
    if (a.key !== b.key) return false;

    return true
  }

  initDeck() {
    if (typeof this.props.children === "undefined") return;
    const { children, loop } = this.props;
    const cards = Array.isArray(children) ? children : [children];
    const initialIndexA = this.props.initialIndex < cards.length ? this.props.initialIndex : 0;
    const initialIndexB = loop ? this.mod(initialIndexA + 1, cards.length) : initialIndexA + 1;
    const cardA = cards[initialIndexA] || null;
    const cardB = cards[initialIndexB] || null;
    this.setState({
      cards,
      cardA,
      cardB,
      sindex: initialIndexB + 1,
    });
  }

  _resetCard() {
    Animated.timing(
      this.state.dragDistance,
      {
        toValue: 0,
        duration: this.props.duration,
        useNativeDriver: this.props.useNativeDriver || false,
      }
    ).start();
    Animated.spring(
      this.state.drag,
      {
        toValue: { x: 0, y: 0 },
        duration: this.props.duration,
        useNativeDriver: this.props.useNativeDriver || false,
      }
    ).start();
  }

  goBackFromTop() {
    this._goBack("top");
  }

  goBackFromRight() {
    this._goBack("right");
  }

  goBackFromLeft() {
    this._goBack("left");
  }

  goBackFromBottom() {
    this._goBack("bottom");
  }

  mod(n, m) {
    return ((n % m) + m) % m;
  }

  _goBack(direction) {
    const { cards, sindex, topCard } = this.state;

    if ((sindex - 3) < 0 && !this.props.loop) return;

    const previusCardIndex = this.mod(sindex - 3, cards.length)
    let update = {};
    if (topCard === "cardA") {
      update = {
        ...update,
        cardB: cards[previusCardIndex]

      }
    } else {
      update = {
        ...update,
        cardA: cards[previusCardIndex],
      }
    }

    this.setState({
      ...update,
      topCard: (topCard === "cardA") ? "cardB" : "cardA",
      sindex: sindex - 1
    }, () => {

      switch (direction) {
      case "top":
        this.state.drag.setValue({ x: 0, y: -height });
        this.state.dragDistance.setValue(height);
        break;
      case "left":
        this.state.drag.setValue({ x: -width, y: 0 });
        this.state.dragDistance.setValue(width);
        break;
      case "right":
        this.state.drag.setValue({ x: width, y: 0 });
        this.state.dragDistance.setValue(width);
        break;
      case "bottom":
        this.state.drag.setValue({ x: 0, y: height });
        this.state.dragDistance.setValue(width);
        break;
      default:

      }

      Animated.spring(
        this.state.dragDistance,
        {
          toValue: 0,
          duration: this.props.duration,
          useNativeDriver: this.props.useNativeDriver || false,
        }
      ).start();

      Animated.spring(
        this.state.drag,
        {
          toValue: { x: 0, y: 0 },
          duration: this.props.duration,
          useNativeDriver: this.props.useNativeDriver || false,
        }
      ).start();
    })
  }

  swipeTop(d = null) {
    this._nextCard("top", 0, -height, d || this.props.duration);
  }

  swipeBottom(d = null) {
    this._nextCard("bottom", 0, height, d || this.props.duration);
  }

  swipeRight(d = null) {
    this._nextCard("right", width * 1.5, 0, d || this.props.duration);
  }

  swipeLeft(d = null) {
    this._nextCard("left", -width * 1.5, 0, d || this.props.duration);
  }

  _nextCard(direction, x, y, duration = 400) {
    const { verticalSwipe, horizontalSwipe, loop } = this.props;
    const { sindex, cards, topCard } = this.state;

    // index for the next card to be renderd
    const nextCard = (loop) ? (Math.abs(sindex) % cards.length) : sindex;

    // index of the swiped card
    const index = (loop) ? this.mod(nextCard - 2, cards.length) : nextCard - 2;

    if (index === cards.length - 1) {
      this.props.onSwipedAll();
    }

    if ((sindex - 2 < cards.length) || (loop)) {
      Animated.spring(
        this.state.dragDistance,
        {
          toValue: 220,
          duration,
          useNativeDriver: this.props.useNativeDriver || false,
        }
      ).start();

      Animated.timing(
        this.state.drag,
        {
          toValue: { x: (horizontalSwipe) ? x : 0, y: (verticalSwipe) ? y : 0 },
          duration,
          useNativeDriver: this.props.useNativeDriver || false,
        }
      ).start(() => {

        const newTopCard = (topCard === "cardA") ? "cardB" : "cardA";

        let update = {};
        if (newTopCard === "cardA") {
          update = {
            ...update,
            cardB: cards[nextCard]
          };
        }
        if (newTopCard === "cardB") {
          update = {
            ...update,
            cardA: cards[nextCard],
          };
        }
        this.state.drag.setValue({ x: 0, y: 0 });
        this.state.dragDistance.setValue(0);
        this.setState({
          ...update,
          topCard: newTopCard,
          sindex: nextCard + 1
        });

        this.props.onSwiped(index);
        switch (direction) {
        case "left":
          this.props.onSwipedLeft(index);
          if (this.state.cards[index] && this.state.cards[index].props.onSwipedLeft)
            this.state.cards[index] && this.state.cards[index].props.onSwipedLeft();
          break;
        case "right":
          this.props.onSwipedRight(index);
          if (this.state.cards[index] && this.state.cards[index].props.onSwipedRight)
            this.state.cards[index].props.onSwipedRight();
          break;
        case "top":
          this.props.onSwipedTop(index);
          if (this.state.cards[index] && this.state.cards[index].props.onSwipedTop)
            this.state.cards[index].props.onSwipedTop();
          break;
        case "bottom":
          this.props.onSwipedBottom(index);
          if (this.state.cards[index] && this.state.cards[index].props.onSwipedBottom)
            this.state.cards[index].props.onSwipedBottom();
          break;
        default:
        }
      });

    }
  }


  /**
   * @description CardB’s click feature is trigger the CardA on the card stack. (Solved on Android)
   * @see https://facebook.github.io/react-native/docs/view#pointerevents
   */
  _setPointerEvents(topCard, topCardName) {
    return { pointerEvents: topCard === topCardName ? "auto" : "none" }
  }

  render() {

    const { secondCardZoom, renderNoMoreCards } = this.props;
    const { drag, dragDistance, cardA, cardB, topCard, sindex } = this.state;

    const scale = dragDistance.interpolate({
      inputRange: [0, 10, 220],
      outputRange: [secondCardZoom, secondCardZoom, 1],
      extrapolate: "clamp",
    });
    const rotate = drag.x.interpolate({
      inputRange: [width * -1.5, 0, width * 1.5],
      outputRange: this.props.outputRotationRange,
      extrapolate: "clamp",
    });

    return (
      <View {...this._panResponder.panHandlers} style={[{ position: "relative" }, this.props.style]}>

        {renderNoMoreCards()}

        <Animated.View
          {...this._setPointerEvents(topCard, "cardB")}
          style={[{
            position: "absolute",
            zIndex: (topCard === "cardB") ? 3 : 2,
            ...Platform.select({
              android: {
                elevation: (topCard === "cardB") ? 3 : 2,
              }
            }),
            transform: [
              { rotate: (topCard === "cardB") ? rotate : "0deg" },
              { translateX: (topCard === "cardB") ? drag.x : 0 },
              { translateY: (topCard === "cardB") ? drag.y : 0 },
              { scale: (topCard === "cardB") ? 1 : scale },
            ]
          }, this.props.cardContainerStyle]}>
          {cardB}
        </Animated.View>
        <Animated.View
          {...this._setPointerEvents(topCard, "cardA")}
          style={[{
            position: "absolute",
            zIndex: (topCard === "cardA") ? 3 : 2,
            ...Platform.select({
              android: {
                elevation: (topCard === "cardA") ? 3 : 2,
              }
            }),
            transform: [
              { rotate: (topCard === "cardA") ? rotate : "0deg" },
              { translateX: (topCard === "cardA") ? drag.x : 0 },
              { translateY: (topCard === "cardA") ? drag.y : 0 },
              { scale: (topCard === "cardA") ? 1 : scale },
            ]
          }, this.props.cardContainerStyle]}>
          {cardA}
        </Animated.View>

      </View>
    );
  }
}

CardStack.propTypes = {

  children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired,

  style: PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.array]),
  cardContainerStyle: PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.array]),
  secondCardZoom: PropTypes.number,
  loop: PropTypes.bool,
  initialIndex: PropTypes.number,
  renderNoMoreCards: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
  onSwipeStart: PropTypes.func,
  onSwipeEnd: PropTypes.func,
  onSwiped: PropTypes.func,
  onSwipedLeft: PropTypes.func,
  onSwipedRight: PropTypes.func,
  onSwipedTop: PropTypes.func,
  onSwipedBottom: PropTypes.func,
  onSwiped: PropTypes.func,
  onSwipedAll: PropTypes.func,
  onSwipe: PropTypes.func,

  disableBottomSwipe: PropTypes.bool,
  disableLeftSwipe: PropTypes.bool,
  disableRightSwipe: PropTypes.bool,
  disableTopSwipe: PropTypes.bool,
  verticalSwipe: PropTypes.bool,
  verticalThreshold: PropTypes.number,

  horizontalSwipe: PropTypes.bool,
  horizontalThreshold: PropTypes.number,
  outputRotationRange: PropTypes.array,
  duration: PropTypes.number
}

CardStack.defaultProps = {

  style: {},
  cardContainerStyle: {},
  secondCardZoom: 0.95,
  loop: false,
  initialIndex: 0,
  renderNoMoreCards: () => { return (<Text>No More Cards</Text>) },
  onSwipeStart: () => null,
  onSwipeEnd: () => null,
  onSwiped: () => { },
  onSwipedLeft: () => { },
  onSwipedRight: () => { },
  onSwipedTop: () => { },
  onSwipedBottom: () => { },
  onSwipedAll: async () => { },
  onSwipe: () => { },

  disableBottomSwipe: false,
  disableLeftSwipe: false,
  disableRightSwipe: false,
  disableTopSwipe: false,
  verticalSwipe: true,
  verticalThreshold: height / 4,
  horizontalSwipe: true,
  horizontalThreshold: width / 2,
  outputRotationRange: ["-15deg", "0deg", "15deg"],
  duration: 300
}

export default CardStack;
