//
//  KolodaView.swift
//  Koloda
//
//  Created by Eugene Andreyev on 4/24/15.
//  Copyright (c) 2015 Eugene Andreyev. All rights reserved.
//

import UIKit
import pop

//Default values
private let defaultCountOfVisibleCards = 3
private let defaultBackgroundCardsTopMargin: CGFloat = 4.0
private let defaultBackgroundCardsScalePercent: CGFloat = 0.95
private let defaultBackgroundCardsLeftMargin: CGFloat = 8.0
private let defaultBackgroundCardFrameAnimationDuration: TimeInterval = 0.2

//Opacity values
private let defaultAlphaValueOpaque: CGFloat = 1.0
private let defaultAlphaValueTransparent: CGFloat = 0.0
private let defaultAlphaValueSemiTransparent: CGFloat = 0.7

public protocol KolodaViewDataSource:class {
    
    func kolodaNumberOfCards(_ koloda: KolodaView) -> UInt
    func koloda(_ koloda: KolodaView, viewForCardAtIndex index: UInt) -> UIView
    func koloda(_ koloda: KolodaView, viewForCardOverlayAtIndex index: UInt) -> OverlayView?
    
}

public extension KolodaViewDataSource {
    
    func koloda(_ koloda: KolodaView, viewForCardOverlayAtIndex index: UInt) -> OverlayView? {
        return nil
    }
    
}

public protocol KolodaViewDelegate:class {
    
    func koloda(_ koloda: KolodaView, allowedDirectionsForIndex index: UInt) -> [SwipeResultDirection]
    func koloda(_ koloda: KolodaView, shouldSwipeCardAtIndex index: UInt, inDirection direction: SwipeResultDirection) -> Bool
    func koloda(_ koloda: KolodaView, didSwipeCardAtIndex index: UInt, inDirection direction: SwipeResultDirection)
    func kolodaDidRunOutOfCards(_ koloda: KolodaView)
    func koloda(_ koloda: KolodaView, didSelectCardAtIndex index: UInt)
    func kolodaShouldApplyAppearAnimation(_ koloda: KolodaView) -> Bool
    func kolodaShouldMoveBackgroundCard(_ koloda: KolodaView) -> Bool
    func kolodaShouldTransparentizeNextCard(_ koloda: KolodaView) -> Bool
    func koloda(_ koloda: KolodaView, draggedCardWithPercentage finishPercentage: CGFloat, inDirection direction: SwipeResultDirection)
    func kolodaDidResetCard(_ koloda: KolodaView)
    func kolodaSwipeThresholdRatioMargin(_ koloda: KolodaView) -> CGFloat?
    func koloda(_ koloda: KolodaView, didShowCardAtIndex index: UInt)
    func koloda(_ koloda: KolodaView, shouldDragCardAtIndex index: UInt ) -> Bool
    
}

public extension KolodaViewDelegate {
    func koloda(_ koloda: KolodaView, shouldSwipeCardAtIndex index: UInt, inDirection direction: SwipeResultDirection) -> Bool { return true }
    
    func koloda(_ koloda: KolodaView, allowedDirectionsForIndex index: UInt) -> [SwipeResultDirection] { return [.Left, .Right] }
    func koloda(_ koloda: KolodaView, didSwipeCardAtIndex index: UInt, inDirection direction: SwipeResultDirection) {}
    func kolodaDidRunOutOfCards(_ koloda: KolodaView) {}
    func koloda(_ koloda: KolodaView, didSelectCardAtIndex index: UInt) {}
    func kolodaShouldApplyAppearAnimation(_ koloda: KolodaView) -> Bool { return true }
    func kolodaShouldMoveBackgroundCard(_ koloda: KolodaView) -> Bool { return true }
    func kolodaShouldTransparentizeNextCard(_ koloda: KolodaView) -> Bool { return true }
    func koloda(_ koloda: KolodaView, draggedCardWithPercentage finishPercentage: CGFloat, inDirection direction: SwipeResultDirection) {}
    func kolodaDidResetCard(_ koloda: KolodaView) {}
    func kolodaSwipeThresholdRatioMargin(_ koloda: KolodaView) -> CGFloat? { return nil}
    func koloda(_ koloda: KolodaView, didShowCardAtIndex index: UInt) {}
    func koloda(_ koloda: KolodaView, shouldDragCardAtIndex index: UInt ) -> Bool { return true }
    
}

