import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import UIKitRuntimeUtils
import Display
import DirectionalPanGesture
import TelegramPresentationData
import MapKit

private let overflowInset: CGFloat = 0.0

public func attachmentDefaultTopInset(layout: ContainerViewLayout?) -> CGFloat {
    guard let layout = layout else {
        return 210.0
    }
    if case .compact = layout.metrics.widthClass {
        var factor: CGFloat = 0.2488
        if layout.size.width <= 320.0 {
            factor = 0.15
        }
        return floor(max(layout.size.width, layout.size.height) * factor)
    } else {
        return 210.0
    }
}

final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
    let wrappingNode: ASDisplayNode
    let clipNode: ASDisplayNode
    let container: NavigationContainer
        
    private(set) var isReady: Bool = false
    private(set) var dismissProgress: CGFloat = 0.0
    var isReadyUpdated: (() -> Void)?
    var updateDismissProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
    var interactivelyDismissed: (() -> Void)?
    var controllerRemoved: ((ViewController) -> Void)?
    
    var shouldCancelPanGesture: (() -> Bool)?
    var requestDismiss: (() -> Void)?
    
    var updateModalProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
    
    private var isUpdatingState = false
    private var isDismissed = false
    private var isInteractiveDimissEnabled = true
    
    public private(set) var isExpanded = false
    
    private var validLayout: (layout: ContainerViewLayout, controllers: [AttachmentContainable], coveredByModalTransition: CGFloat)?
    
    var keyboardViewManager: KeyboardViewManager? {
        didSet {
            if self.keyboardViewManager !== oldValue {
                self.container.keyboardViewManager = self.keyboardViewManager
            }
        }
    }
    
    var canHaveKeyboardFocus: Bool = false {
        didSet {
            self.container.canHaveKeyboardFocus = self.canHaveKeyboardFocus
        }
    }
    
    private var panGestureRecognizer: UIPanGestureRecognizer?
    
    var isPanningUpdated: (Bool) -> Void = { _ in }
    var isExpandedUpdated: (Bool) -> Void = { _ in }
    var onExpandAnimationCompleted: () -> Void = {}
    
    override init() {
        self.wrappingNode = ASDisplayNode()
        self.clipNode = ASDisplayNode()
        
        var controllerRemovedImpl: ((ViewController) -> Void)?
        self.container = NavigationContainer(isFlat: false, controllerRemoved: { c in
            controllerRemovedImpl?(c)
        })
        self.container.clipsToBounds = true
        self.container.overflowInset = overflowInset
        self.container.shouldAnimateDisappearance = true
        
        super.init()
        
        self.addSubnode(self.wrappingNode)
        self.wrappingNode.addSubnode(self.clipNode)
        self.clipNode.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)
        
        controllerRemovedImpl = { [weak self] c in
            self?.controllerRemoved?(c)
        }
    }
    
    override func didLoad() {
        super.didLoad()
        
        let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
        panRecognizer.delegate = self
        panRecognizer.delaysTouchesBegan = false
        panRecognizer.cancelsTouchesInView = true
        self.panGestureRecognizer = panRecognizer
        self.wrappingNode.view.addGestureRecognizer(panRecognizer)
    }
    
    func cancelPanGesture() {
        if let panGestureRecognizer = self.panGestureRecognizer, panGestureRecognizer.isEnabled {
            self.panGestureArguments = nil
            panGestureRecognizer.isEnabled = false
            panGestureRecognizer.isEnabled = true
        }
    }
    
    override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if let (layout, _, _) = self.validLayout {
            if case .regular = layout.metrics.widthClass {
                return false
            }
        }
        return true
    }
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        if let _ = gestureRecognizer as? UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer {
            if let _ = otherGestureRecognizer.view?.superview as? MKMapView {
                return false
            }
            if let view = otherGestureRecognizer.view, view.description.contains("WKChildScroll") {
                return false
//                let velocity = panGestureRecognizer.velocity(in: nil)
//                if abs(velocity.x) > abs(velocity.y) * 2.0 {
//                    return false
//                }
            }
            if let _ = otherGestureRecognizer.view?.asyncdisplaykit_node as? CollectionIndexNode {
                return false
            }
            return true
        }
        if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UILongPressGestureRecognizer {
            return true
        }
        return false
    }
    
    var isTracking: Bool {
        return self.panGestureArguments != nil
    }
    
    private var isAnimating = false
    var isPanning: Bool {
        return self.panGestureArguments != nil || self.isAnimating
    }
    
    private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)?
    @objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
        guard let (layout, controllers, coveredByModalTransition) = self.validLayout else {
            return
        }
        
        let isLandscape = layout.orientation == .landscape
        let edgeTopInset = isLandscape ? 0.0 : attachmentDefaultTopInset(layout: layout)
    
        let completion = {
            self.isAnimating = false
            guard self.panGestureArguments == nil else {
                return
            }
            self.isPanningUpdated(false)
        }
        
        switch recognizer.state {
            case .began:
                let point = recognizer.location(in: self.view)
                let currentHitView = self.hitTest(point, with: nil)
                
                var scrollViewAndListNode = self.findScrollView(view: currentHitView)
                if scrollViewAndListNode?.0.frame.height == self.frame.width {
                    scrollViewAndListNode = nil
                }
                let scrollView = scrollViewAndListNode?.0
                let listNode = scrollViewAndListNode?.1
            
                let topInset: CGFloat
                if self.isExpanded {
                    topInset = 0.0
                } else {
                    topInset = edgeTopInset
                }
                
                self.panGestureArguments = (topInset, 0.0, scrollView, listNode)
            case .changed:
                guard let (topInset, panOffset, scrollView, listNode) = self.panGestureArguments else {
                    return
                }
                let visibleContentOffset = listNode?.visibleContentOffset()
                let contentOffset = scrollView?.contentOffset.y ?? 0.0
            
                var translation = recognizer.translation(in: self.view).y

                var currentOffset = topInset + translation
            
                let epsilon = 1.0
                if case let .known(value) = visibleContentOffset, value <= epsilon {
                    if let scrollView = scrollView {
                        scrollView.bounces = false
                        scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: 0.0), animated: false)
                    }
                } else if let scrollView = scrollView, contentOffset <= -scrollView.contentInset.top + epsilon {
                    scrollView.bounces = false
                    scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: -scrollView.contentInset.top), animated: false)
                } else if let scrollView = scrollView {
                    translation = panOffset
                    currentOffset = topInset + translation
                    if self.isExpanded {
                        recognizer.setTranslation(CGPoint(), in: self.view)
                    } else if currentOffset > 0.0 {
                        scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: -scrollView.contentInset.top), animated: false)
                    }
                }
                
                self.panGestureArguments = (topInset, translation, scrollView, listNode)
                
                if !self.isExpanded {
                    if currentOffset > 0.0, let scrollView = scrollView {
                        scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView)
                    }
                }
            
                if !self.isExpanded, translation > 40.0, let shouldCancelPanGesture = self.shouldCancelPanGesture, shouldCancelPanGesture() {
                    self.cancelPanGesture()
                    self.requestDismiss?()
                    return
                }
            
                var bounds = self.bounds
                if self.isExpanded {
                    bounds.origin.y = -max(0.0, translation - edgeTopInset)
                } else {
                    bounds.origin.y = -translation
                }
                bounds.origin.y = min(0.0, bounds.origin.y)
                self.bounds = bounds
            
                self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: .immediate)
            case .ended:
                guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else {
                    return
                }
                self.panGestureArguments = nil
            
                let visibleContentOffset = listNode?.visibleContentOffset()
                let contentOffset = scrollView?.contentOffset.y ?? 0.0
            
                let translation = recognizer.translation(in: self.view).y
                var velocity = recognizer.velocity(in: self.view)
                
                if self.isExpanded {
                    if case let .known(value) = visibleContentOffset, value > 0.1 {
                        velocity = CGPoint()
                    } else if case .unknown = visibleContentOffset {
                        velocity = CGPoint()
                    } else if contentOffset > 0.1 {
                        velocity = CGPoint()
                    }
                }
            
                var bounds = self.bounds
                if self.isExpanded {
                    bounds.origin.y = -max(0.0, translation - edgeTopInset)
                } else {
                    bounds.origin.y = -translation
                }
                bounds.origin.y = min(0.0, bounds.origin.y)
            
                scrollView?.bounces = true
            
                let offset = currentTopInset + panOffset
                let topInset: CGFloat = edgeTopInset
            
                var ignoreDismiss = false
                if let shouldCancelPanGesture = self.shouldCancelPanGesture, shouldCancelPanGesture() {
                    ignoreDismiss = true
                }
            
                var dismissing = false
                if (bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0)) && !ignoreDismiss {
                    self.interactivelyDismissed?()
                    dismissing = true
                } else if self.isExpanded {
                    if velocity.y > 300.0 || offset > topInset / 2.0 {
                        self.isExpanded = false
                        if let listNode = listNode {
                            listNode.scroller.setContentOffset(CGPoint(), animated: false)
                        } else if let scrollView = scrollView {
                            scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: -scrollView.contentInset.top), animated: false)
                        }
                        
                        let distance = topInset - offset
                        let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance)
                        let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
                        
                        self.isAnimating = true
                        self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: transition, completion: completion)
                    } else {
                        self.isExpanded = true
                        
                        self.isAnimating = true
                        self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: .animated(duration: 0.3, curve: .easeInOut), completion: completion)
                    }
                } else if (velocity.y < -300.0 || offset < topInset / 2.0) {
                    if velocity.y > -2200.0 && velocity.y < -300.0, let listNode = listNode {
                        DispatchQueue.main.async {
                            listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
                        }
                    }
                                                
                    self.isExpanded = true
                   
                    let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset)
                    self.isAnimating = true
                    self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: .animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)), completion: completion)
                } else {
                    if let listNode = listNode {
                        listNode.scroller.setContentOffset(CGPoint(), animated: false)
                    } else if let scrollView = scrollView {
                        scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: -scrollView.contentInset.top), animated: false)
                        Queue.mainQueue().after(0.01, {
                            scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: -scrollView.contentInset.top), animated: false)
                        })
                    }
                    
                    self.isAnimating = true
                    self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: .animated(duration: 0.3, curve: .easeInOut), completion: completion)
                }
                
                if !dismissing {
                    var bounds = self.bounds
                    let previousBounds = bounds
                    bounds.origin.y = 0.0
                    self.bounds = bounds
                    self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
                }
            case .cancelled:
                self.panGestureArguments = nil
                
                self.isAnimating = true
                self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: .animated(duration: 0.3, curve: .easeInOut), completion: completion)
              
                var bounds = self.bounds
                let previousBounds = bounds
                bounds.origin.y = 0.0
                self.bounds = bounds
                self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
            default:
                break
        }
    }
    
    private func checkInteractiveDismissWithControllers() -> Bool {
        if let controller = self.container.controllers.last {
            if !controller.attemptNavigation({
            }) {
                return false
            }
        }
        return true
    }
    
    func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) {
        guard isExpanded != self.isExpanded else {
            return
        }
        self.isExpanded = isExpanded
        
        guard let (layout, controllers, coveredByModalTransition) = self.validLayout else {
            return
        }
        self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: transition)
    }
                
    func update(layout: ContainerViewLayout, controllers: [AttachmentContainable], coveredByModalTransition: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) {
        if self.isDismissed {
            return
        }
        self.isUpdatingState = true
        
        self.validLayout = (layout, controllers, coveredByModalTransition)
                
        self.panGestureRecognizer?.isEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0)

        let defaultTopInset = attachmentDefaultTopInset(layout: layout)
        let isLandscape = layout.orientation == .landscape
        let edgeTopInset = isLandscape ? 0.0 : defaultTopInset
        
        var effectiveExpanded = self.isExpanded
        if case .regular = layout.metrics.widthClass {
            effectiveExpanded = true
        }
        
        let topInset: CGFloat
        if let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments {
            if effectiveExpanded {
                topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset))
            } else {
                topInset = max(0.0, panInitialTopInset + min(0.0, panOffset))
            }
        } else {
            topInset = effectiveExpanded ? 0.0 : edgeTopInset
        }
        transition.updateFrame(node: self.wrappingNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: { _ in
            completion()
        })
        
        let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / defaultTopInset)
        self.updateModalProgress?(modalProgress, transition)
                
        let containerLayout: ContainerViewLayout
        let containerFrame: CGRect
        let clipFrame: CGRect
        let containerScale: CGFloat
        if case .compact = layout.metrics.widthClass {
            self.clipNode.clipsToBounds = true
            
            if isLandscape {
                self.clipNode.cornerRadius = 0.0
            } else {
                self.clipNode.cornerRadius = 10.0
            }
            
            if #available(iOS 11.0, *) {
                if layout.safeInsets.bottom.isZero {
                    self.wrappingNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
                } else {
                    self.wrappingNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
                }
            }
            
            var containerTopInset: CGFloat
            if isLandscape {
                containerTopInset = 0.0
                containerLayout = layout
                
                let unscaledFrame = CGRect(origin: CGPoint(), size: containerLayout.size)
                containerScale = 1.0
                containerFrame = unscaledFrame
                clipFrame = unscaledFrame
            } else {
                containerTopInset = 10.0
                if let statusBarHeight = layout.statusBarHeight {
                    containerTopInset += statusBarHeight
                }
                
                let effectiveStatusBarHeight: CGFloat? = nil
                
                var safeInsets = layout.safeInsets
                safeInsets.left += overflowInset
                safeInsets.right += overflowInset
                
                var intrinsicInsets = layout.intrinsicInsets
                intrinsicInsets.left += overflowInset
                intrinsicInsets.right += overflowInset
                
                var additionalInsets = layout.additionalInsets
                additionalInsets.bottom = topInset
                
                containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width + overflowInset * 2.0, height: layout.size.height - containerTopInset), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: intrinsicInsets.left, bottom: intrinsicInsets.bottom, right: intrinsicInsets.right), safeInsets: UIEdgeInsets(top: 0.0, left: safeInsets.left, bottom: safeInsets.bottom, right: safeInsets.right), additionalInsets: additionalInsets, statusBarHeight: effectiveStatusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
                let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - 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 = containerTopInset - 10.0
                let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition
                containerFrame = unscaledFrame.offsetBy(dx: -overflowInset, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
                
                clipFrame = CGRect(x: containerFrame.minX + overflowInset, y: containerFrame.minY, width: containerFrame.width - overflowInset * 2.0, height: containerFrame.height)
            }
        } else {
            containerLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: layout.inVoiceOver)
            
            let unscaledFrame = CGRect(origin: CGPoint(), size: containerLayout.size)
            containerScale = 1.0
            containerFrame = unscaledFrame
            clipFrame = unscaledFrame
        }
        transition.updateFrameAsPositionAndBounds(node: self.clipNode, frame: clipFrame)
        transition.updateFrameAsPositionAndBounds(node: self.container, frame: CGRect(origin: CGPoint(x: containerFrame.minX, y: 0.0), size: containerFrame.size))
        transition.updateTransformScale(node: self.container, scale: containerScale)
        self.container.update(layout: containerLayout, canBeClosed: true, controllers: controllers, transition: transition)
        
        self.isUpdatingState = false
    }
        
    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 {
                let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
                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)
                }
                if let (layout, _, coveredByModalTransition) = self.validLayout {
                    self.update(layout: layout, controllers: [], coveredByModalTransition: coveredByModalTransition, transition: .immediate)
                }
                completion()
                
                var bounds = self.bounds
                bounds.origin.y = 0.0
                self.bounds = bounds
                
                return transition
            }
        }
    }
    
    private func findScrollView(view: UIView?) -> (UIScrollView, ListView?)? {
        if let view = view {
            if let view = view as? UIScrollView {
                if view.description.contains("WKChildScroll") {
                    return nil
                } else {
                    return (view, nil)
                }
            }
            if let node = view.asyncdisplaykit_node as? ListView {
                return (node.scroller, node)
            }
            return findScrollView(view: view.superview)
        } else {
            return nil
        }
    }
    
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        let convertedPoint = self.view.convert(point, to: self.container.view)
        if !self.container.frame.contains(convertedPoint) {
            return false
        }
        return super.point(inside: point, with: event)
    }
}