import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import UIKitRuntimeUtils

final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDelegate {
    private var theme: NavigationControllerTheme
    let isFlat: Bool
    
    private let dim: ASDisplayNode
    private let scrollNode: ASScrollNode
    let container: NavigationContainer
    
    private var panRecognizer: InteractiveTransitionGestureRecognizer?
    
    private(set) var isReady: Bool = false
    private(set) var dismissProgress: CGFloat = 0.0
    var isReadyUpdated: (() -> Void)?
    var updateDismissProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
    var interactivelyDismissed: ((Bool) -> Void)?
    
    private var isUpdatingState = false
    private var ignoreScrolling = false
    private var isDismissed = false
    private var isInteractiveDimissEnabled = true
    
    private var validLayout: ContainerViewLayout?
    private var horizontalDismissOffset: CGFloat?
    
    var keyboardViewManager: KeyboardViewManager? {
        didSet {
            if self.keyboardViewManager !== oldValue {
                self.container.keyboardViewManager = self.keyboardViewManager
            }
        }
    }
    
    var canHaveKeyboardFocus: Bool = false {
        didSet {
            self.container.canHaveKeyboardFocus = self.canHaveKeyboardFocus
        }
    }
    
    init(theme: NavigationControllerTheme, isFlat: Bool, controllerRemoved: @escaping (ViewController) -> Void) {
        self.theme = theme
        self.isFlat = isFlat
        
        self.dim = ASDisplayNode()
        self.dim.alpha = 0.0
        
        self.scrollNode = ASScrollNode()
        
        self.container = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved)
        self.container.clipsToBounds = true
        
        super.init()
        
        self.addSubnode(self.dim)
        self.addSubnode(self.scrollNode)
        self.scrollNode.addSubnode(self.container)
        
        self.isReady = self.container.isReady
        self.container.isReadyUpdated = { [weak self] in
            guard let strongSelf = self else {
                return
            }
            if !strongSelf.isReady {
                strongSelf.isReady = true
                if !strongSelf.isUpdatingState {
                    strongSelf.isReadyUpdated?()
                }
            }
        }
        
        applySmoothRoundedCorners(self.container.layer)
    }
    
    override func didLoad() {
        super.didLoad()
        
        self.scrollNode.view.alwaysBounceVertical = false
        self.scrollNode.view.alwaysBounceHorizontal = false
        self.scrollNode.view.bounces = false
        self.scrollNode.view.showsVerticalScrollIndicator = false
        self.scrollNode.view.showsHorizontalScrollIndicator = false
        if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
            self.scrollNode.view.contentInsetAdjustmentBehavior = .never
        }
        self.scrollNode.view.delaysContentTouches = false
        self.scrollNode.view.clipsToBounds = false
        self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
        self.scrollNode.view.tag = 0x5C4011
        
        let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
            guard let strongSelf = self, !strongSelf.isDismissed else {
                return []
            }
            return .right
        })
        if #available(iOS 13.4, *) {
            panRecognizer.allowedScrollTypesMask = .continuous
        }
        self.panRecognizer = panRecognizer
        if let layout = self.validLayout {
            switch layout.metrics.widthClass {
            case .compact:
                panRecognizer.isEnabled = true
            case .regular:
                panRecognizer.isEnabled = false
            }
        }
        panRecognizer.delegate = self.wrappedGestureRecognizerDelegate
        panRecognizer.delaysTouchesBegan = false
        panRecognizer.cancelsTouchesInView = true
        if !self.isFlat {
            self.view.addGestureRecognizer(panRecognizer)
            self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
        }
    }
    
    public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if gestureRecognizer == self.panRecognizer, let gestureRecognizer = self.panRecognizer, gestureRecognizer.numberOfTouches == 0 {
            let translation = gestureRecognizer.velocity(in: gestureRecognizer.view)
            if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
                return false
            }
            if translation.x < 4.0 {
                return false
            }
            if self.isDismissed {
                return false
            }
            return true
        } else {
            return true
        }
    }
    
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return false
    }
    
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
            return true
        }
        return false
    }
    
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
            return true
        }
        return false
    }
    
    private func checkInteractiveDismissWithControllers() -> Bool {
        if let controller = self.container.controllers.last {
            if !controller.attemptNavigation({
            }) {
                return false
            }
        }
        return true
    }
    
    @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
        switch recognizer.state {
        case .began:
            self.horizontalDismissOffset = 0.0
        case .changed:
            let translation = max(0.0, recognizer.translation(in: self.view).x)
            let progress = translation / self.bounds.width
            self.horizontalDismissOffset = translation
            self.dismissProgress = progress
            self.applyDismissProgress(transition: .immediate, completion: {})
            self.container.updateAdditionalKeyboardLeftEdgeOffset(translation, transition: .immediate)
        case .ended, .cancelled:
            let translation = max(0.0, recognizer.translation(in: self.view).x)
            let progress = translation / self.bounds.width
            let velocity = recognizer.velocity(in: self.view).x
            
            if (velocity > 1000 || progress > 0.2) && self.checkInteractiveDismissWithControllers() {
                self.isDismissed = true
                self.horizontalDismissOffset = self.bounds.width
                self.dismissProgress = 1.0
                let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring)
                transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: self.bounds.width, y: 0.0), size: self.scrollNode.bounds.size))
                self.container.updateAdditionalKeyboardLeftEdgeOffset(self.bounds.width, transition: transition)
                self.applyDismissProgress(transition: transition, completion: { [weak self] in
                    guard let strongSelf = self else {
                        return
                    }
                    let hadInputFocus = viewTreeContainsFirstResponder(view: strongSelf.view)
                    strongSelf.keyboardViewManager?.dismissEditingWithoutAnimation(view: strongSelf.view)
                    strongSelf.interactivelyDismissed?(hadInputFocus)
                })
            } else {
                self.horizontalDismissOffset = nil
                self.dismissProgress = 0.0
                let transition: ContainedViewLayoutTransition = .animated(duration: 0.1, curve: .easeInOut)
                self.applyDismissProgress(transition: transition, completion: {})
                self.container.updateAdditionalKeyboardLeftEdgeOffset(0.0, transition: transition)
            }
        default:
            break
        }
    }
    
    @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
        if case .ended = recognizer.state {
            if !self.isDismissed {
                self.dismissWithAnimation()
            }
        }
    }
    
    private func dismissWithAnimation() {
        let scrollView = self.scrollNode.view
        let targetOffset: CGFloat
        let duration = 0.3
        let transition: ContainedViewLayoutTransition
        let dismissProgress: CGFloat
        dismissProgress = 1.0
        targetOffset = 0.0
        transition = .animated(duration: duration, curve: .easeInOut)
        self.isDismissed = true
        self.ignoreScrolling = true
        let deltaY = targetOffset - scrollView.contentOffset.y
        scrollView.setContentOffset(scrollView.contentOffset, animated: false)
        scrollView.setContentOffset(CGPoint(x: 0.0, y: targetOffset), animated: false)
        transition.animateOffsetAdditive(layer: self.scrollNode.layer, offset: -deltaY, completion: { [weak self] in
            guard let strongSelf = self else {
                return
            }
            if targetOffset == 0.0 {
                strongSelf.interactivelyDismissed?(false)
            }
        })
        self.ignoreScrolling = false
        self.dismissProgress = dismissProgress
        
        self.applyDismissProgress(transition: transition, completion: {})
        
        self.view.endEditing(true)
    }
    
    private var isDraggingHeader = false
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if self.ignoreScrolling || self.isDismissed {
            return
        }
        var progress = (self.bounds.height - scrollView.bounds.origin.y) / self.bounds.height
        progress = max(0.0, min(1.0, progress))
        self.dismissProgress = progress
        self.applyDismissProgress(transition: .immediate, completion: {})
        
        let location = scrollView.panGestureRecognizer.location(in: scrollView).offsetBy(dx: 0.0, dy: -self.container.frame.minY)
        self.isDraggingHeader = location.y < 66.0
    }
    
    private func applyDismissProgress(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
        transition.updateAlpha(node: self.dim, alpha: 1.0 - self.dismissProgress, completion: { _ in
            completion()
        })
        self.updateDismissProgress?(self.dismissProgress, transition)
    }
    
    private var endDraggingVelocity: CGPoint?
    
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        let velocity = self.endDraggingVelocity ?? CGPoint()
        self.endDraggingVelocity = nil
        
        var progress = (self.bounds.height - scrollView.bounds.origin.y) / self.bounds.height
        progress = max(0.0, min(1.0, progress))
        
        let targetOffset: CGFloat
        let velocityFactor: CGFloat = 0.4 / max(1.0, abs(velocity.y))
        let duration = Double(min(0.3, velocityFactor))
        let transition: ContainedViewLayoutTransition
        let dismissProgress: CGFloat
        if (velocity.y < -0.5 || progress >= 0.5) && self.checkInteractiveDismissWithControllers() {
            if let controller = self.container.controllers.last as? MinimizableController {
                dismissProgress = 0.0
                targetOffset = 0.0
                transition = .immediate
                
                let topEdgeOffset = self.container.view.convert(self.container.bounds, to: self.view).minY
                controller.requestMinimize(topEdgeOffset: topEdgeOffset, initialVelocity: velocity.y)
                self.dim.removeFromSupernode()
            } else {
                dismissProgress = 1.0
                targetOffset = 0.0
                transition = .animated(duration: duration, curve: .easeInOut)
                self.isDismissed = true
            }
        } else {
            dismissProgress = 0.0
            targetOffset = self.bounds.height
            transition = .animated(duration: 0.5, curve: .spring)
        }
        self.ignoreScrolling = true
        let deltaY = targetOffset - scrollView.contentOffset.y
        scrollView.setContentOffset(scrollView.contentOffset, animated: false)
        scrollView.setContentOffset(CGPoint(x: 0.0, y: targetOffset), animated: false)
        transition.animateOffsetAdditive(layer: self.scrollNode.layer, offset: -deltaY, completion: { [weak self] in
            guard let strongSelf = self else {
                return
            }
            if targetOffset == 0.0 {
                strongSelf.interactivelyDismissed?(false)
            }
        })
        self.ignoreScrolling = false
        self.dismissProgress = dismissProgress
        
        self.applyDismissProgress(transition: transition, completion: {})
    }
    
    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        self.endDraggingVelocity = velocity
        targetContentOffset.pointee = scrollView.contentOffset
    }
    
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    }
    
    func scrollViewShouldScroll(toTop scrollView: UIScrollView) -> Bool {
        return false
    }
    
    func update(layout: ContainerViewLayout, controllers: [ViewController], coveredByModalTransition: CGFloat, transition: ContainedViewLayoutTransition) {
        if self.isDismissed {
            return
        }
        
        self.isUpdatingState = true
        
        self.validLayout = layout
        
        var isStandaloneModal = false
        if let controller = controllers.first, case .standaloneModal = controller.navigationPresentation {
            isStandaloneModal = true
        }
        
        transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size))
        self.ignoreScrolling = true
        self.scrollNode.view.isScrollEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0) && self.isInteractiveDimissEnabled && !self.isFlat
        let previousBounds = self.scrollNode.bounds
        let scrollNodeFrame = CGRect(origin: CGPoint(x: self.horizontalDismissOffset ?? 0.0, y: 0.0), size: layout.size)
        self.scrollNode.frame = scrollNodeFrame
        self.scrollNode.view.contentSize = CGSize(width: layout.size.width, height: layout.size.height * 2.0)
        if !self.scrollNode.view.isDecelerating && !self.scrollNode.view.isDragging {
            let defaultBounds = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: layout.size)
            if self.scrollNode.bounds != defaultBounds {
                self.scrollNode.bounds = defaultBounds
            }
            if previousBounds.minY != defaultBounds.minY {
                transition.animateOffsetAdditive(node: self.scrollNode, offset: previousBounds.minY - defaultBounds.minY)
            }
        }
        self.ignoreScrolling = false
        
        self.scrollNode.view.isScrollEnabled = !isStandaloneModal && !self.isFlat
        
        let isLandscape = layout.orientation == .landscape
        let containerLayout: ContainerViewLayout
        let containerFrame: CGRect
        let containerScale: CGFloat
        if layout.metrics.widthClass == .compact || self.isFlat {
            self.panRecognizer?.isEnabled = true
            self.container.clipsToBounds = true
            if self.isFlat {
                self.dim.backgroundColor = .clear
            } else {
                self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
            }
            if isStandaloneModal || isLandscape || self.isFlat {
                self.container.cornerRadius = 0.0
            } else {
                self.container.cornerRadius = 10.0
            }
            
            if #available(iOS 11.0, *) {
                if layout.safeInsets.bottom.isZero {
                    self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
                } else {
                    self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
                }
            }
            
            var topInset: CGFloat
            if isStandaloneModal || isLandscape {
                topInset = 0.0
                containerLayout = layout
                
                let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerLayout.size)
                containerScale = 1.0
                containerFrame = unscaledFrame
            } else {
                topInset = 10.0
                if self.isFlat {
                    topInset = 0.0
                } else if let statusBarHeight = layout.statusBarHeight {
                    topInset += statusBarHeight
                }
                
                let effectiveStatusBarHeight: CGFloat?
                if self.isFlat {
                    effectiveStatusBarHeight = layout.statusBarHeight
                } else {
                    effectiveStatusBarHeight = nil
                }
                
                containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width, height: layout.size.height - topInset), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: layout.intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.intrinsicInsets.right), safeInsets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.safeInsets.bottom, right: layout.safeInsets.right), additionalInsets: layout.additionalInsets, statusBarHeight: effectiveStatusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
                let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - coveredByModalTransition * 10.0), size: containerLayout.size)
                let maxScale: CGFloat = (containerLayout.size.width - 16.0 * 2.0) / containerLayout.size.width
                containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition
                let maxScaledTopInset: CGFloat = topInset - 10.0
                let scaledTopInset: CGFloat = topInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition
                containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
            }
        } else {
            self.panRecognizer?.isEnabled = false
            if self.isFlat {
                self.dim.backgroundColor = .clear
                self.container.clipsToBounds = true
                self.container.cornerRadius = 0.0
                if #available(iOS 11.0, *) {
                    self.container.layer.maskedCorners = []
                }
            } else {
                self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
                self.container.clipsToBounds = true
                self.container.cornerRadius = 10.0
                if #available(iOS 11.0, *) {
                    self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
                }
            }
            
            let verticalInset: CGFloat = 44.0
            
            let maxSide = max(layout.size.width, layout.size.height)
            let minSide = min(layout.size.width, layout.size.height)
            var containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0)
            if let preferredSize = controllers.last?.preferredContentSizeForLayout(layout) {
                containerSize = preferredSize
            }
            containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize)
            containerScale = 1.0
            
            var inputHeight: CGFloat?
            if let inputHeightValue = layout.inputHeight {
                inputHeight = max(0.0, inputHeightValue - (layout.size.height - containerFrame.maxY))
            }
            
            let effectiveStatusBarHeight: CGFloat?
            if self.isFlat {
                effectiveStatusBarHeight = layout.statusBarHeight
            } else {
                effectiveStatusBarHeight = nil
            }
            
            containerLayout = ContainerViewLayout(size: containerSize, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: effectiveStatusBarHeight, inputHeight: inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
        }
        transition.updateFrameAsPositionAndBounds(node: self.container, frame: containerFrame.offsetBy(dx: 0.0, dy: layout.size.height))
        transition.updateTransformScale(node: self.container, scale: containerScale)
        self.container.update(layout: containerLayout, canBeClosed: true, controllers: controllers, transition: transition)
        
        self.isUpdatingState = false
    }
    
    func animateIn(transition: ContainedViewLayoutTransition) {
        if let controller = self.container.controllers.first, case .standaloneModal = controller.navigationPresentation {
        } else if self.isFlat {
        } else {
            transition.updateAlpha(node: self.dim, alpha: 1.0)
            transition.animatePositionAdditive(node: self.container, offset: CGPoint(x: 0.0, y: self.bounds.height + self.container.bounds.height / 2.0 - (self.container.position.y - self.bounds.height)))
        }
    }
    
    func dismiss(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) -> ContainedViewLayoutTransition {
        for controller in self.container.controllers {
            controller.viewWillDisappear(transition.isAnimated)
        }
        
        if let firstController = self.container.controllers.first, case .standaloneModal = firstController.navigationPresentation {
            for controller in self.container.controllers {
                controller.setIgnoreAppearanceMethodInvocations(true)
                controller.displayNode.removeFromSupernode()
                controller.setIgnoreAppearanceMethodInvocations(false)
                controller.viewDidDisappear(transition.isAnimated)
            }
            completion()
            return transition
        } else {
            if transition.isAnimated && !self.isFlat {
                let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
                let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
                alphaTransition.updateAlpha(node: self.dim, alpha: 0.0, beginWithCurrentState: true)
                if let lastController = self.container.controllers.last as? MinimizableController, lastController.isMinimized {
                    self.dim.layer.removeAllAnimations()
                }
                positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: self.bounds.height + self.container.bounds.height / 2.0 + self.bounds.height), beginWithCurrentState: true, completion: { [weak self] _ in
                    guard let strongSelf = self else {
                        return
                    }
                    for controller in strongSelf.container.controllers {
                        controller.viewDidDisappear(transition.isAnimated)
                    }
                    completion()
                })
                return positionTransition
            } else {
                for controller in self.container.controllers {
                    controller.setIgnoreAppearanceMethodInvocations(true)
                    controller.displayNode.removeFromSupernode()
                    controller.setIgnoreAppearanceMethodInvocations(false)
                    controller.viewDidDisappear(transition.isAnimated)
                }
                completion()
                return transition
            }
        }
    }
    
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard let result = super.hitTest(point, with: event) else {
            return nil
        }
        if !self.container.bounds.contains(self.view.convert(point, to: self.container.view)) {
            return self.dim.view
        }
        if self.isFlat {
            return result
        }
        var currentParent: UIView? = result
        var enableScrolling = true
        while true {
            if currentParent == nil {
                break
            }
            if currentParent is UIKeyInput {
                if currentParent?.disablesInteractiveModalDismiss == true {
                    enableScrolling = false
                    break
                }
            } else if let scrollView = currentParent as? UIScrollView {
                if scrollView === self.scrollNode.view {
                    break
                }
                if scrollView.disablesInteractiveModalDismiss {
                    enableScrolling = false
                    break
                } else {
                    if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top {
                        return self.scrollNode.view
                    }
                }
            } else if let listView = currentParent as? ListViewBackingView, let listNode = listView.target {
                if listNode.view.disablesInteractiveModalDismiss {
                    enableScrolling = false
                    break
                } else if listNode.scroller.isDecelerating && listNode.scroller.contentOffset.y < listNode.scroller.contentInset.top {
                    return self.scrollNode.view
                }
            } else if let currentParent, currentParent.disablesInteractiveModalDismiss {
                enableScrolling = false
                break
            }
            currentParent = currentParent?.superview
        }
        if let controller = self.container.controllers.last {
            if controller.view.disablesInteractiveModalDismiss {
                enableScrolling = false
            }
        }
        self.isInteractiveDimissEnabled = enableScrolling
        if let layout = self.validLayout {
            if layout.inputHeight != nil && layout.inputHeight != 0.0 {
                enableScrolling = false
            }
        }
        self.scrollNode.view.isScrollEnabled = enableScrolling && !self.isFlat
        return result
    }
}