public class KolodaView: UIView, DraggableCardDelegate {
    
    //Opacity values
    public var alphaValueOpaque = defaultAlphaValueOpaque
    public var alphaValueTransparent = defaultAlphaValueTransparent
    public var alphaValueSemiTransparent = defaultAlphaValueSemiTransparent
    public var shouldPassthroughTapsWhenNoVisibleCards = false
    
    public weak var dataSource: KolodaViewDataSource? {
        didSet {
            setupDeck()
        }
    }
    
    public weak var delegate: KolodaViewDelegate?
    
    public lazy var animator: KolodaViewAnimator = {
        return KolodaViewAnimator(koloda: self)
    }()
    
    internal var animating = false
    
    internal var shouldTransparentizeNextCard: Bool {
        return delegate?.kolodaShouldTransparentizeNextCard(self) ?? true
    }
    
    private(set) public var currentCardIndex = 0
    private(set) public var countOfCards = 0
    public var countOfVisibleCards = defaultCountOfVisibleCards
    public var visibleCards = [DraggableCardView]()
    
    override public func layoutSubviews() {
        super.layoutSubviews()
        
        if !animating {
            layoutDeck()
        }
    }
    
    //MARK: Configurations
    
    private func setupDeck() {
        if let dataSource = dataSource {
            countOfCards = Int(dataSource.kolodaNumberOfCards(self))
            
            if countOfCards - currentCardIndex > 0 {
                let countOfNeededCards = min(countOfVisibleCards, countOfCards - currentCardIndex)
                
                for index in 0..<countOfNeededCards {
                    let actualIndex = UInt(index + currentCardIndex)
                    let nextCardView = createCardAtIndex(actualIndex)
                    let isTop = index == 0
                    nextCardView.isUserInteractionEnabled = isTop
                    nextCardView.alpha = alphaValueOpaque
                    if shouldTransparentizeNextCard && !isTop {
                        nextCardView.alpha = alphaValueSemiTransparent
                    }
                    visibleCards.append(nextCardView)
                    isTop ? addSubview(nextCardView) : insertSubview(nextCardView, belowSubview: visibleCards[index - 1])
                }
                self.delegate?.koloda(self, didShowCardAtIndex: UInt(currentCardIndex))
            }
        }
    }
    
    public func layoutDeck() {
        for (index, card) in visibleCards.enumerated() {
            layoutCard(card, atIndex: UInt(index))
        }
    }
    
    private func layoutCard(_ card: DraggableCardView, atIndex index: UInt) {
        if index == 0 {
            card.layer.transform = CATransform3DIdentity
            card.frame = frameForTopCard()
        } else {
            let cardParameters = backgroundCardParametersForFrame(frameForCardAtIndex(UInt(index)))
            let scale = cardParameters.scale
            card.layer.transform = CATransform3DScale(CATransform3DIdentity, scale.width, scale.height, 1.0)
            card.frame = cardParameters.frame
        }
    }
    
    //MARK: Frames
    public func frameForCardAtIndex(_ index: UInt) -> CGRect {
        let bottomOffset:CGFloat = 0
        let topOffset = defaultBackgroundCardsTopMargin * CGFloat(countOfVisibleCards - 1)
        let scalePercent = defaultBackgroundCardsScalePercent
        let width = self.frame.width * pow(scalePercent, CGFloat(index))
        let xOffset = (self.frame.width - width) / 2
        let height = (self.frame.height - bottomOffset - topOffset) * pow(scalePercent, CGFloat(index))
        let multiplier: CGFloat = index > 0 ? 1.0 : 0.0
        let previousCardFrame = index > 0 ? frameForCardAtIndex(max(index - 1, 0)) : CGRect.zero
        let yOffset = (previousCardFrame.height - height + previousCardFrame.origin.y + defaultBackgroundCardsTopMargin) * multiplier
        let frame = CGRect(x: xOffset, y: yOffset, width: width, height: height)
        
        return frame
    }
    
    internal func frameForTopCard() -> CGRect {
        return frameForCardAtIndex(0)
    }
    
    internal func backgroundCardParametersForFrame(_ initialFrame: CGRect) -> (frame: CGRect, scale: CGSize) {
        var finalFrame = frameForTopCard()
        finalFrame.origin = initialFrame.origin
        
        var scale = CGSize.zero
        scale.width = initialFrame.width / finalFrame.width
        scale.height = initialFrame.height / finalFrame.height
        
        return (finalFrame, scale)
    }
    
