import React, { Component } from 'react'
import {
  Animated,
  Easing,
  FlatList,
  I18nManager,
  Platform,
  ScrollView,
  View,
  ViewPropTypes
} from 'react-native'
import PropTypes from 'prop-types'
import shallowCompare from 'react-addons-shallow-compare'
import {
  defaultScrollInterpolator,
  defaultAnimatedStyles
} from './animations'

const IS_IOS = Platform.OS === 'ios'

const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView)

const IS_RTL = I18nManager.isRTL

export default class Carousel extends Component {
  static propTypes = {
    data: PropTypes.array.isRequired,
    renderItem: PropTypes.func.isRequired,
    itemWidth: PropTypes.number,
    sliderWidth: PropTypes.number,
    sliderHeight: PropTypes.number,
    activeAnimationType: PropTypes.string,
    activeAnimationOptions: PropTypes.object,
    activeSlideAlignment: PropTypes.oneOf(['center', 'end', 'start']),
    activeSlideOffset: PropTypes.number,
    apparitionDelay: PropTypes.number,
    autoplayDelay: PropTypes.number,
    autoplayInterval: PropTypes.number,
    callbackOffsetMargin: PropTypes.number,
    containerCustomStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style,
    contentContainerCustomStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style,
    enableMomentum: PropTypes.bool,
    enableSnap: PropTypes.bool,
    firstItem: PropTypes.number,
    hasParallaxImages: PropTypes.bool,
    inactiveSlideOpacity: PropTypes.number,
    inactiveSlideScale: PropTypes.number,
    inactiveSlideShift: PropTypes.number,
    layout: PropTypes.oneOf(['default', 'stack', 'tinder']),
    layoutCardOffset: PropTypes.number,
    lockScrollTimeoutDuration: PropTypes.number,
    lockScrollWhileSnapping: PropTypes.bool,
    loop: PropTypes.bool,
    loopClonesPerSide: PropTypes.number,
    scrollInterpolator: PropTypes.func,
    slideInterpolatedStyle: PropTypes.func,
    slideStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style,
    shouldOptimizeUpdates: PropTypes.bool,
    swipeThreshold: PropTypes.number,
    useScrollView: PropTypes.bool,
    onBeforeSnapToItem: PropTypes.func,
    onSnapToItem: PropTypes.func
  };

  static defaultProps = {
    activeAnimationType: 'timing',
    activeAnimationOptions: null,
    activeSlideAlignment: 'center',
    activeSlideOffset: 0,
    apparitionDelay: 0,
    callbackOffsetMargin: 1,
    containerCustomStyle: {},
    contentContainerCustomStyle: {},
    enableMomentum: false,
    enableSnap: true,
    firstItem: 0,
    hasParallaxImages: false,
    inactiveSlideOpacity: 0.7,
    inactiveSlideScale: 0.9,
    inactiveSlideShift: 0,
    layout: 'default',
    lockScrollTimeoutDuration: 1000,
    lockScrollWhileSnapping: false,
    loop: false,
    loopClonesPerSide: 3,
    slideStyle: {},
    shouldOptimizeUpdates: true,
    swipeThreshold: 20
  }

  constructor(props) {
    super(props)

    this.state = {
      hideCarousel: true,
      interpolators: []
    }

    // The following values are not stored in the state because 'setState()' is asynchronous
    // and this results in an absolutely crappy behavior on Android while swiping (see #156)
    const initialActiveItem = this._getFirstItem(props.firstItem)
    this._activeItem = initialActiveItem
    this._previousActiveItem = initialActiveItem
    this._previousFirstItem = initialActiveItem
    this._previousItemsLength = initialActiveItem

    this._mounted = false
    this._positions = []
    this._currentContentOffset = 0 // store ScrollView's scroll position
    this._canFireBeforeCallback = false
    this._canFireCallback = false
    this._scrollOffsetRef = null
    this._onScrollTriggered = true // used when momentum is enabled to prevent an issue with edges items
    this._lastScrollDate = 0 // used to work around a FlatList bug
    this._scrollEnabled = props.scrollEnabled !== false

    this._initPositionsAndInterpolators = this._initPositionsAndInterpolators.bind(this)
    this._renderItem = this._renderItem.bind(this)
    this._onSnap = this._onSnap.bind(this)

    this._onLayout = this._onLayout.bind(this)
    this._onScroll = this._onScroll.bind(this)
    this._onScrollBeginDrag = props.enableSnap ? this._onScrollBeginDrag.bind(this) : undefined
    this._onScrollEnd = props.enableSnap ? this._onScrollEnd.bind(this) : undefined
    this._onScrollEndDrag = !props.enableMomentum ? this._onScrollEndDrag.bind(this) : undefined
    this._onMomentumScrollEnd = props.enableMomentum ? this._onMomentumScrollEnd.bind(this) : undefined
    this._onTouchStart = this._onTouchStart.bind(this)
    this._onTouchRelease = this._onTouchRelease.bind(this)

    this._getKeyExtractor = this._getKeyExtractor.bind(this)

    // Native driver for scroll events
    const scrollEventConfig = {
      listener: this._onScroll,
      useNativeDriver: true
    }
    this._scrollPos = new Animated.Value(0)
    this._onScrollHandler = Animated.event(
      [{ nativeEvent: { contentOffset: { x: this._scrollPos } } }],
      scrollEventConfig
    )

    // This bool aims at fixing an iOS bug due to scrollTo that triggers onMomentumScrollEnd.
    // onMomentumScrollEnd fires this._snapScroll, thus creating an infinite loop.
    this._ignoreNextMomentum = false
  }

