//
//  SwipeView.swift
//  KSSwipeStack
//
//  Created by Simon Arneson on 2017-03-24.
//  Copyright © 2017 Kicksort Consulting AB. All rights reserved.
//

import UIKit
import RxSwift


struct SwipeHistoryItem {
    let data: SwipableData
    let origin: CGPoint
}

/// Represents a swipable view to be rendered in the swipe stack. 
/// The visual representation of a SwipeData object.
public class SwipeView: UIView {
    private lazy var swipeHelper = SwipeHelper(with: frame)
    private lazy var horizontalPan = PanDirectionGestureRecognizer(direction: .Horizontal, target: self, action: #selector(respondToHorizontalPan))
    private lazy var verticalPan = PanDirectionGestureRecognizer(direction: .Vertical, target: self, action: #selector(respondToVerticalPan))
    
    fileprivate var dataset: [SwipableData] = []
    fileprivate var renderedCards: [SwipableView] = []
    fileprivate var swipeHistory: [SwipeHistoryItem] = []
    fileprivate var options = SwipeOptions()
    
    fileprivate var swipeDelegate: SwipeDelegate?
    fileprivate var swipeSubject: PublishSubject<Swipe>?
    fileprivate var refillSubject: PublishSubject<Swipe>?
    
    public func setup() {
        setup(options: self.options, swipeDelegate: nil)
    }
    
    public func setup(options: SwipeOptions) {
        setup(options: options, swipeDelegate: nil)
    }
    
    public func setup(swipeDelegate: SwipeDelegate?) {
        setup(options: self.options, swipeDelegate: swipeDelegate)
    }
    
    public func setup(options: SwipeOptions, swipeDelegate: SwipeDelegate?) {
        self.options = options
        swipeHelper.options = options
        
        if let swipeDelegate = swipeDelegate {
            self.swipeDelegate = swipeDelegate
        }
        
        setSwipeDirections(horizontal: options.allowHorizontalSwipes, vertical: options.allowVerticalSwipes)
    }
    
    /**
     Sets whether it should be possible to swipe cards horizontally and/or vertically.
     - parameter horizontal: Set to true if the SwipeView should respond to horizontal swipes.
     - parameter vertical: Set to true if the SwipeView should respond to vertical swipes.
     */
    public func setSwipeDirections(horizontal: Bool, vertical: Bool) {
        if horizontal {
            addGestureRecognizer(horizontalPan)
        } else {
            removeGestureRecognizer(horizontalPan)
        }
    
        if vertical {
            addGestureRecognizer(verticalPan)
        } else {
            removeGestureRecognizer(verticalPan)
        }
    }
    
    /**
     Adds a card to the stack and calls notifyDatasetUpdated to make sure it is rendered if needed.
     - parameter data: The data the new card represents.
     */
    public func addCard(_ data: SwipableData) {
        dataset.append(data)
        notifyDatasetUpdated()
    }
    
    /**
     Adds a card to the top of the stack and calls notifyDatasetUpdated to make sure it is rendered if needed.
     - parameter data: The data the new card represents.
     */
    public func addCardToTop(_ data: SwipableData) {
        let cardFrame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
        let renderedCard = renderCard(data.getView(with: cardFrame))
        renderedCards.insert(renderedCard, at: 0)
        addSubview(renderedCard)
        bringSubview(toFront: renderedCard)
    }
    
    /**
     Adds a card to the top of the stack and calls notifyDatasetUpdated to make sure it is rendered if needed.
     - parameter data: The data the new card represents.
     */
    public func addCardToTop(_ data: SwipableData, from origin: CGPoint) {
        let cardFrame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
        let renderedCard = renderCard(data.getView(with: cardFrame))
        renderedCard.frame.origin = origin
        renderedCards.insert(renderedCard, at: 0)
        addSubview(renderedCard)
        swipeHelper.transformCard(renderedCard)
        bringSubview(toFront: renderedCard)
        snapBack()
    }
    
    /**
     Get swipe events generated by the SwipeView
     - returns: RxObservable firing once for each swipe
     */
    public func getSwipes() -> Observable<Swipe> {
        if let swipeSubject = swipeSubject {
            return swipeSubject.asObserver()
        }
        swipeSubject = PublishSubject<Swipe>()
        return getSwipes()
    }
    
    /**
     Get notifications when the card stack has reached the refillThreshold defined in SwipeOptions
     - returns: RxObservable firing with the swipe which put the stack below the refillThreshold
     */
    public func needsRefill() -> Observable<Swipe> {
        if let refillSubject = refillSubject {
            return refillSubject.asObserver()
        }
        refillSubject = PublishSubject<Swipe>()
        return needsRefill()
    }
    
    // Undoes last swipe
    public func undoSwipe() {
        guard options.allowUndo else {
            return
        }
        
        if let cardToUndo = swipeHistory.popLast() {
            addCardToTop(cardToUndo.data, from: cardToUndo.origin)
        }
    }
    
    fileprivate func setupSwipeCards() {
        
    }
    
    /**
     Fetch the card currently visible at the top of the stack.
     - returns: The top card (the currently visible) in the stack.
     */
    fileprivate func getCurrentCard() -> SwipableView? {
        return renderedCards.first
    }
    
    /**
     Notify the swipe view that the dataset has changed.
     */
    public func notifyDatasetUpdated() {
        if self.renderedCards.count < options.maxRenderedCards, !dataset.isEmpty {
            fillStack()
        }
    }
    
    /**
     Fills the card stack by rendering new cards from the dataset if needed.
     */
    private func fillStack() {
        let cardFrame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
        let card = renderCard(dataset.removeFirst().getView(with: cardFrame))
        self.renderedCards.append(card)
        if self.renderedCards.count < options.maxRenderedCards, !dataset.isEmpty {
            fillStack()
        }
    }
    
    /**
     Renders a Swipeble View onto the screen and puts it in the correct postion in the stack.
     - parameter view: The SwipeableView to render.
     */
    func renderCard(_ view: SwipableView) -> SwipableView {
        if !renderedCards.isEmpty, let lastCard = renderedCards.last {
            insertSubview(view, belowSubview: lastCard)
        } else {
            addSubview(view)
            sendSubview(toBack: view)
        }
        return view
    }
    
    /// Renders the next card from the stack
    func showNextCard() {
        if !renderedCards.isEmpty {
            let swipedCard = renderedCards.removeFirst()
            self.isUserInteractionEnabled = true
            swipedCard.removeFromSuperview()
            swipedCard.respondToDismiss()
        }
        
        if self.renderedCards.count < options.maxRenderedCards, !dataset.isEmpty {
            fillStack()
        }
        
        isUserInteractionEnabled = true
    }
    
    /// Handles horizontal pan gestures (drags) in the view
    ///
    /// - Parameter gesture: the gesture itself
    @objc func respondToHorizontalPan(gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: self)
        let velocity = gesture.velocity(in: self)
        let magnitude = swipeHelper.calculateThrowMagnitude(for: velocity)
        
        if let card = getCurrentCard(){
            let previousOrigin = card.frame.origin
            let nextOrigin = CGPoint(x: self.frame.origin.x + translation.x, y: self.frame.origin.y + translation.y)
            card.center = CGPoint(x: frame.width/2 + translation.x, y: frame.height/2 + translation.y)
            swipeHelper.transformCard(card)
            
            let opacity = abs(Float(card.center.x.distance(to: self.center.x) / (frame.width / 4)))
            card.respondToSwipe(like: translation.x > 0, opacity: opacity)
            
            if gesture.state == .ended {
                let throwingThresholdExceeded = magnitude > options.throwingThreshold
                let panThresholdExceeded = abs(nextOrigin.x) > frame.width * options.horizontalPanThreshold
                if throwingThresholdExceeded {
                    if velocity.x > 0 {
                        respondToSwipe(.right, gesture: gesture)
                    } else {
                        respondToSwipe(.left, gesture: gesture)
                    }
                } else if panThresholdExceeded {
                    if previousOrigin.x < options.visibleImageOrigin.x {
                        respondToSwipe(.left, gesture: gesture)
                    } else {
                        respondToSwipe(.right, gesture: gesture)
                    }
                } else {
                    snapBack()
                }
            } else if gesture.state == .cancelled {
                snapBack()
            }
        }
    }
    
    /// Handles vertical pan gestures (drags) in the view
    ///
    /// - Parameter gesture: the gesture itself
    @objc func respondToVerticalPan(gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: self)
        let velocity = gesture.velocity(in: self)
        let magnitude = swipeHelper.calculateThrowMagnitude(for: velocity)
        
        if let card = getCurrentCard(){
            let previousOrigin = card.frame.origin
            let nextOrigin = CGPoint(x: self.frame.origin.x + translation.x, y: self.frame.origin.y + translation.y)
            card.center = CGPoint(x: frame.width/2 + translation.x, y: frame.height/2 + translation.y)
            swipeHelper.transformCard(card)
            
            let opacity = abs(Float(card.center.y.distance(to: self.center.y) / (frame.height / 4)))
            card.respondToSwipe(like: translation.y > 0, opacity: opacity)
            
            if gesture.state == .ended {
                let throwingThresholdExceeded = magnitude > options.throwingThreshold
                let panThresholdExceeded = abs(nextOrigin.y) > frame.height * options.verticalPanThreshold
                if throwingThresholdExceeded {
                    if velocity.y > 0 {
                        respondToSwipe(.down, gesture: gesture)
                    } else {
                        respondToSwipe(.up, gesture: gesture)
                    }
                } else if panThresholdExceeded {
                    if previousOrigin.y < options.visibleImageOrigin.y {
                        respondToSwipe(.up, gesture: gesture)
                    } else {
                        respondToSwipe(.down, gesture: gesture)
                    }
                } else {
                    snapBack()
                }
            } else if gesture.state == .cancelled {
                snapBack()
            }
        }
    }
    