    internal func moveOtherCardsWithPercentage(_ percentage: CGFloat) {
        if visibleCards.count > 1 {
            for index in 1..<visibleCards.count {
                let previousCardFrame = frameForCardAtIndex(UInt(index - 1))
                var frame = frameForCardAtIndex(UInt(index))
                let fraction = percentage / 100
                
                let distanceToMoveY: CGFloat = (frame.origin.y - previousCardFrame.origin.y) * fraction
                
                frame.origin.y -= distanceToMoveY
                
                let distanceToMoveX: CGFloat = (previousCardFrame.origin.x - frame.origin.x) * fraction
                
                frame.origin.x += distanceToMoveX
                
                let widthDelta = (previousCardFrame.size.width - frame.size.width) * fraction
                let heightDelta = (previousCardFrame.size.height - frame.size.height) * fraction
                
                frame.size.width += widthDelta
                frame.size.height += heightDelta
                
                let cardParameters = backgroundCardParametersForFrame(frame)
                let scale = cardParameters.scale
                
                let card = visibleCards[index]
                
                card.layer.transform = CATransform3DScale(CATransform3DIdentity, scale.width, scale.height, 1.0)
                card.frame = cardParameters.frame
                
                //For fully visible next card, when moving top card
                if shouldTransparentizeNextCard {
                    if index == 1 {
                        card.alpha = alphaValueSemiTransparent + (alphaValueOpaque - alphaValueSemiTransparent) * fraction
                    }
                }
            }
        }
    }
    
    //MARK: Animations
    
    private func applyAppearAnimation() {
        alpha = 0
        isUserInteractionEnabled = false
        animating = true
        
        animator.animateAppearanceWithCompletion { [weak self] _ in
            self?.isUserInteractionEnabled = true
            self?.animating = false
        }
    }
    
    public func applyAppearAnimationIfNeeded() {
        if let shouldApply = delegate?.kolodaShouldApplyAppearAnimation(self) , shouldApply == true {
            applyAppearAnimation()
        }
    }
    
    //MARK: DraggableCardDelegate
    
    func card(_ card: DraggableCardView, wasDraggedWithFinishPercentage percentage: CGFloat, inDirection direction: SwipeResultDirection) {
        animating = true
        
        if let shouldMove = delegate?.kolodaShouldMoveBackgroundCard(self) , shouldMove {
            self.moveOtherCardsWithPercentage(percentage)
        }
        delegate?.koloda(self, draggedCardWithPercentage: percentage, inDirection: direction)
    }
    
    func card(_ card: DraggableCardView, shouldSwipeInDirection direction: SwipeResultDirection) -> Bool {
        return delegate?.koloda(self, shouldSwipeCardAtIndex: UInt(self.currentCardIndex), inDirection: direction) ?? true
    }
    
    func card(cardAllowedDirections card: DraggableCardView) -> [SwipeResultDirection] {
        let index = currentCardIndex + visibleCards.index(of: card)!
        return delegate?.koloda(self, allowedDirectionsForIndex: UInt(index)) ?? [.Left, .Right]
    }
    
    func card(_ card: DraggableCardView, wasSwipedInDirection direction: SwipeResultDirection) {
        swipedAction(direction)
    }
    
    func card(cardWasReset card: DraggableCardView) {
        if visibleCards.count > 1 {
            animating = true
            animator.resetBackgroundCardsWithCompletion { [weak self] _ in
                guard let _self = self else {
                    return
                }
                
                _self.animating = false
                
                for index in 1..<_self.visibleCards.count {
                    let card = _self.visibleCards[index]
                    if _self.shouldTransparentizeNextCard {
                        card.alpha = index == 0 ? _self.alphaValueOpaque : _self.alphaValueSemiTransparent
                    }
                }
            }
        } else {
            animating = false
        }
        
        delegate?.kolodaDidResetCard(self)
    }
    
    func card(cardWasTapped card: DraggableCardView) {
        guard let visibleIndex = visibleCards.index(of: card) else { return }
        
        let index = currentCardIndex + visibleIndex
        delegate?.koloda(self, didSelectCardAtIndex: UInt(index))
    }
    
