//
//  BasePresentationController.swift
//  SideMenu
//
//  Created by Jon Kent on 10/20/18.
//

import UIKit

internal protocol PresentationModel {
    /// Draws `presentStyle.backgroundColor` behind the status bar. Default is 1.
    var statusBarEndAlpha: CGFloat { get }
    /// Enable or disable interaction with the presenting view controller while the menu is displayed. Enabling may make it difficult to dismiss the menu or cause exceptions if the user tries to present and already presented menu. `presentingViewControllerUseSnapshot` must also set to false. Default is false.
    var presentingViewControllerUserInteractionEnabled: Bool { get }
    /// Use a snapshot for the presenting vierw controller while the menu is displayed. Useful when layout changes occur during transitions. Not recommended for apps that support rotation. Default is false.
    var presentingViewControllerUseSnapshot: Bool { get }
    /// The presentation style of the menu.
    var presentationStyle: SideMenuPresentationStyle { get }
    /// Width of the menu when presented on screen, showing the existing view controller in the remaining space. Default is zero.
    var menuWidth: CGFloat { get }
}

internal protocol SideMenuPresentationControllerDelegate: class {
    func sideMenuPresentationControllerDidTap(_ presentationController: SideMenuPresentationController)
    func sideMenuPresentationController(_ presentationController: SideMenuPresentationController, didPanWith gesture: UIPanGestureRecognizer)
}

internal final class SideMenuPresentationController {

    private let config: PresentationModel
    private weak var containerView: UIView?
    private var interactivePopGestureRecognizerEnabled: Bool?
    private var clipsToBounds: Bool?
    private let leftSide: Bool
    private weak var presentedViewController: UIViewController?
    private weak var presentingViewController: UIViewController?

    private lazy var snapshotView: UIView? = {
        guard config.presentingViewControllerUseSnapshot,
            let view = presentingViewController?.view.snapshotView(afterScreenUpdates: true) else {
                return nil
        }

        view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
        return view
    }()

    private lazy var statusBarView: UIView? = {
        guard config.statusBarEndAlpha > .leastNonzeroMagnitude else { return nil }

        return UIView {
            $0.backgroundColor = config.presentationStyle.backgroundColor
            $0.autoresizingMask = [.flexibleHeight, .flexibleWidth]
            $0.isUserInteractionEnabled = false
        }
    }()

    required init(config: PresentationModel, leftSide: Bool, presentedViewController: UIViewController, presentingViewController: UIViewController, containerView: UIView) {
        self.config = config
        self.containerView = containerView
        self.leftSide = leftSide
        self.presentedViewController = presentedViewController
        self.presentingViewController = presentingViewController
    }

    deinit {
        guard presentedViewController?.isHidden == false else { return }

        // Presentations must be reversed to preserve user experience
        dismissalTransitionWillBegin()
        dismissalTransition()
        dismissalTransitionDidEnd(true)
    }
    
    func containerViewWillLayoutSubviews() {
        guard let containerView = containerView,
            let presentedViewController = presentedViewController,
            let presentingViewController = presentingViewController
            else { return }

        presentedViewController.view.untransform {
            presentedViewController.view.frame = frameOfPresentedViewInContainerView
        }
        presentingViewController.view.untransform {
            presentingViewController.view.frame = frameOfPresentingViewInContainerView
            snapshotView?.frame = presentingViewController.view.bounds
        }

        guard let statusBarView = statusBarView else { return }

        var statusBarFrame: CGRect = self.statusBarFrame
        statusBarFrame.size.height -= containerView.frame.minY
        statusBarView.frame = statusBarFrame
    }
    
    func presentationTransitionWillBegin() {
        guard let containerView = containerView,
            let presentedViewController = presentedViewController,
            let presentingViewController = presentingViewController
            else { return }

        if let snapshotView = snapshotView {
            presentingViewController.view.addSubview(snapshotView)
        }

        presentingViewController.view.isUserInteractionEnabled = config.presentingViewControllerUserInteractionEnabled
        containerView.backgroundColor = config.presentationStyle.backgroundColor
        
        layerViews()

        if let statusBarView = statusBarView {
            containerView.addSubview(statusBarView)
        }
        
        dismissalTransition()
        config.presentationStyle.presentationTransitionWillBegin(to: presentedViewController, from: presentingViewController)
    }

    func presentationTransition() {
        guard let presentedViewController = presentedViewController,
            let presentingViewController = presentingViewController
            else { return }

        transition(
            to: presentedViewController,
            from: presentingViewController,
            alpha: config.presentationStyle.presentingEndAlpha,
            statusBarAlpha: config.statusBarEndAlpha,
            scale: config.presentationStyle.presentingScaleFactor,
            translate: config.presentationStyle.presentingTranslateFactor
        )

        config.presentationStyle.presentationTransition(to: presentedViewController, from: presentingViewController)
    }
    
    func presentationTransitionDidEnd(_ completed: Bool) {
        guard completed else {
            snapshotView?.removeFromSuperview()
            dismissalTransitionDidEnd(!completed)
            return
        }

        guard let presentedViewController = presentedViewController,
            let presentingViewController = presentingViewController
            else { return }

        addParallax(to: presentingViewController.view)
        
        if let topNavigationController = presentingViewController as? UINavigationController {
            interactivePopGestureRecognizerEnabled = interactivePopGestureRecognizerEnabled ?? topNavigationController.interactivePopGestureRecognizer?.isEnabled
            topNavigationController.interactivePopGestureRecognizer?.isEnabled = false
        }

        containerViewWillLayoutSubviews()
        config.presentationStyle.presentationTransitionDidEnd(to: presentedViewController, from: presentingViewController, completed)
    }