    /// Handles when a view is swiped in the view
    ///
    /// - Parameters:
    ///   - direction: The direction in which the view was swiped
    ///   - gesture: The gesture generating the swipe
    open func respondToSwipe(_ direction: SwipeDirection, gesture: UIGestureRecognizer? = nil) {
        guard let card = getCurrentCard() else {
            // TODO
            return
        }
    
        if card.isUndoable(), let data = card.getData() {
            swipeHistory.append(SwipeHistoryItem(data: data, origin: card.frame.origin))
        }
        
        dismissCard(direction: direction, gesture: gesture, completion: { [weak self] in
            let swipe = Swipe(direction: direction, data: card.getData())
            if let swipeHandler = self?.swipeDelegate {
                swipeHandler.onNext(swipe)
            }
            if let swipeSubject = self?.swipeSubject {
                swipeSubject.onNext(swipe)
            }
            if self?.needsRefill() ?? false, let refillSubject = self?.refillSubject {
                refillSubject.onNext(swipe)
            }
        })
    }
    
    /// Resets the currently visible view to the original position with a 'snap' effect.
    func snapBack() {
        if let currentCard = getCurrentCard() {
            swipeHelper.resetCard(currentCard)
            swipeHelper.move(currentCard, duration: options.snapDuration, toPoint: options.visibleImageOrigin)
            currentCard.resetView()
        }
    }
    