    func card(cardSwipeThresholdRatioMargin card: DraggableCardView) -> CGFloat? {
        return delegate?.kolodaSwipeThresholdRatioMargin(self)
    }
    
    func card(cardShouldDrag card: DraggableCardView) -> Bool {
        guard let visibleIndex = visibleCards.index(of: card) else { return true}
        
        let index = currentCardIndex + visibleIndex
        return delegate?.koloda(self, shouldDragCardAtIndex: UInt(index)) ?? true
    }
    
    //MARK: Private
    private func clear() {
        currentCardIndex = 0
        
        for card in visibleCards {
            card.removeFromSuperview()
        }
        
        visibleCards.removeAll(keepingCapacity: true)
    }
    
    //MARK: Actions
    private func swipedAction(_ direction: SwipeResultDirection) {
        animating = true
        visibleCards.removeFirst()
        
        currentCardIndex += 1
        let shownCardsCount = currentCardIndex + countOfVisibleCards
        if shownCardsCount - 1 < countOfCards {
            loadNextCard()
        }
        
        if !visibleCards.isEmpty {
            animateCardsAfterLoadingWithCompletion { [weak self] in
                guard let _self = self else {
                    return
                }
                
                _self.visibleCards.last?.isHidden = false
                _self.animating = false
                _self.delegate?.koloda(_self, didSwipeCardAtIndex: UInt(_self.currentCardIndex - 1), inDirection: direction)
                _self.delegate?.koloda(_self, didShowCardAtIndex: UInt(_self.currentCardIndex))
            }
        } else {
            animating = false
            delegate?.koloda(self, didSwipeCardAtIndex: UInt(self.currentCardIndex - 1), inDirection: direction)
            delegate?.kolodaDidRunOutOfCards(self)
        }
    }
    
    private func loadNextCard() {
        guard dataSource != nil else {
            return
        }
        
        let cardParameters = backgroundCardParametersForFrame(frameForCardAtIndex(UInt(visibleCards.count)))
        let lastCard = createCardAtIndex(UInt(currentCardIndex + countOfVisibleCards - 1), frame: cardParameters.frame)
        
        let scale = cardParameters.scale
        lastCard.layer.transform = CATransform3DScale(CATransform3DIdentity, scale.width, scale.height, 1)
        lastCard.isHidden = true
        lastCard.isUserInteractionEnabled = true
        
        if let card = visibleCards.last {
            insertSubview(lastCard, belowSubview: card)
        } else {
            addSubview(lastCard)
        }
        visibleCards.append(lastCard)
    }
    
    private func animateCardsAfterLoadingWithCompletion(_ completion: ((Void) -> Void)? = nil) {
        for (index, currentCard) in visibleCards.enumerated() {
            currentCard.removeAnimations()
            
            currentCard.isUserInteractionEnabled = index == 0
            let cardParameters = backgroundCardParametersForFrame(frameForCardAtIndex(UInt(index)))
            var animationCompletion: ((Bool) -> Void)? = nil
            if index != 0 {
                if shouldTransparentizeNextCard {
                    currentCard.alpha = alphaValueSemiTransparent
                }
            } else {
                animationCompletion = { finished in
                    completion?()
                }
                
                if shouldTransparentizeNextCard {
                    animator.applyAlphaAnimation(currentCard, alpha: alphaValueOpaque)
                } else {
                    currentCard.alpha = alphaValueOpaque
                }
            }
            
            animator.applyScaleAnimation(
                currentCard,
                scale: cardParameters.scale,
                frame: cardParameters.frame,
                duration: defaultBackgroundCardFrameAnimationDuration,
                completion: animationCompletion
            )
        }
    }
    
