import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import Postbox
import SyncCore
import TelegramCore
import AccountContext
import ContextUI

protocol ChatListSearchPaneNode: ASDisplayNode {
    var isReady: Signal<Bool, NoError> { get }
    
    func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition)
    func scrollToTop() -> Bool
    func cancelPreviewGestures()
    func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
    func addToTransitionSurface(view: UIView)
    func updateHiddenMedia()
    func updateSelectedMessages(animated: Bool)
    func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)?
    var searchCurrentMessages: [Message]? { get }
}

final class ChatListSearchPaneWrapper {
    let key: ChatListSearchPaneKey
    let node: ChatListSearchPaneNode
    var isAnimatingOut: Bool = false
    private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, PresentationData)?
    
    init(key: ChatListSearchPaneKey, node: ChatListSearchPaneNode) {
        self.key = key
        self.node = node
    }
    
    func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
        if let (currentSize, currentSideInset, currentBottomInset, _, currentPresentationData) = self.appliedParams {
            if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset && currentPresentationData === presentationData {
                return
            }
        }
        self.appliedParams = (size, sideInset, bottomInset, visibleHeight, presentationData)
        self.node.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: synchronous, transition: transition)
    }
}

enum ChatListSearchPaneKey {
    case chats
    case media
    case links
    case files
    case music
    case voice
}

let defaultAvailableSearchPanes: [ChatListSearchPaneKey] = [.chats, .media, .links, .files, .music, .voice]

struct ChatListSearchPaneSpecifier: Equatable {
    var key: ChatListSearchPaneKey
    var title: String
}

private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect {
    return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t)))
}

private final class ChatListSearchPendingPane {
    let pane: ChatListSearchPaneWrapper
    private var disposable: Disposable?
    var isReady: Bool = false
    
    init(
        context: AccountContext,
        interaction: ChatListSearchInteraction,
        navigationController: NavigationController?,
        peersFilter: ChatListNodePeersFilter,
        groupId: PeerGroupId,
        searchQuery: Signal<String?, NoError>,
        searchOptions: Signal<ChatListSearchOptions?, NoError>,
        key: ChatListSearchPaneKey,
        hasBecomeReady: @escaping (ChatListSearchPaneKey) -> Void
    ) {
        let paneNode = ChatListSearchListPaneNode(context: context, interaction: interaction, key: key, peersFilter: key == .chats ? peersFilter : [], groupId: groupId, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController)
        
        self.pane = ChatListSearchPaneWrapper(key: key, node: paneNode)
        self.disposable = (paneNode.isReady
        |> take(1)
        |> deliverOnMainQueue).start(next: { [weak self] _ in
            self?.isReady = true
            hasBecomeReady(key)
        })
    }
    
    deinit {
        self.disposable?.dispose()
    }
}

