//
//  TopBottomAnimation.swift
//  SwiftMessages
//
//  Created by Timothy Moose on 6/4/17.
//  Copyright © 2017 SwiftKick Mobile. All rights reserved.
//

import UIKit

@MainActor
public class TopBottomAnimation: NSObject, Animator {

    public weak var delegate: AnimationDelegate?

    public let style: TopBottomAnimationStyle

    public var showDuration: TimeInterval = 0.4

    public var hideDuration: TimeInterval = 0.2

    public var springDamping: CGFloat = 0.8

    public var closeSpeedThreshold: CGFloat = 750.0;

    public var closePercentThreshold: CGFloat = 0.33;

    public var closeAbsoluteThreshold: CGFloat = 75.0;

    public private(set) lazy var panGestureRecognizer: UIPanGestureRecognizer = {
        let pan = UIPanGestureRecognizer()
        pan.addTarget(self, action: #selector(pan(_:)))
        return pan
    }()

    weak var messageView: UIView?
    weak var containerView: UIView?
    var context: AnimationContext?

    public init(style: TopBottomAnimationStyle) {
        self.style = style
    }

    init(style: TopBottomAnimationStyle, delegate: AnimationDelegate) {
        self.style = style
        self.delegate = delegate
    }

    public func show(context: AnimationContext, completion: @escaping AnimationCompletion) {
        NotificationCenter.default.addObserver(self, selector: #selector(adjustMargins), name: UIDevice.orientationDidChangeNotification, object: nil)
        install(context: context)
        showAnimation(completion: completion)
    }

    public func hide(context: AnimationContext, completion: @escaping AnimationCompletion) {
        NotificationCenter.default.removeObserver(self)
        let view = context.messageView
        self.context = context
        UIView.animate(withDuration: hideDuration, delay: 0, options: [.beginFromCurrentState, .curveEaseIn], animations: {
            switch self.style {
            case .top:
                view.transform = CGAffineTransform(translationX: 0, y: -view.frame.height)
            case .bottom:
                view.transform = CGAffineTransform(translationX: 0, y: view.frame.maxY + view.frame.height)
            }
        }, completion: { completed in
            #if SWIFTMESSAGES_APP_EXTENSIONS
            completion(completed)
            #else
            // Fix #131 by always completing if application isn't active.
            completion(completed || UIApplication.shared.applicationState != .active)
            #endif
        })
    }

    func install(context: AnimationContext) {
        let view = context.messageView
        let container = context.containerView
        messageView = view
        containerView = container
        self.context = context
        if let adjustable = context.messageView as? MarginAdjustable {
            bounceOffset = adjustable.bounceAnimationOffset
        }
        view.translatesAutoresizingMaskIntoConstraints = false
        container.addSubview(view)
        view.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
        view.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
        switch style {
        case .top:
            view.topAnchor.constraint(equalTo: container.topAnchor, constant: -bounceOffset).with(priority: UILayoutPriority(200)).isActive = true
        case .bottom:
            view.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: bounceOffset).with(priority: UILayoutPriority(200)).isActive = true
        }
        // Important to layout now in order to get the right safe area insets
        container.layoutIfNeeded()
        adjustMargins()
        container.layoutIfNeeded()
        let animationDistance = view.frame.height
        switch style {
        case .top:
            view.transform = CGAffineTransform(translationX: 0, y: -animationDistance)
        case .bottom:
            view.transform = CGAffineTransform(translationX: 0, y: animationDistance)
        }
        if context.interactiveHide {
            if let view = view as? BackgroundViewable {
                view.backgroundView.addGestureRecognizer(panGestureRecognizer)
            } else {
                view.addGestureRecognizer(panGestureRecognizer)
            }
        }
        if let view = view as? BackgroundViewable,
            let cornerRoundingView = view.backgroundView as? CornerRoundingView,
            cornerRoundingView.roundsLeadingCorners {
            switch style {
            case .top:
                cornerRoundingView.roundedCorners = [.bottomLeft, .bottomRight]
            case .bottom:
                cornerRoundingView.roundedCorners = [.topLeft, .topRight]
            }
        }
    }

    @objc public func adjustMargins() {
        guard let adjustable = messageView as? MarginAdjustable & UIView,
            let context = context else { return }
        adjustable.preservesSuperviewLayoutMargins = false
        adjustable.insetsLayoutMarginsFromSafeArea = false
        var layoutMargins = adjustable.defaultMarginAdjustment(context: context)
        switch style {
        case .top:
            layoutMargins.top += bounceOffset
        case .bottom:
            layoutMargins.bottom += bounceOffset
        }
        adjustable.layoutMargins = layoutMargins
    }

    func showAnimation(completion: @escaping AnimationCompletion) {
        guard let view = messageView else {
            completion(false)
            return
        }
        let animationDistance = abs(view.transform.ty)
        // Cap the initial velocity at zero because the bounceOffset may not be great
        // enough to allow for greater bounce induced by a quick panning motion.
        let initialSpringVelocity = animationDistance == 0.0 ? 0.0 : min(0.0, closeSpeed / animationDistance)
        UIView.animate(withDuration: showDuration, delay: 0.0, usingSpringWithDamping: springDamping, initialSpringVelocity: initialSpringVelocity, options: [.beginFromCurrentState, .curveLinear, .allowUserInteraction], animations: {
            view.transform = .identity
        }, completion: { completed in
            // Fix #131 by always completing if application isn't active.
            #if SWIFTMESSAGES_APP_EXTENSIONS
            completion(completed)
            #else
            completion(completed || UIApplication.shared.applicationState != .active)
            #endif
        })
    }

    fileprivate var bounceOffset: CGFloat = 5

    /*
     MARK: - Pan to close
     */

    fileprivate var closing = false
    fileprivate var rubberBanding = false
    fileprivate var closeSpeed: CGFloat = 0.0
    fileprivate var closePercent: CGFloat = 0.0
    fileprivate var panTranslationY: CGFloat = 0.0

    @objc func pan(_ pan: UIPanGestureRecognizer) {
        switch pan.state {
        case .changed:
            guard let view = messageView else { return }
            let height = view.bounds.height - bounceOffset
            if height <= 0 { return }
            var velocity = pan.velocity(in: view)
            var translation = pan.translation(in: view)
            if case .top = style {
                velocity.y *= -1.0
                translation.y *= -1.0
            }
            var translationAmount = translation.y >= 0 ? translation.y : -pow(abs(translation.y), 0.7)
            if !closing {
                // Turn on rubber banding if background view is inset from message view.
                if let background = (messageView as? BackgroundViewable)?.backgroundView, background != view {
                    switch style {
                    case .top:
                        rubberBanding = background.frame.minY > 0
                    case .bottom:
                        rubberBanding = background.frame.maxY < view.bounds.height
                    }
                }
                if !rubberBanding && translationAmount < 0 { return }
                closing = true
                delegate?.panStarted(animator: self)
            }
            if !rubberBanding && translationAmount < 0 { translationAmount = 0 }
            switch style {
            case .top:
                view.transform = CGAffineTransform(translationX: 0, y: -translationAmount)
            case .bottom:
                view.transform = CGAffineTransform(translationX: 0, y: translationAmount)
            }
            closeSpeed = velocity.y
            closePercent = translation.y / height
            panTranslationY = translation.y
        case .ended, .cancelled:
            if closeSpeed > closeSpeedThreshold || closePercent > closePercentThreshold || panTranslationY > closeAbsoluteThreshold {
                delegate?.hide(animator: self)
            } else {
                closing = false
                rubberBanding = false
                closeSpeed = 0.0
                closePercent = 0.0
                panTranslationY = 0.0
                showAnimation(completion: { (completed) in
                    self.delegate?.panEnded(animator: self)
                })
            }
        default:
            break
        }
    }
}