    public func revertAction() {
        if currentCardIndex > 0 && !animating {
            
            if countOfCards - currentCardIndex >= countOfVisibleCards {
                if let lastCard = visibleCards.last {
                    lastCard.removeFromSuperview()
                    visibleCards.removeLast()
                }
            }
            currentCardIndex -= 1
            
            if dataSource != nil {
                let firstCardView = createCardAtIndex(UInt(currentCardIndex), frame: frameForTopCard())
                
                if shouldTransparentizeNextCard {
                    firstCardView.alpha = alphaValueTransparent
                }
                firstCardView.delegate = self
                
                addSubview(firstCardView)
                visibleCards.insert(firstCardView, at: 0)
                
                animating = true
                animator.applyReverseAnimation(firstCardView, completion: { [weak self] _ in
                    guard let _self = self else {
                        return
                    }
                    
                    _self.animating = false
                    _self.delegate?.koloda(_self, didShowCardAtIndex: UInt(_self.currentCardIndex))
                    })
            }
            
            for (index, card) in visibleCards.dropFirst().enumerated() {
                if shouldTransparentizeNextCard {
                    card.alpha = alphaValueSemiTransparent
                }
                card.isUserInteractionEnabled = false
                
                let cardParameters = backgroundCardParametersForFrame(frameForCardAtIndex(UInt(index + 1)))
                animator.applyScaleAnimation(
                    card,
                    scale: cardParameters.scale,
                    frame: cardParameters.frame,
                    duration: defaultBackgroundCardFrameAnimationDuration,
                    completion: nil
                )
            }
        }
    }
    
    private func loadMissingCards(_ missingCardsCount: Int) {
        if missingCardsCount > 0 {
            let cardsToAdd = min(missingCardsCount, countOfCards - currentCardIndex)
            let startIndex = visibleCards.count
            let endIndex = startIndex + cardsToAdd - 1
            
            for index in startIndex...endIndex {
                let nextCardView = generateCard(frameForTopCard())
                layoutCard(nextCardView, atIndex: UInt(index))
                nextCardView.alpha = shouldTransparentizeNextCard ? alphaValueSemiTransparent : alphaValueOpaque
                
                visibleCards.append(nextCardView)
                configureCard(nextCardView, atIndex: UInt(currentCardIndex + index))
                insertSubview(nextCardView, belowSubview: visibleCards[index - 1])
            }
        }
    }
    
    private func reconfigureCards() {
        if dataSource != nil {
            for (index, card) in visibleCards.enumerated() {
                let actualIndex = UInt(currentCardIndex + index)
                configureCard(card, atIndex: actualIndex)
            }
        }
    }
    
    private func missingCardsCount() -> Int {
        return min(countOfVisibleCards - visibleCards.count, countOfCards - (currentCardIndex + 1))
    }
    
    // MARK: Public
    
    public func reloadData() {
        guard let numberOfCards = dataSource?.kolodaNumberOfCards(self) , numberOfCards > 0 else {
            clear()
            return
        }
        
        if currentCardIndex == 0 {
            clear()
        }
        
        countOfCards = Int(numberOfCards)
        if countOfCards - (currentCardIndex + visibleCards.count) > 0 {
            if !visibleCards.isEmpty {
                let missingCards = missingCardsCount()
                loadMissingCards(missingCards)
            } else {
                setupDeck()
                layoutDeck()
                applyAppearAnimationIfNeeded()
            }
        } else {
            reconfigureCards()
        }
    }
    
    public func swipe(_ direction: SwipeResultDirection) {
        
        let validDirection = delegate?.koloda(self, allowedDirectionsForIndex: UInt(currentCardIndex)).contains(direction) ?? true
        guard validDirection else { return }
        
        if !animating {
            if let frontCard = visibleCards.first {
                animating = true
                
                if visibleCards.count > 1 {
                    let nextCard = visibleCards[1]
                    nextCard.alpha = shouldTransparentizeNextCard ? alphaValueSemiTransparent : alphaValueOpaque
                }
                frontCard.swipe(direction)
                frontCard.delegate = nil
            }
        }
    }
    
    public func resetCurrentCardIndex() {
        clear()
        reloadData()
    }
    
    public func viewForCardAtIndex(_ index: Int) -> UIView? {
        if visibleCards.count + currentCardIndex > index && index >= currentCardIndex {
            return visibleCards[index - currentCardIndex].contentView
        } else {
            return nil
        }
    }
    
    public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        if !shouldPassthroughTapsWhenNoVisibleCards {
            return super.point(inside: point, with: event)
        }
        
