//
//  SideMenuAnimationController.swift
//  SideMenu
//
//  Created by Jon Kent on 10/24/18.
//

import UIKit

internal protocol AnimationModel {
    /// The animation options when a menu is displayed. Ignored when displayed with a gesture.
    var animationOptions: UIView.AnimationOptions { get }
    /// Duration of the remaining animation when the menu is partially dismissed with gestures. Default is 0.35 seconds.
    var completeGestureDuration: Double { get }
    /// Duration of the animation when the menu is dismissed without gestures. Default is 0.35 seconds.
    var dismissDuration: Double { get }
    /// The animation initial spring velocity when a menu is displayed. Ignored when displayed with a gesture.
    var initialSpringVelocity: CGFloat { get }
    /// Duration of the animation when the menu is presented without gestures. Default is 0.35 seconds.
    var presentDuration: Double { get }
    /// The animation spring damping when a menu is displayed. Ignored when displayed with a gesture.
    var usingSpringWithDamping: CGFloat { get }
}

internal protocol SideMenuAnimationControllerDelegate: class {
    func sideMenuAnimationController(_ animationController: SideMenuAnimationController, didDismiss viewController: UIViewController)
    func sideMenuAnimationController(_ animationController: SideMenuAnimationController, didPresent viewController: UIViewController)
}

internal final class SideMenuAnimationController: NSObject, UIViewControllerAnimatedTransitioning {

    typealias Model = AnimationModel & PresentationModel

    private var config: Model
    private weak var containerView: UIView?
    private let leftSide: Bool
    private weak var originalSuperview: UIView?
    private var presentationController: SideMenuPresentationController?
    private unowned var presentedViewController: UIViewController?
    private unowned var presentingViewController: UIViewController?
    weak var delegate: SideMenuAnimationControllerDelegate?

    init(config: Model, leftSide: Bool, delegate: SideMenuAnimationControllerDelegate? = nil) {
        self.config = config
        self.leftSide = leftSide
        self.delegate = delegate
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard
            let presentedViewController = transitionContext.presentedViewController,
            let presentingViewController = transitionContext.presentingViewController
            else { return }

        if transitionContext.isPresenting {
            self.containerView = transitionContext.containerView
            self.presentedViewController = presentedViewController
            self.presentingViewController = presentingViewController
            self.presentationController = SideMenuPresentationController(
                config: config,
                leftSide: leftSide,
                presentedViewController: presentedViewController,
                presentingViewController: presentingViewController,
                containerView: transitionContext.containerView
            )
        }

        transition(using: transitionContext)
    }

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        guard let transitionContext = transitionContext else { return 0 }
        return duration(presenting: transitionContext.isPresenting, interactive: transitionContext.isInteractive)
    }

    func animationEnded(_ transitionCompleted: Bool) {
        guard let presentedViewController = presentedViewController else { return }
        if presentedViewController.isHidden {
            delegate?.sideMenuAnimationController(self, didDismiss: presentedViewController)
        } else {
            delegate?.sideMenuAnimationController(self, didPresent: presentedViewController)
        }
    }

    func transition(presenting: Bool, animated: Bool = true, interactive: Bool = false, alongsideTransition: (() -> Void)? = nil, complete: Bool = true, completion: ((Bool) -> Void)? = nil) {
        prepare(presenting: presenting)
        transitionWillBegin(presenting: presenting)
        transition(
            presenting: presenting,
            animated: animated,
            interactive: interactive,
            animations: { [weak self] in
                guard let self = self else { return }
                self.transition(presenting: presenting)
                alongsideTransition?()
            }, completion: { [weak self] _ in
                guard let self = self else { return }
                if complete {
                    self.transitionDidEnd(presenting: presenting, completed: true)
                    self.finish(presenting: presenting, completed: true)
                }
                completion?(true)
        })
    }

    func layout() {
        presentationController?.containerViewWillLayoutSubviews()
    }
}

private extension SideMenuAnimationController {

    func duration(presenting: Bool, interactive: Bool) -> Double {
        if interactive { return config.completeGestureDuration }
        return presenting ? config.presentDuration : config.dismissDuration
    }

    func prepare(presenting: Bool) {
        guard
            presenting,
            let presentingViewController = presentingViewController,
            let presentedViewController = presentedViewController
            else { return }

        originalSuperview = presentingViewController.view.superview
        containerView?.addSubview(presentingViewController.view)
        containerView?.addSubview(presentedViewController.view)
    }

    func transitionWillBegin(presenting: Bool) {
        // prevent any other menu gestures from firing
        containerView?.isUserInteractionEnabled = false
        if presenting {
            presentationController?.presentationTransitionWillBegin()
        } else {
            presentationController?.dismissalTransitionWillBegin()
        }
    }

    func transition(presenting: Bool) {
        if presenting {
            presentationController?.presentationTransition()
        } else {
            presentationController?.dismissalTransition()
        }
    }

    func transitionDidEnd(presenting: Bool, completed: Bool) {
        if presenting {
            presentationController?.presentationTransitionDidEnd(completed)
        } else {
            presentationController?.dismissalTransitionDidEnd(completed)
        }
        containerView?.isUserInteractionEnabled = true
    }

    func finish(presenting: Bool, completed: Bool) {
        guard
            presenting != completed,
            let presentingViewController = self.presentingViewController
            else { return }
        presentedViewController?.view.removeFromSuperview()
        originalSuperview?.addSubview(presentingViewController.view)
    }

    func transition(using transitionContext: UIViewControllerContextTransitioning) {
        prepare(presenting: transitionContext.isPresenting)
        transitionWillBegin(presenting: transitionContext.isPresenting)
        transition(
            presenting: transitionContext.isPresenting,
            animated: transitionContext.isAnimated,
            interactive: transitionContext.isInteractive,
            animations: { [weak self] in
                guard let self = self else { return }
                self.transition(presenting: transitionContext.isPresenting)
        }, completion: { [weak self] _ in
            guard let self = self else { return }
            let completed = !transitionContext.transitionWasCancelled
            self.transitionDidEnd(presenting: transitionContext.isPresenting, completed: completed)
            self.finish(presenting: transitionContext.isPresenting, completed: completed)

            // Called last. This causes the transition container to be removed and animationEnded() to be called.
            transitionContext.completeTransition(completed)
        })
    }

    func transition(presenting: Bool, animated: Bool = true, interactive: Bool = false, animations: @escaping (() -> Void) = {}, completion: @escaping ((Bool) -> Void) = { _ in }) {
        if !animated {
            animations()
            completion(true)
            return
        }

        let duration = self.duration(presenting: presenting, interactive: interactive)
        if interactive {
            // IMPORTANT: The non-interactive animation block will not complete if adapted for interactive. The below animation block must be used!
            UIView.animate(
                withDuration: duration,
                delay: duration, // HACK: If zero, the animation briefly flashes in iOS 11.
                options: .curveLinear,
                animations: animations,
                completion: completion
            )
            return
        }

        UIView.animate(
            withDuration: duration,
            delay: 0,
            usingSpringWithDamping: config.usingSpringWithDamping,
            initialSpringVelocity: config.initialSpringVelocity,
            options: config.animationOptions,
            animations: animations,
            completion: completion
        )
    }
}

private extension UIViewControllerContextTransitioning {

    var isPresenting: Bool {
        return viewController(forKey: .from)?.presentedViewController === viewController(forKey: .to)
    }

    var presentingViewController: UIViewController? {
        return viewController(forKey: isPresenting ? .from : .to)
    }

    var presentedViewController: UIViewController? {
        return viewController(forKey: isPresenting ? .to : .from)
    }
}