  componentDidMount() {
    const { apparitionDelay, firstItem } = this.props
    const _firstItem = this._getFirstItem(firstItem)
    const apparitionCallback = () => {
      this.setState({ hideCarousel: false })
    }

    this._mounted = true
    this._initPositionsAndInterpolators()

    requestAnimationFrame(() => {
      if (!this._mounted) {
        return
      }

      this._snapToItem(_firstItem, false, false, true, false)
      this._hackActiveSlideAnimation(_firstItem, 'start', true)

      if (apparitionDelay) {
        this._apparitionTimeout = setTimeout(() => {
          apparitionCallback()
        }, apparitionDelay)
      } else {
        apparitionCallback()
      }
    })
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.shouldOptimizeUpdates === false) {
      return true
    }

    return shallowCompare(this, nextProps, nextState)
  }

  componentWillReceiveProps(nextProps) {
    const { interpolators } = this.state
    const {
      firstItem, itemWidth, itemHeight, scrollEnabled, sliderHeight, sliderWidth
    } = nextProps
    const itemsLength = this._getCustomDataLength(nextProps)

    if (!itemsLength) {
      return
    }

    const nextFirstItem = this._getFirstItem(firstItem, nextProps)
    let nextActiveItem = this._activeItem || this._activeItem === 0 ? this._activeItem : nextFirstItem

    const hasNewSliderWidth = sliderWidth && sliderWidth !== this.props.sliderWidth
    const hasNewSliderHeight = sliderHeight && sliderHeight !== this.props.sliderHeight
    const hasNewItemWidth = itemWidth && itemWidth !== this.props.itemWidth
    const hasNewItemHeight = itemHeight && itemHeight !== this.props.itemHeight
    const hasNewScrollEnabled = scrollEnabled !== this.props.scrollEnabled

    if (nextActiveItem > itemsLength - 1) {
      nextActiveItem = itemsLength - 1
    }

    if (hasNewScrollEnabled) {
      this._setScrollEnabled(scrollEnabled)
    }

    if (interpolators.length !== itemsLength || hasNewSliderWidth ||
      hasNewSliderHeight || hasNewItemWidth || hasNewItemHeight) {
      this._activeItem = nextActiveItem
      this._previousItemsLength = itemsLength

      this._initPositionsAndInterpolators(nextProps)

      if (this._previousItemsLength > itemsLength) {
        this._hackActiveSlideAnimation(nextActiveItem, null, true)
      }

      if (hasNewSliderWidth || hasNewSliderHeight || hasNewItemWidth || hasNewItemHeight) {
        this._snapToItem(nextActiveItem, false, false, false, false)
      }
    } else if (nextFirstItem !== this._previousFirstItem && nextFirstItem !== this._activeItem) {
      this._activeItem = nextFirstItem
      this._previousFirstItem = nextFirstItem
      this._snapToItem(nextFirstItem, true, true, false, false)
    }
  }

  componentWillUnmount() {
    this._mounted = false
    clearTimeout(this._apparitionTimeout)
    clearTimeout(this._hackSlideAnimationTimeout)
    clearTimeout(this._enableAutoplayTimeout)
    clearTimeout(this._autoplayTimeout)
    clearTimeout(this._snapNoMomentumTimeout)
    clearTimeout(this._edgeItemTimeout)
    clearTimeout(this._lockScrollTimeout)
  }

  get realIndex() {
    return this._activeItem
  }

  get currentIndex() {
    return this._getDataIndex(this._activeItem)
  }

  get currentScrollPosition() {
    return this._currentContentOffset
  }

  _needsScrollView() {
    const { useScrollView } = this.props

    return useScrollView || !AnimatedFlatList || this._shouldUseStackLayout() || this._shouldUseTinderLayout()
  }

  _needsRTLAdaptations() {
    return IS_RTL && !IS_IOS
  }

  _canLockScroll() {
    const { enableMomentum, lockScrollWhileSnapping } = this.props

    return !enableMomentum && lockScrollWhileSnapping
  }

  _enableLoop() {
    const { data, enableSnap, loop } = this.props

    return enableSnap && loop && data.length && data.length > 1
  }

  _shouldAnimateSlides(props = this.props) {
    const {
      inactiveSlideOpacity, inactiveSlideScale, scrollInterpolator, slideInterpolatedStyle
    } = props

    return inactiveSlideOpacity < 1 ||
      inactiveSlideScale < 1 ||
      !!scrollInterpolator ||
      !!slideInterpolatedStyle ||
      this._shouldUseShiftLayout() ||
      this._shouldUseStackLayout() ||
      this._shouldUseTinderLayout()
  }

  _shouldUseCustomAnimation() {
    const { activeAnimationOptions } = this.props

    return !!activeAnimationOptions && !this._shouldUseStackLayout() && !this._shouldUseTinderLayout()
  }

  _shouldUseShiftLayout() {
    const { inactiveSlideShift, layout } = this.props

    return layout === 'default' && inactiveSlideShift !== 0
  }

  _shouldUseStackLayout() {
    return this.props.layout === 'stack'
  }

  _shouldUseTinderLayout() {
    return this.props.layout === 'tinder'
  }

  _getCustomData(props = this.props) {
    const { data, loopClonesPerSide } = props
    const dataLength = data.length

    if (!data || !dataLength) {
      return []
    }

    if (!this._enableLoop()) {
      return data
    }

    let previousItems = []
    let nextItems = []

    if (loopClonesPerSide > dataLength) {
      const dataMultiplier = Math.floor(loopClonesPerSide / dataLength)
      const remainder = loopClonesPerSide % dataLength

      for (let i = 0; i < dataMultiplier; i++) {
        previousItems.push(...data)
        nextItems.push(...data)
      }

      previousItems.unshift(...data.slice(-remainder))
      nextItems.push(...data.slice(0, remainder))
    } else {
      previousItems = data.slice(-loopClonesPerSide)
      nextItems = data.slice(0, loopClonesPerSide)
    }

    return previousItems.concat(data, nextItems)
  }

  _getCustomDataLength(props = this.props) {
    const { data, loopClonesPerSide } = props
    const dataLength = data && data.length

    if (!dataLength) {
      return 0
    }

    return this._enableLoop() ? dataLength + (2 * loopClonesPerSide) : dataLength
  }

  _getCustomIndex(index, props = this.props) {
    const itemsLength = this._getCustomDataLength(props)

    if (!itemsLength || (!index && index !== 0)) {
      return 0
    }

    return this._needsRTLAdaptations() ? itemsLength - index - 1 : index
  }

  _getDataIndex(index) {
    const { data, loopClonesPerSide } = this.props
    const dataLength = data && data.length

    if (!this._enableLoop() || !dataLength) {
      return index
    }

    if (index >= dataLength + loopClonesPerSide) {
      return loopClonesPerSide > dataLength ?
        (index - loopClonesPerSide) % dataLength :
        index - dataLength - loopClonesPerSide
    } else if (index < loopClonesPerSide) {
      // TODO: is there a simpler way of determining the interpolated index?
      if (loopClonesPerSide > dataLength) {
        const baseDataIndexes = []
        const dataIndexes = []
        const dataMultiplier = Math.floor(loopClonesPerSide / dataLength)
        const remainder = loopClonesPerSide % dataLength

        for (let i = 0; i < dataLength; i++) {
          baseDataIndexes.push(i)
        }

        for (let j = 0; j < dataMultiplier; j++) {
          dataIndexes.push(...baseDataIndexes)
        }

        dataIndexes.unshift(...baseDataIndexes.slice(-remainder))

        return dataIndexes[index]
      }

      return index + dataLength - loopClonesPerSide
    }

    return index - loopClonesPerSide
  }

  // Used with `snapToItem()` and 'PaginationDot'
  _getPositionIndex(index) {
    const { loop, loopClonesPerSide } = this.props

    return loop ? index + loopClonesPerSide : index
  }

  _getFirstItem(index, props = this.props) {
    const { loopClonesPerSide } = props
    const itemsLength = this._getCustomDataLength(props)

    if (!itemsLength || index > itemsLength - 1 || index < 0) {
      return 0
    }

    return this._enableLoop() ? index + loopClonesPerSide : index
  }

  _getWrappedRef() {
    // https://github.com/facebook/react-native/issues/10635
    // https://stackoverflow.com/a/48786374/8412141
    return this._carouselRef && this._carouselRef.getNode && this._carouselRef.getNode()
  }

  _getScrollEnabled() {
    return this._scrollEnabled
  }

  _setScrollEnabled(value = true) {
    const { scrollEnabled } = this.props
    const wrappedRef = this._getWrappedRef()

    if (!wrappedRef || !wrappedRef.setNativeProps) {
      return
    }

    // 'setNativeProps()' is used instead of 'setState()' because the latter
    // really takes a toll on Android behavior when momentum is disabled
    wrappedRef.setNativeProps({ scrollEnabled: value })
    this._scrollEnabled = value
  }

  _getKeyExtractor(item, index) {
    return `scrollview-item-${index}`
  }

  _getScrollOffset(event) {
    return (event && event.nativeEvent && event.nativeEvent.contentOffset &&
      event.nativeEvent.contentOffset.x) || 0
  }

  _getContainerInnerMargin(opposite = false) {
    const {
      sliderWidth, sliderHeight, itemWidth, itemHeight, activeSlideAlignment
    } = this.props

    if ((activeSlideAlignment === 'start' && !opposite) ||
      (activeSlideAlignment === 'end' && opposite)) {
      return 0
    } else if ((activeSlideAlignment === 'end' && !opposite) ||
      (activeSlideAlignment === 'start' && opposite)) {
      return sliderWidth - itemWidth
    }

    return (sliderWidth - itemWidth) / 2
  }

  _getViewportOffet() {
    const {
      sliderWidth, sliderHeight, itemWidth, itemHeight, activeSlideAlignment
    } = this.props

    if (activeSlideAlignment === 'start') {
      return itemWidth / 2
    } else if (activeSlideAlignment === 'end') {
      return
      sliderWidth - (itemWidth / 2)
    }

    return sliderWidth / 2
  }

  _getCenter(offset) {
    return offset + this._getViewportOffet()
  }

  _getActiveItem(offset) {
    const { activeSlideOffset, swipeThreshold } = this.props
    const center = this._getCenter(offset)
    const centerOffset = activeSlideOffset || swipeThreshold

    for (let i = 0; i < this._positions.length; i++) {
      const { start, end } = this._positions[i]
      if (center + centerOffset >= start && center - centerOffset <= end) {
        return i
      }
    }

    const lastIndex = this._positions.length - 1
    if (this._positions[lastIndex] && center - centerOffset > this._positions[lastIndex].end) {
      return lastIndex
    }

    return 0
  }

  _initPositionsAndInterpolators(props = this.props) {
    const {
      data, itemWidth, itemHeight, scrollInterpolator
    } = props
    const sizeRef = itemWidth

    if (!data.length) {
      return
    }

    const interpolators = []
    this._positions = []

    this._getCustomData(props).forEach((itemData, index) => {
      const _index = this._getCustomIndex(index, props)
      let animatedValue

      this._positions[index] = {
        start: index * sizeRef,
        end: index * sizeRef + sizeRef
      }

      if (!this._shouldAnimateSlides(props)) {
        animatedValue = 1
      } else if (this._shouldUseCustomAnimation()) {
        animatedValue = new Animated.Value(_index === this._activeItem ? 1 : 0)
      } else {
        let interpolator

        if (!interpolator || !interpolator.inputRange || !interpolator.outputRange) {
          interpolator = defaultScrollInterpolator(_index, props)
        }

        animatedValue = this._scrollPos.interpolate({
          ...interpolator,
          extrapolate: 'clamp'
        })
      }

      interpolators.push(animatedValue)
    })

    this.setState({ interpolators })
  }

  _getSlideAnimation(index, toValue) {
    const { interpolators } = this.state
    const { activeAnimationType, activeAnimationOptions } = this.props

    const animatedValue = interpolators && interpolators[index]

    if (!animatedValue && animatedValue !== 0) {
      return false
    }

    const animationCommonOptions = {
      isInteraction: false,
      useNativeDriver: true,
      ...activeAnimationOptions,
      toValue
    }

    return Animated.parallel([
      Animated.timing(
        animatedValue,
        { ...animationCommonOptions, easing: Easing.linear }
      ),
      Animated[activeAnimationType](
        animatedValue,
        { ...animationCommonOptions }
      )
    ])
  }

  _playCustomSlideAnimation(current, next) {
    const { interpolators } = this.state
    const itemsLength = this._getCustomDataLength()
    const _currentIndex = this._getCustomIndex(current)
    const _currentDataIndex = this._getDataIndex(_currentIndex)
    const _nextIndex = this._getCustomIndex(next)
    const _nextDataIndex = this._getDataIndex(_nextIndex)
    const animations = []

    // Keep animations in sync when looping
    if (this._enableLoop()) {
      for (let i = 0; i < itemsLength; i++) {
        if (this._getDataIndex(i) === _currentDataIndex && interpolators[i]) {
          animations.push(this._getSlideAnimation(i, 0))
        } else if (this._getDataIndex(i) === _nextDataIndex && interpolators[i]) {
          animations.push(this._getSlideAnimation(i, 1))
        }
      }
    } else {
      if (interpolators[current]) {
        animations.push(this._getSlideAnimation(current, 0))
      }
      if (interpolators[next]) {
        animations.push(this._getSlideAnimation(next, 1))
      }
    }

    Animated.parallel(animations, { stopTogether: false }).start()
  }

  _hackActiveSlideAnimation(index, goTo, force = false) {
    const { data } = this.props

    if (!this._mounted || !this._carouselRef || !this._positions[index] || (!force && this._enableLoop())) {
      return
    }

    const offset = this._positions[index] && this._positions[index].start

    if (!offset && offset !== 0) {
      return
    }

    const itemsLength = data && data.length
    const direction = goTo || itemsLength === 1 ? 'start' : 'end'

    this._scrollTo(offset + (direction === 'start' ? -1 : 1), false)

    clearTimeout(this._hackSlideAnimationTimeout)
    this._hackSlideAnimationTimeout = setTimeout(() => {
      this._scrollTo(offset, false)
    }, 50) // works randomly when set to '0'
  }

  _lockScroll() {
    const { lockScrollTimeoutDuration } = this.props
    clearTimeout(this._lockScrollTimeout)
    this._lockScrollTimeout = setTimeout(() => {
      this._releaseScroll()
    }, lockScrollTimeoutDuration)
    this._setScrollEnabled(false)
  }

  _releaseScroll() {
    clearTimeout(this._lockScrollTimeout)
    this._setScrollEnabled(true)
  }

  _repositionScroll(index) {
    const { data, loopClonesPerSide } = this.props
    const dataLength = data && data.length

    if (!this._enableLoop() || !dataLength ||
      (index >= loopClonesPerSide && index < dataLength + loopClonesPerSide)) {
      return
    }

    let repositionTo = index

    if (index >= dataLength + loopClonesPerSide) {
      repositionTo = index - dataLength
    } else if (index < loopClonesPerSide) {
      repositionTo = index + dataLength
    }

    this._snapToItem(repositionTo, false, false, false, false)
  }

  _scrollTo(offset, animated = true) {
    const wrappedRef = this._getWrappedRef()

    if (!this._mounted || !wrappedRef) {
      return
    }

    const specificOptions = {
      x: offset,
      y: 0
    }

    const options = {
      ...specificOptions,
      animated
    }
    wrappedRef.scrollTo(options)
  }

  _onScroll(event) {
    const { callbackOffsetMargin, enableMomentum, onScroll } = this.props

    const scrollOffset = event ? this._getScrollOffset(event) : this._currentContentOffset
    const nextActiveItem = this._getActiveItem(scrollOffset)
    const itemReached = nextActiveItem === this._itemToSnapTo
    const scrollConditions =
      scrollOffset >= this._scrollOffsetRef - callbackOffsetMargin &&
      scrollOffset <= this._scrollOffsetRef + callbackOffsetMargin

    this._currentContentOffset = scrollOffset
    this._onScrollTriggered = true
    this._lastScrollDate = Date.now()

    if (this._activeItem !== nextActiveItem && this._shouldUseCustomAnimation()) {
      this._playCustomSlideAnimation(this._activeItem, nextActiveItem)
    }

    if (enableMomentum) {
      clearTimeout(this._snapNoMomentumTimeout)

      if (this._activeItem !== nextActiveItem) {
        this._activeItem = nextActiveItem
      }

      if (itemReached) {
        if (this._canFireBeforeCallback) {
          this._onBeforeSnap(this._getDataIndex(nextActiveItem))
        }

        if (scrollConditions && this._canFireCallback) {
          this._onSnap(this._getDataIndex(nextActiveItem))
        }
      }
    } else if (this._activeItem !== nextActiveItem && itemReached) {
      if (this._canFireBeforeCallback) {
        this._onBeforeSnap(this._getDataIndex(nextActiveItem))
      }

      if (scrollConditions) {
        this._activeItem = nextActiveItem

        if (this._canLockScroll()) {
          this._releaseScroll()
        }

        if (this._canFireCallback) {
          this._onSnap(this._getDataIndex(nextActiveItem))
        }
      }
    }

    if (nextActiveItem === this._itemToSnapTo &&
      scrollOffset === this._scrollOffsetRef) {
      this._repositionScroll(nextActiveItem)
    }

    if (onScroll && event) {
      onScroll(event)
    }
  }

  _onStartShouldSetResponderCapture(event) {
    const { onStartShouldSetResponderCapture } = this.props

    if (onStartShouldSetResponderCapture) {
      onStartShouldSetResponderCapture(event)
    }

    return this._getScrollEnabled()
  }

  _onTouchStart() {
    // `onTouchStart` is fired even when `scrollEnabled` is set to `false`
    if (this._getScrollEnabled() !== false && this._autoplaying) {
      this.stopAutoplay()
    }
  }

  // Used when `enableSnap` is ENABLED
  _onScrollBeginDrag(event) {
    const { onScrollBeginDrag } = this.props

    if (!this._getScrollEnabled()) {
      return
    }

    this._scrollStartOffset = this._getScrollOffset(event)
    this._scrollStartActive = this._getActiveItem(this._scrollStartOffset)
    this._ignoreNextMomentum = false
    // this._canFireCallback = false;

    if (onScrollBeginDrag) {
      onScrollBeginDrag(event)
    }
  }

  // Used when `enableMomentum` is DISABLED
  _onScrollEndDrag(event) {
    const { onScrollEndDrag } = this.props

    if (this._carouselRef) {
      this._onScrollEnd && this._onScrollEnd()
    }

    if (onScrollEndDrag) {
      onScrollEndDrag(event)
    }
  }

  // Used when `enableMomentum` is ENABLED
  _onMomentumScrollEnd(event) {
    const { onMomentumScrollEnd } = this.props

    if (this._carouselRef) {
      this._onScrollEnd && this._onScrollEnd()
    }

    if (onMomentumScrollEnd) {
      onMomentumScrollEnd(event)
    }
  }

  _onScrollEnd(event) {
    const { enableSnap } = this.props

    if (this._ignoreNextMomentum) {
      // iOS fix
      this._ignoreNextMomentum = false

      return
    }

    this._scrollEndOffset = this._currentContentOffset
    this._scrollEndActive = this._getActiveItem(this._scrollEndOffset)

    if (enableSnap) {
      this._snapScroll(this._scrollEndOffset - this._scrollStartOffset)
    }
  }

  _onTouchRelease(event) {
    const { enableMomentum } = this.props

    if (enableMomentum && IS_IOS) {
      clearTimeout(this._snapNoMomentumTimeout)
      this._snapNoMomentumTimeout = setTimeout(() => {
        this._snapToItem(this._activeItem)
      }, 100)
    }
  }

  _onLayout(event) {
    const { onLayout } = this.props

    // Prevent unneeded actions during the first 'onLayout' (triggered on init)
    if (this._onLayoutInitDone) {
      this._initPositionsAndInterpolators()
      this._snapToItem(this._activeItem, false, false, false, false)
    } else {
      this._onLayoutInitDone = true
    }

    if (onLayout) {
      onLayout(event)
    }
  }

  _snapScroll(delta) {
    const { swipeThreshold } = this.props

    // When using momentum and releasing the touch with
    // no velocity, scrollEndActive will be undefined (iOS)
    if (!this._scrollEndActive && this._scrollEndActive !== 0 && IS_IOS) {
      this._scrollEndActive = this._scrollStartActive
    }

    if (this._scrollStartActive !== this._scrollEndActive) {
      // Snap to the new active item
      this._snapToItem(this._scrollEndActive)
    } else {
      // Snap depending on delta
      if (delta > 0) {
        if (delta > swipeThreshold) {
          this._snapToItem(this._scrollStartActive + 1)
        } else {
          this._snapToItem(this._scrollEndActive)
        }
      } else if (delta < 0) {
        if (delta < -swipeThreshold) {
          this._snapToItem(this._scrollStartActive - 1)
        } else {
          this._snapToItem(this._scrollEndActive)
        }
      } else {
        // Snap to current
        this._snapToItem(this._scrollEndActive)
      }
    }
  }

  _snapToItem(index, animated = true, fireCallback = true, initial = false, lockScroll = true) {
    const { enableMomentum, onSnapToItem, onBeforeSnapToItem } = this.props
    const itemsLength = this._getCustomDataLength()
    const wrappedRef = this._getWrappedRef()

    if (!itemsLength || !wrappedRef) {
      return
    }

    if (!index || index < 0) {
      index = 0
    } else if (itemsLength > 0 && index >= itemsLength) {
      index = itemsLength - 1
    }

    if (index !== this._previousActiveItem) {
      this._previousActiveItem = index

      // Placed here to allow overscrolling for edges items
      if (lockScroll && this._canLockScroll()) {
        this._lockScroll()
      }

      if (fireCallback) {
        if (onBeforeSnapToItem) {
          this._canFireBeforeCallback = true
        }

        if (onSnapToItem) {
          this._canFireCallback = true
        }
      }
    }

    this._itemToSnapTo = index
    this._scrollOffsetRef = this._positions[index] && this._positions[index].start
    this._onScrollTriggered = false

    if (!this._scrollOffsetRef && this._scrollOffsetRef !== 0) {
      return
    }

    this._scrollTo(this._scrollOffsetRef, animated)

    if (enableMomentum) {
      // iOS fix, check the note in the constructor
      if (IS_IOS && !initial) {
        this._ignoreNextMomentum = true
      }

      if (index === 0 || index === itemsLength - 1) {
        clearTimeout(this._edgeItemTimeout)
        this._edgeItemTimeout = setTimeout(() => {
          if (!initial && index === this._activeItem && !this._onScrollTriggered) {
            this._onScroll()
          }
        }, 250)
      }
    }
  }

  _onBeforeSnap(index) {
    const { onBeforeSnapToItem } = this.props

    if (!this._carouselRef) {
      return
    }

    this._canFireBeforeCallback = false
    onBeforeSnapToItem && onBeforeSnapToItem(index)
  }

  _onSnap(index) {
    const { onSnapToItem } = this.props

    if (!this._carouselRef) {
      return
    }

    this._canFireCallback = false
    onSnapToItem && onSnapToItem(index)
  }

  startAutoplay() {
    const { autoplayInterval, autoplayDelay } = this.props

    if (this._autoplaying) {
      return
    }

    clearTimeout(this._autoplayTimeout)
    this._autoplayTimeout = setTimeout(() => {
      this._autoplaying = true
      this._autoplayInterval = setInterval(() => {
        if (this._autoplaying) {
          this.snapToNext()
        }
      }, autoplayInterval)
    }, autoplayDelay)
  }

  stopAutoplay() {
    this._autoplaying = false
    clearInterval(this._autoplayInterval)
  }

  snapToItem(index, animated = true, fireCallback = true) {
    if (!index || index < 0) {
      index = 0
    }

    const positionIndex = this._getPositionIndex(index)

    if (positionIndex === this._activeItem) {
      return
    }

    this._snapToItem(positionIndex, animated, fireCallback)
  }

  snapToNext(animated = true, fireCallback = true) {
    const itemsLength = this._getCustomDataLength()

    let newIndex = this._activeItem + 1
    if (newIndex > itemsLength - 1) {
      if (!this._enableLoop()) {
        return
      }
      newIndex = 0
    }
    this._snapToItem(newIndex, animated, fireCallback)
  }

  snapToPrev(animated = true, fireCallback = true) {
    const itemsLength = this._getCustomDataLength()

    let newIndex = this._activeItem - 1
    if (newIndex < 0) {
      if (!this._enableLoop()) {
        return
      }
      newIndex = itemsLength - 1
    }
    this._snapToItem(newIndex, animated, fireCallback)
  }

  // https://github.com/facebook/react-native/issues/1831#issuecomment-231069668
  triggerRenderingHack(offset) {
    // Avoid messing with user scroll
    if (Date.now() - this._lastScrollDate < 500) {
      return
    }

    const scrollPosition = this._currentContentOffset
    if (!scrollPosition && scrollPosition !== 0) {
      return
    }

    const scrollOffset = offset || (scrollPosition === 0 ? 1 : -1)
    this._scrollTo(scrollPosition + scrollOffset, false)
  }

  _getSlideInterpolatedStyle(index, animatedValue) {
    const { layoutCardOffset, slideInterpolatedStyle } = this.props

    if (slideInterpolatedStyle) {
      return slideInterpolatedStyle(index, animatedValue, this.props)
    } else if (this._shouldUseTinderLayout()) {
      return tinderAnimatedStyles(index, animatedValue, this.props, layoutCardOffset)
    } else if (this._shouldUseStackLayout()) {
      return stackAnimatedStyles(index, animatedValue, this.props, layoutCardOffset)
    } else if (this._shouldUseShiftLayout()) {
      return shiftAnimatedStyles(index, animatedValue, this.props)
    }

    return defaultAnimatedStyles(index, animatedValue, this.props)
  }

  _renderItem({ item, index }) {
    const { interpolators } = this.state
    const {
      hasParallaxImages,
      itemWidth,
      itemHeight,
      keyExtractor,
      renderItem,
      sliderHeight,
      sliderWidth,
      slideStyle
    } = this.props

    const animatedValue = interpolators && interpolators[index]

    if (!animatedValue && animatedValue !== 0) {
      return false
    }

    const animate = this._shouldAnimateSlides()
    const Component = animate ? Animated.View : View
    const animatedStyle = animate ? this._getSlideInterpolatedStyle(index, animatedValue) : {}

    const parallaxProps = hasParallaxImages ? {
      scrollPosition: this._scrollPos,
      carouselRef: this._carouselRef,
      sliderWidth,
      sliderHeight,
      itemWidth,
      itemHeight
    } : undefined

    const mainDimension = { width: itemWidth }
    const specificProps = this._needsScrollView() ? {
      key: keyExtractor ? keyExtractor(item, index) : this._getKeyExtractor(item, index)
    } : {}

    return (
      <Component
        style={[mainDimension, slideStyle, animatedStyle]}
        pointerEvents='box-none'
        {...specificProps}
      >
        {renderItem({ item, index }, parallaxProps)}
      </Component>
    )
  }

  _getComponentOverridableProps() {
    const {
      enableMomentum,
      itemWidth,
      itemHeight,
      loopClonesPerSide,
      sliderWidth,
      sliderHeight
    } = this.props

    const visibleItems = Math.ceil(
      sliderWidth / itemWidth
    ) + 1
    const initialNumPerSide = this._enableLoop() ? loopClonesPerSide : 2
    const initialNumToRender = visibleItems + (initialNumPerSide * 2)
    const maxToRenderPerBatch = 1 + (initialNumToRender * 2)
    const windowSize = maxToRenderPerBatch

    const specificProps = !this._needsScrollView() ? {
      initialNumToRender,
      maxToRenderPerBatch,
      windowSize
      // updateCellsBatchingPeriod
    } : {}

    return {
      decelerationRate: enableMomentum ? 0.9 : 'fast',
      showsHorizontalScrollIndicator: false,
      showsVerticalScrollIndicator: false,
      overScrollMode: 'never',
      automaticallyAdjustContentInsets: false,
      directionalLockEnabled: true,
      pinchGestureEnabled: false,
      scrollsToTop: false,
      removeClippedSubviews: true,
      inverted: this._needsRTLAdaptations(),
      // renderToHardwareTextureAndroid: true,
      ...specificProps
    }
  }

  _getComponentStaticProps() {
    const { hideCarousel } = this.state
    const {
      containerCustomStyle,
      contentContainerCustomStyle,
      keyExtractor,
      sliderWidth
    } = this.props

    const containerStyle = [
      containerCustomStyle || style || {},
      hideCarousel ? { opacity: 0 } : {},
      { width: sliderWidth, flexDirection: this._needsRTLAdaptations() ? 'row-reverse' : 'row' }
    ]
    const contentContainerStyle = [
      contentContainerCustomStyle || {},
      {
        paddingLeft: this._getContainerInnerMargin(),
        paddingRight: this._getContainerInnerMargin(true)
      }
    ]

    const specificProps = !this._needsScrollView() ? {
      // extraData: this.state,
      renderItem: this._renderItem,
      numColumns: 1,
      getItemLayout: undefined, // see #193
      initialScrollIndex: undefined, // see #193
      keyExtractor: keyExtractor || this._getKeyExtractor
    } : {}

    return {
      ref: c => this._carouselRef = c,
      data: this._getCustomData(),
      style: containerStyle,
      contentContainerStyle,
      horizontal: true,
      scrollEventThrottle: 1,
      onScroll: this._onScrollHandler,
      onScrollBeginDrag: this._onScrollBeginDrag,
      onScrollEndDrag: this._onScrollEndDrag,
      onMomentumScrollEnd: this._onMomentumScrollEnd,
      onResponderRelease: this._onTouchRelease,
      onStartShouldSetResponderCapture: this._onStartShouldSetResponderCapture,
      onTouchStart: this._onTouchStart,
      onLayout: this._onLayout,
      ...specificProps
    }
  }

  render() {
    const { data, renderItem } = this.props

    if (!data || !renderItem) {
      return false
    }

    const props = {
      ...this._getComponentOverridableProps(),
      ...this.props,
      ...this._getComponentStaticProps()
    }

    return (
      <View>
        <AnimatedScrollView
          {...props}
          style={{ height: this.props.sliderHeight }}
        >
          {
            this._getCustomData().map((item, index) => this._renderItem({ item, index }))
          }
        </AnimatedScrollView>
      </View>
    )
  }
}