final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
    private let context: AccountContext
    private let peersFilter: ChatListNodePeersFilter
    private let groupId: PeerGroupId
    private let searchQuery: Signal<String?, NoError>
    private let searchOptions: Signal<ChatListSearchOptions?, NoError>
    private let navigationController: NavigationController?
    var interaction: ChatListSearchInteraction?
        
    let isReady = Promise<Bool>()
    var didSetIsReady = false
    
    var isAdjacentLoadingEnabled = false
    
    private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData, [ChatListSearchPaneKey])?
    
    private(set) var currentPaneKey: ChatListSearchPaneKey?
    var pendingSwitchToPaneKey: ChatListSearchPaneKey?
    
    var currentPane: ChatListSearchPaneWrapper? {
        if let currentPaneKey = self.currentPaneKey {
            return self.currentPanes[currentPaneKey]
        } else {
            return nil
        }
    }
    
    var currentPanes: [ChatListSearchPaneKey: ChatListSearchPaneWrapper] = [:]
    private var pendingPanes: [ChatListSearchPaneKey: ChatListSearchPendingPane] = [:]
    
    private var transitionFraction: CGFloat = 0.0
            
    var currentPaneUpdated: ((ChatListSearchPaneKey?, CGFloat, ContainedViewLayoutTransition) -> Void)?
    var requestExpandTabs: (() -> Bool)?
    
    private var currentAvailablePanes: [ChatListSearchPaneKey]?
    
    init(context: AccountContext, peersFilter: ChatListNodePeersFilter, groupId: PeerGroupId, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?) {
        self.context = context
        self.peersFilter = peersFilter
        self.groupId = groupId
        self.searchQuery = searchQuery
        self.searchOptions = searchOptions
        self.navigationController = navigationController
        
        super.init()
    }
    
    func requestSelectPane(_ key: ChatListSearchPaneKey) {
        if self.currentPaneKey == key {
            if let requestExpandTabs = self.requestExpandTabs, requestExpandTabs() {
            } else {
                let _ = self.currentPane?.node.scrollToTop()
            }
            return
        }
        self.isAdjacentLoadingEnabled = true
        if self.currentPanes[key] != nil {
            self.currentPaneKey = key

            if let (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes) = self.currentParams {
                self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, availablePanes: availablePanes, transition: .animated(duration: 0.4, curve: .spring))
            }
        } else if self.pendingSwitchToPaneKey != key {
            self.pendingSwitchToPaneKey = key

            if let (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes) = self.currentParams {
                self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, availablePanes: availablePanes, transition: .animated(duration: 0.4, curve: .spring))
            }
        }
    }
    
    override func didLoad() {
        super.didLoad()
        
        let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in
            guard let strongSelf = self, let (_, _, _, _, _, availablePanes) = strongSelf.currentParams, let currentPaneKey = strongSelf.currentPaneKey, let index = availablePanes.firstIndex(of: currentPaneKey) else {
                return []
            }
            if index == 0 {
                return .left
            }
            return [.left, .right]
        })
        panRecognizer.delegate = self
        panRecognizer.delaysTouchesBegan = false
        panRecognizer.cancelsTouchesInView = true
        self.view.addGestureRecognizer(panRecognizer)
    }
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return false
    }
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
            return false
        }
        if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
            return true
        }
        return false
    }
    
    @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
        switch recognizer.state {
        case .began:
            func cancelContextGestures(view: UIView) {
                if let gestureRecognizers = view.gestureRecognizers {
                    for gesture in gestureRecognizers {
                        if let gesture = gesture as? ContextGesture {
                            gesture.cancel()
                        }
                    }
                }
                for subview in view.subviews {
                    cancelContextGestures(view: subview)
                }
            }
            
            cancelContextGestures(view: self.view)
        case .changed:
            if let (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes) = self.currentParams, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey) {
                self.isAdjacentLoadingEnabled = true
                let translation = recognizer.translation(in: self.view)
                var transitionFraction = translation.x / size.width
                if currentIndex <= 0 {
                    transitionFraction = min(0.0, transitionFraction)
                }
                if currentIndex >= availablePanes.count - 1 {
                    transitionFraction = max(0.0, transitionFraction)
                }
                self.transitionFraction = transitionFraction
                self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, availablePanes: availablePanes, transition: .immediate)
            }
        case .cancelled, .ended:
            if let (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes) = self.currentParams, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey) {
                let translation = recognizer.translation(in: self.view)
                let velocity = recognizer.velocity(in: self.view)
                var directionIsToRight: Bool?
                if abs(velocity.x) > 10.0 {
                    directionIsToRight = velocity.x < 0.0
                } else {
                    if abs(translation.x) > size.width / 2.0 {
                        directionIsToRight = translation.x > size.width / 2.0
                    }
                }
                
                if let directionIsToRight = directionIsToRight {
                    var updatedIndex = currentIndex
                    if directionIsToRight {
                        updatedIndex = min(updatedIndex + 1, availablePanes.count - 1)
                    } else {
                        updatedIndex = max(updatedIndex - 1, 0)
                    }
                    let switchToKey = availablePanes[updatedIndex]
                    if switchToKey != self.currentPaneKey && self.currentPanes[switchToKey] != nil{
                        self.currentPaneKey = switchToKey
                    }
                }
                self.transitionFraction = 0.0
                self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, availablePanes: availablePanes, transition: .animated(duration: 0.35, curve: .spring))
            }
        default:
            break
        }
    }
    
    func scrollToTop() -> Bool {
        if let currentPane = self.currentPane {
            return currentPane.node.scrollToTop()
        } else {
            return false
        }
    }

    func updateHiddenMedia() {
        self.currentPane?.node.updateHiddenMedia()
    }
    
    func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
        return self.currentPane?.node.transitionNodeForGallery(messageId: messageId, media: media)
    }
    
    func updateSelectedMessageIds(_ selectedMessageIds: Set<MessageId>?, animated: Bool) {
        for (_, pane) in self.currentPanes {
            pane.node.updateSelectedMessages(animated: animated)
        }
        for (_, pane) in self.pendingPanes {
            pane.pane.node.updateSelectedMessages(animated: animated)
        }
    }
    
    func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData, availablePanes: [ChatListSearchPaneKey], transition: ContainedViewLayoutTransition) {
        let previousAvailablePanes = self.currentAvailablePanes ?? []
        self.currentAvailablePanes = availablePanes
                
        if let currentPaneKey = self.currentPaneKey, !availablePanes.contains(currentPaneKey) {
            var nextCandidatePaneKey: ChatListSearchPaneKey?
            if let index = previousAvailablePanes.firstIndex(of: currentPaneKey), index != 0 {
                for i in (0 ... index - 1).reversed() {
                    if availablePanes.contains(previousAvailablePanes[i]) {
                        nextCandidatePaneKey = previousAvailablePanes[i]
                    }
                }
            }
            if nextCandidatePaneKey == nil {
                nextCandidatePaneKey = availablePanes.first
            }
            
            if let nextCandidatePaneKey = nextCandidatePaneKey {
                self.pendingSwitchToPaneKey = nextCandidatePaneKey
            } else {
                self.currentPaneKey = nil
                self.pendingSwitchToPaneKey = nil
            }
        } else if self.currentPaneKey == nil && self.pendingSwitchToPaneKey == nil {
            self.pendingSwitchToPaneKey = availablePanes.first
        }
        
        let currentIndex: Int?
        if let currentPaneKey = self.currentPaneKey {
            currentIndex = availablePanes.firstIndex(of: currentPaneKey)
        } else {
            currentIndex = nil
        }
        
        self.currentParams = (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes)
                
        self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor
        
        let paneFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))
        
        var visiblePaneIndices: [Int] = []
        var requiredPendingKeys: [ChatListSearchPaneKey] = []
        if let currentIndex = currentIndex {
            if currentIndex != 0 && self.isAdjacentLoadingEnabled {
                visiblePaneIndices.append(currentIndex - 1)
            }
            visiblePaneIndices.append(currentIndex)
            if currentIndex != availablePanes.count - 1 && self.isAdjacentLoadingEnabled {
                visiblePaneIndices.append(currentIndex + 1)
            }
        
            for index in visiblePaneIndices {
                let key = availablePanes[index]
                if self.currentPanes[key] == nil && self.pendingPanes[key] == nil {
                    requiredPendingKeys.append(key)
                }
            }
        }
        if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey {
            if self.currentPanes[pendingSwitchToPaneKey] == nil && self.pendingPanes[pendingSwitchToPaneKey] == nil {
                if !requiredPendingKeys.contains(pendingSwitchToPaneKey) {
                    requiredPendingKeys.append(pendingSwitchToPaneKey)
                }
            }
        }
        
        for key in requiredPendingKeys {
            if self.pendingPanes[key] == nil {
                var leftScope = false
                let pane = ChatListSearchPendingPane(
                    context: self.context,
                    interaction: self.interaction!,
                    navigationController: self.navigationController,
                    peersFilter: self.peersFilter,
                    groupId: self.groupId,
                    searchQuery: self.searchQuery,
                    searchOptions: self.searchOptions,
                    key: key,
                    hasBecomeReady: { [weak self] key in
                        let apply: () -> Void = {
                            guard let strongSelf = self else {
                                return
                            }
                            if let (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes) = strongSelf.currentParams {
                                var transition: ContainedViewLayoutTransition = .immediate
                                if strongSelf.pendingSwitchToPaneKey == key && strongSelf.currentPaneKey != nil {
                                    transition = .animated(duration: 0.4, curve: .spring)
                                }
                                strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, availablePanes: availablePanes, transition: transition)
                            }
                        }
                        if leftScope {
                            apply()
                        }
                    }
                )
                self.pendingPanes[key] = pane
                pane.pane.node.frame = paneFrame
                pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: true, transition: .immediate)
                leftScope = true
            }
        }
                
        for (key, pane) in self.pendingPanes {
            pane.pane.node.frame = paneFrame
            pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate)
            
            if pane.isReady {
                self.pendingPanes.removeValue(forKey: key)
                self.currentPanes[key] = pane.pane
            }
        }
        
        var paneDefaultTransition = transition
        var previousPaneKey: ChatListSearchPaneKey?
        var paneSwitchAnimationOffset: CGFloat = 0.0
        
        var updatedCurrentIndex = currentIndex
        if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey, let _ = self.currentPanes[pendingSwitchToPaneKey] {
            self.pendingSwitchToPaneKey = nil
            previousPaneKey = self.currentPaneKey
            self.currentPaneKey = pendingSwitchToPaneKey
            updatedCurrentIndex = availablePanes.firstIndex(of: pendingSwitchToPaneKey)
            if let previousPaneKey = previousPaneKey, let previousIndex = availablePanes.firstIndex(of: previousPaneKey), let updatedCurrentIndex = updatedCurrentIndex {
                if updatedCurrentIndex < previousIndex {
                    paneSwitchAnimationOffset = -size.width
                } else {
                    paneSwitchAnimationOffset = size.width
                }
            }
            
            paneDefaultTransition = .immediate
        }
        
        for (key, pane) in self.currentPanes {
            if let index = availablePanes.firstIndex(of: key), let updatedCurrentIndex = updatedCurrentIndex {
                var paneWasAdded = false
                if pane.node.supernode == nil {
                    self.addSubnode(pane.node)
                    paneWasAdded = true
                }
                let indexOffset = CGFloat(index - updatedCurrentIndex)
                
                let paneTransition: ContainedViewLayoutTransition = paneWasAdded ? .immediate : paneDefaultTransition
                let adjustedFrame = paneFrame.offsetBy(dx: size.width * self.transitionFraction + indexOffset * size.width, dy: 0.0)
                
                let paneCompletion: () -> Void = { [weak self, weak pane] in
                    guard let strongSelf = self, let pane = pane else {
                        return
                    }
                    pane.isAnimatingOut = false
                    if let _ = strongSelf.currentParams {
                        if let currentPaneKey = strongSelf.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey), let paneIndex = availablePanes.firstIndex(of: key), paneIndex == 0 || abs(paneIndex - currentIndex) <= 1 {
                        } else {
                            if let pane = strongSelf.currentPanes.removeValue(forKey: key) {
                                pane.node.removeFromSupernode()
                            }
                        }
                    }
                }
                if let previousPaneKey = previousPaneKey, key == previousPaneKey {
                    pane.node.frame = adjustedFrame
                    let isAnimatingOut = pane.isAnimatingOut
                    pane.isAnimatingOut = true
                    transition.animateFrame(node: pane.node, from: paneFrame, to: paneFrame.offsetBy(dx: -paneSwitchAnimationOffset, dy: 0.0), completion: isAnimatingOut ? nil : { _ in
                        paneCompletion()
                    })
                } else if let _ = previousPaneKey, key == self.currentPaneKey {
                    pane.node.frame = adjustedFrame
                    let isAnimatingOut = pane.isAnimatingOut
                    pane.isAnimatingOut = true
                    transition.animatePositionAdditive(node: pane.node, offset: CGPoint(x: paneSwitchAnimationOffset, y: 0.0), completion: isAnimatingOut ? nil : {
                        paneCompletion()
                    })
                } else {
                    let isAnimatingOut = pane.isAnimatingOut
                    pane.isAnimatingOut = true
                    paneTransition.updateFrame(node: pane.node, frame: adjustedFrame, completion: isAnimatingOut ? nil :  { _ in
                        paneCompletion()
                    })
                }
                pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition)
            }
        }
        
        for (_, pane) in self.pendingPanes {
            let paneTransition: ContainedViewLayoutTransition = .immediate
            paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame)
            pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: true, transition: paneTransition)
        }
        if !self.didSetIsReady {
            if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] {
                self.didSetIsReady = true
                self.isReady.set(currentPane.node.isReady)
            } else if self.pendingSwitchToPaneKey == nil {
                self.didSetIsReady = true
                self.isReady.set(.single(true))
            }
        }
        
        self.currentPaneUpdated?(self.currentPaneKey, self.transitionFraction, transition)
    }
    
    func allCurrentMessages() -> [MessageId: Message] {
        var allMessages: [MessageId: Message] = [:]
        for (_, pane) in self.currentPanes {
            if let messages = pane.node.searchCurrentMessages {
                for message in messages {
                    allMessages[message.id] = message
                }
            }
        }
        return allMessages
    }
}