        return visibleCards.count > 0
    }
    
    // MARK: Cards managing - Insertion
    
    private func insertVisibleCardsWithIndexes(_ visibleIndexes: [Int]) -> [DraggableCardView] {
        var insertedCards: [DraggableCardView] = []
        visibleIndexes.forEach { insertionIndex in
            let card = createCardAtIndex(UInt(insertionIndex))
            let visibleCardIndex = insertionIndex - currentCardIndex
            visibleCards.insert(card, at: visibleCardIndex)
            if visibleCardIndex == 0 {
                card.isUserInteractionEnabled = true
                card.alpha = alphaValueOpaque
                insertSubview(card, at: visibleCards.count - 1)
            } else {
                card.isUserInteractionEnabled = false
                card.alpha = shouldTransparentizeNextCard ? alphaValueSemiTransparent : alphaValueOpaque
                insertSubview(card, belowSubview: visibleCards[visibleCardIndex - 1])
            }
            layoutCard(card, atIndex: UInt(visibleCardIndex))
            insertedCards.append(card)
        }
        
        return insertedCards
    }
    
    private func removeCards(_ cards: [DraggableCardView]) {
        cards.forEach { card in
            card.delegate = nil
            card.removeFromSuperview()
        }
    }
    
    private func removeCards(_ cards: [DraggableCardView], animated: Bool) {
        visibleCards.removeLast(cards.count)
        if animated {
            animator.applyRemovalAnimation(
                cards,
                completion: { _ in
                    self.removeCards(cards)
                }
            )
        } else {
            self.removeCards(cards)
        }
    }
    
    public func insertCardAtIndexRange(_ indexRange: CountableRange<Int>, animated: Bool = true) {
        guard let dataSource = dataSource else {
            return
        }
        
        let currentItemsCount = countOfCards
        countOfCards = Int(dataSource.kolodaNumberOfCards(self))
        
        let visibleIndexes = [Int](indexRange).filter { $0 >= currentCardIndex && $0 < currentCardIndex + countOfVisibleCards }
        let insertedCards = insertVisibleCardsWithIndexes(visibleIndexes.sorted())
        let cardsToRemove = visibleCards.dropFirst(countOfVisibleCards).map { $0 }
        removeCards(cardsToRemove, animated: animated)
        animator.resetBackgroundCardsWithCompletion()
        if animated {
            animating = true
            animator.applyInsertionAnimation(
                insertedCards,
                completion: { _ in
                    self.animating = false
                }
            )
        }
        
        assert(
            currentItemsCount + indexRange.count == countOfCards,
            "Cards count after update is not equal to data source count"
        )
    }
    
    // MARK: Cards managing - Deletion
    
    private func proceedDeletionInRange(_ range: CountableRange<Int>) {
        let deletionIndexes = [Int](range)
        deletionIndexes.sorted { $0 > $1 }.forEach { deletionIndex in
            let visibleCardIndex = deletionIndex - currentCardIndex
            let card = visibleCards[visibleCardIndex]
            card.delegate = nil
            card.swipe(.Right)
            visibleCards.remove(at: visibleCardIndex)
        }
    }
    
    public func removeCardInIndexRange(_ indexRange: CountableRange<Int>, animated: Bool) {
        guard let dataSource = dataSource else {
            return
        }
        
        animating = true
        let currentItemsCount = countOfCards
        countOfCards = Int(dataSource.kolodaNumberOfCards(self))
        
        let visibleIndexes = [Int](indexRange).filter { $0 >= currentCardIndex && $0 < currentCardIndex + countOfVisibleCards }
        if !visibleIndexes.isEmpty {
            proceedDeletionInRange(visibleIndexes[0]..<visibleIndexes[visibleIndexes.count - 1])
        }
        loadMissingCards(missingCardsCount())
        layoutDeck()
        for (index, card) in visibleCards.enumerated() {
            card.alpha = shouldTransparentizeNextCard && index != 0 ? alphaValueSemiTransparent : alphaValueOpaque
            card.isUserInteractionEnabled = index == 0
        }
        animating = false
        
        assert(
            currentItemsCount - indexRange.count == countOfCards,
            "Cards count after update is not equal to data source count"
        )
    }
    
    // MARK: Cards managing - Reloading
    
    public func reloadCardsInIndexRange(_ indexRange: CountableRange<Int>) {
        guard dataSource != nil else {
            return
        }
        
        let visibleIndexes = [Int](indexRange).filter { $0 >= currentCardIndex && $0 < currentCardIndex + countOfVisibleCards }
        visibleIndexes.forEach { index in
            let visibleCardIndex = index - currentCardIndex
            if visibleCards.count > visibleCardIndex {
                let card = visibleCards[visibleCardIndex]
                configureCard(card, atIndex: UInt(index))
            }
        }
    }
}