    func dismissalTransitionWillBegin() {
        snapshotView?.removeFromSuperview()
        presentationTransition()

        guard let presentedViewController = presentedViewController,
            let presentingViewController = presentingViewController
            else { return }

        config.presentationStyle.dismissalTransitionWillBegin(to: presentedViewController, from: presentingViewController)
    }

    func dismissalTransition() {
        guard let presentedViewController = presentedViewController,
            let presentingViewController = presentingViewController
            else { return }

        transition(
            to: presentingViewController,
            from: presentedViewController,
            alpha: config.presentationStyle.menuStartAlpha,
            statusBarAlpha: 0,
            scale: config.presentationStyle.menuScaleFactor,
            translate: config.presentationStyle.menuTranslateFactor
        )

        config.presentationStyle.dismissalTransition(to: presentedViewController, from: presentingViewController)
    }

    func dismissalTransitionDidEnd(_ completed: Bool) {
        guard completed else {
            if let snapshotView = snapshotView, let presentingViewController = presentingViewController {
                presentingViewController.view.addSubview(snapshotView)
            }
            presentationTransitionDidEnd(!completed)
            return
        }

        guard let presentedViewController = presentedViewController,
            let presentingViewController = presentingViewController
            else { return }

        statusBarView?.removeFromSuperview()
        removeStyles(from: presentingViewController.containerViewController.view)
        
        if let interactivePopGestureRecognizerEnabled = interactivePopGestureRecognizerEnabled,
            let topNavigationController = presentingViewController as? UINavigationController {
            topNavigationController.interactivePopGestureRecognizer?.isEnabled = interactivePopGestureRecognizerEnabled
        }

        presentingViewController.view.isUserInteractionEnabled = true
        config.presentationStyle.dismissalTransitionDidEnd(to: presentedViewController, from: presentingViewController, completed)
    }
}

private extension SideMenuPresentationController {

    var statusBarFrame: CGRect {
        if #available(iOS 13.0, *) {
            return containerView?.window?.windowScene?.statusBarManager?.statusBarFrame ?? .zero
        } else {
            return UIApplication.shared.statusBarFrame
        }
    }

    var frameOfPresentedViewInContainerView: CGRect {
        guard let containerView = containerView else { return .zero }
        var rect = containerView.bounds
        rect.origin.x = leftSide ? 0 : rect.width - config.menuWidth
        rect.size.width = config.menuWidth
        return rect
    }

    var frameOfPresentingViewInContainerView: CGRect {
        guard let containerView = containerView else { return .zero }
        var rect = containerView.frame
        if containerView.superview != nil, containerView.frame.minY > .ulpOfOne {
            let statusBarOffset = statusBarFrame.height - rect.minY
            rect.origin.y = statusBarOffset
            rect.size.height -= statusBarOffset
        }
        return rect
    }

    func transition(to: UIViewController, from: UIViewController, alpha: CGFloat, statusBarAlpha: CGFloat, scale: CGFloat, translate: CGFloat) {
        containerViewWillLayoutSubviews()
        
        to.view.transform = .identity
        to.view.alpha = 1

        let x = (leftSide ? 1 : -1) * config.menuWidth * translate
        from.view.alpha = alpha
        from.view.transform = CGAffineTransform
            .identity
            .translatedBy(x: x, y: 0)
            .scaledBy(x: scale, y: scale)

        statusBarView?.alpha = statusBarAlpha
    }

    func layerViews() {
        guard let presentedViewController = presentedViewController,
            let presentingViewController = presentingViewController
            else { return }

        statusBarView?.layer.zPosition = 2

        if config.presentationStyle.menuOnTop {
            addShadow(to: presentedViewController.view)
            presentedViewController.view.layer.zPosition = 1
        } else {
            addShadow(to: presentingViewController.view)
            presentedViewController.view.layer.zPosition = -1
        }
    }

    func addShadow(to view: UIView) {
        view.layer.shadowColor = config.presentationStyle.onTopShadowColor.cgColor
        view.layer.shadowRadius = config.presentationStyle.onTopShadowRadius
        view.layer.shadowOpacity = config.presentationStyle.onTopShadowOpacity
        view.layer.shadowOffset = config.presentationStyle.onTopShadowOffset
        clipsToBounds = clipsToBounds ?? view.clipsToBounds
        view.clipsToBounds = false
    }

    func addParallax(to view: UIView) {
        var effects: [UIInterpolatingMotionEffect] = []

        let x = config.presentationStyle.presentingParallaxStrength.width
        if x > 0 {
            let horizontal = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis)
            horizontal.minimumRelativeValue = -x
            horizontal.maximumRelativeValue = x
            effects.append(horizontal)
        }

        let y = config.presentationStyle.presentingParallaxStrength.height
        if y > 0 {
            let vertical = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis)
            vertical.minimumRelativeValue = -y
            vertical.maximumRelativeValue = y
            effects.append(vertical)
        }

        if effects.count > 0 {
            let group = UIMotionEffectGroup()
            group.motionEffects = effects
            view.motionEffects.removeAll()
            view.addMotionEffect(group)
        }
    }

    func removeStyles(from view: UIView) {
        view.motionEffects.removeAll()
        view.layer.shadowOpacity = 0
        view.layer.shadowOpacity = 0
        view.clipsToBounds = clipsToBounds ?? true
        clipsToBounds = false
    }
}