    /// Get the number of items in the swipe view, both rendered and unrendered.
    ///
    /// - Returns: The number of items in the dataset
    func getDataCount() -> Int {
        return self.renderedCards.count + self.dataset.count
    }
    
    /// Checks if the refill threshold is surpassed
    ///
    /// - Returns: true if the data count is below the refill threshold
    func needsRefill() -> Bool {
        return getDataCount() <= options.refillThreshold
    }
    
    /// Used to dismiss a swipable view through an end position.
    /// Animates a movement to specified position and then dismisses the swipable view.
    /// - Parameters:
    ///   - toPoint: destination of dismissal animation
    ///   - gesture: the gesture generating the dismissal
    ///   - completion: callback firing when the animation is completed and the view is dismissed.
    fileprivate func dismissCard(direction: SwipeDirection, gesture: UIGestureRecognizer?, completion: @escaping () -> Void) {
        guard let card = getCurrentCard() else {
            return
        }
        
        isUserInteractionEnabled = !options.freezeWhenDismissing
        
        var toPoint = swipeHelper.convertToCenter(origin: direction.getSwipeEndpoint())
        
        if options.allowHorizontalSwipes && !options.allowVerticalSwipes {
            // Special case to better handle rapid flicks
            if !(card.frame.origin.x == 0 && card.frame.origin.y == 0) {
                if card.center.x > frame.width / 2 {
                    toPoint = swipeHelper.calculateEndpoint(card)
                } else if let gesture = gesture as? UIPanGestureRecognizer{
                    let velocity = gesture.velocity(in: self)
                    if !(velocity.x == 0 && velocity.y == 0) {
                        toPoint = swipeHelper.calculateEndpoint(card, basedOn: velocity)
                    }
                }
            }
        }
        
        swipeHelper.moveFastAndTransform(card, toPoint: toPoint, completion: {
            completion()
            self.showNextCard()
        })
    }
}
