import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ContextUI
import AnimatedStickerNode
import SwiftSignalKit
import ContextUI
import Postbox
import TelegramCore
import ReactionSelectionNode

private final class OverlayTransitionContainerNode: ViewControllerTracingNode {
    override init() {
        super.init()
    }

    deinit {
    }

    override func didLoad() {
        super.didLoad()
    }

    func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
    }

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        return nil
    }
}

private final class OverlayTransitionContainerController: ViewController, StandalonePresentableController {
    private let _ready = Promise<Bool>()
    override public var ready: Promise<Bool> {
        return self._ready
    }

    private var controllerNode: OverlayTransitionContainerNode {
        return self.displayNode as! OverlayTransitionContainerNode
    }

    private var wasDismissed: Bool = false

    init() {
        super.init(navigationBarPresentationData: nil)

        self.statusBar.statusBarStyle = .Ignore
    }

    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    deinit {
    }

    override public func loadDisplayNode() {
        self.displayNode = OverlayTransitionContainerNode()

        self.displayNodeDidLoad()

        self._ready.set(.single(true))
    }

    override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
        super.containerLayoutUpdated(layout, transition: transition)

        self.controllerNode.updateLayout(layout: layout, transition: transition)
    }

    override public func viewDidAppear(_ animated: Bool) {
        if self.ignoreAppearanceMethodInvocations() {
            return
        }
        super.viewDidAppear(animated)
    }

    override public func dismiss(completion: (() -> Void)? = nil) {
        if !self.wasDismissed {
            self.wasDismissed = true
            self.presentingViewController?.dismiss(animated: false, completion: nil)
            completion?()
        }
    }
}

public final class ChatMessageTransitionNode: ASDisplayNode {
    static let animationDuration: Double = 0.3

    static let verticalAnimationControlPoints: (Float, Float, Float, Float) = (0.19919472913616398, 0.010644531250000006, 0.27920937042459737, 0.91025390625)
    static let verticalAnimationCurve: ContainedViewLayoutTransitionCurve = .custom(verticalAnimationControlPoints.0, verticalAnimationControlPoints.1, verticalAnimationControlPoints.2, verticalAnimationControlPoints.3)
    static let horizontalAnimationCurve: ContainedViewLayoutTransitionCurve = .custom(0.23, 1.0, 0.32, 1.0)

    final class ReplyPanel {
        let titleNode: ASDisplayNode
        let textNode: ASDisplayNode
        let lineNode: ASDisplayNode
        let imageNode: ASDisplayNode
        let relativeSourceRect: CGRect
        let relativeTargetRect: CGRect

        init(titleNode: ASDisplayNode, textNode: ASDisplayNode, lineNode: ASDisplayNode, imageNode: ASDisplayNode, relativeSourceRect: CGRect, relativeTargetRect: CGRect) {
            self.titleNode = titleNode
            self.textNode = textNode
            self.lineNode = lineNode
            self.imageNode = imageNode
            self.relativeSourceRect = relativeSourceRect
            self.relativeTargetRect = relativeTargetRect
        }
    }

    final class Sticker {
        let imageNode: TransformImageNode
        let animationNode: GenericAnimatedStickerNode?
        let placeholderNode: ASDisplayNode?
        let relativeSourceRect: CGRect

        init(imageNode: TransformImageNode, animationNode: GenericAnimatedStickerNode?, placeholderNode: ASDisplayNode?, relativeSourceRect: CGRect) {
            self.imageNode = imageNode
            self.animationNode = animationNode
            self.placeholderNode = placeholderNode
            self.relativeSourceRect = relativeSourceRect
        }
    }

    enum Source {
        final class TextInput {
            let backgroundView: UIView
            let contentView: UIView
            let sourceRect: CGRect
            let scrollOffset: CGFloat

            init(backgroundView: UIView, contentView: UIView, sourceRect: CGRect, scrollOffset: CGFloat) {
                self.backgroundView = backgroundView
                self.contentView = contentView
                self.sourceRect = sourceRect
                self.scrollOffset = scrollOffset
            }
        }

        enum StickerInput {
            case inputPanel(itemNode: ChatMediaInputStickerGridItemNode)
            case mediaPanel(itemNode: HorizontalStickerGridItemNode)
            case inputPanelSearch(itemNode: StickerPaneSearchStickerItemNode)
            case emptyPanel(itemNode: ChatEmptyNodeStickerContentNode)
        }

        final class AudioMicInput {
            let micButton: ChatTextInputMediaRecordingButton

            init(micButton: ChatTextInputMediaRecordingButton) {
                self.micButton = micButton
            }
        }

        final class VideoMessage {
            let view: UIView

            init(view: UIView) {
                self.view = view
            }
        }

        final class MediaInput {
            let extractSnapshot: () -> UIView?

            init(extractSnapshot: @escaping () -> UIView?) {
                self.extractSnapshot = extractSnapshot
            }
        }

        case textInput(textInput: TextInput, replyPanel: ReplyAccessoryPanelNode?)
        case stickerMediaInput(input: StickerInput, replyPanel: ReplyAccessoryPanelNode?)
        case audioMicInput(AudioMicInput)
        case videoMessage(VideoMessage)
        case mediaInput(MediaInput)
    }
            
    final class DecorationItemNode: ASDisplayNode {
        let itemNode: ChatMessageItemView
        let contentView: UIView
        private let getContentAreaInScreenSpace: () -> CGRect
        
        private let scrollingContainer: ASDisplayNode
        private let containerNode: ASDisplayNode
        private let clippingNode: ASDisplayNode
        
        fileprivate weak var overlayController: OverlayTransitionContainerController?
        
        init(itemNode: ChatMessageItemView, contentView: UIView, getContentAreaInScreenSpace: @escaping () -> CGRect) {
            self.itemNode = itemNode
            self.contentView = contentView
            self.getContentAreaInScreenSpace = getContentAreaInScreenSpace
            
            self.clippingNode = ASDisplayNode()
            self.clippingNode.clipsToBounds = true
            
            self.scrollingContainer = ASDisplayNode()
            self.containerNode = ASDisplayNode()
            
            super.init()
            
            self.addSubnode(self.clippingNode)
            self.clippingNode.addSubnode(self.scrollingContainer)
            self.scrollingContainer.addSubnode(self.containerNode)
            self.containerNode.view.addSubview(self.contentView)
        }
        
        func updateLayout(size: CGSize) {
            self.clippingNode.frame = CGRect(origin: CGPoint(), size: size)
            
            let absoluteRect = self.itemNode.view.convert(self.itemNode.view.bounds, to: self.itemNode.supernode?.supernode?.view)
            self.containerNode.frame = absoluteRect
        }
        
        func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
            if transition.isAnimated {
                assert(true)
            }
            self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: -offset)
            transition.animateOffsetAdditive(node: self.scrollingContainer, offset: offset)
        }

        func addContentOffset(offset: CGFloat) {
            self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: offset)
        }
    }

    private final class AnimatingItemNode: ASDisplayNode {
        let itemNode: ChatMessageItemView
        private let contextSourceNode: ContextExtractedContentContainingNode
        private let source: ChatMessageTransitionNode.Source
        private let getContentAreaInScreenSpace: () -> CGRect

        private let scrollingContainer: ASDisplayNode
        private let containerNode: ASDisplayNode
        private let clippingNode: ASDisplayNode

        weak var overlayController: OverlayTransitionContainerController?

        var animationEnded: (() -> Void)?
        var updateAfterCompletion: Bool = false

        init(itemNode: ChatMessageItemView, contextSourceNode: ContextExtractedContentContainingNode, source: ChatMessageTransitionNode.Source, getContentAreaInScreenSpace: @escaping () -> CGRect) {
            self.itemNode = itemNode
            self.getContentAreaInScreenSpace = getContentAreaInScreenSpace

            self.clippingNode = ASDisplayNode()
            self.clippingNode.clipsToBounds = true

            self.scrollingContainer = ASDisplayNode()
            self.containerNode = ASDisplayNode()
            self.contextSourceNode = contextSourceNode
            self.source = source

            super.init()

            self.addSubnode(self.clippingNode)
            self.clippingNode.addSubnode(self.scrollingContainer)
            self.scrollingContainer.addSubnode(self.containerNode)
        }

        deinit {
            self.contextSourceNode.addSubnode(self.contextSourceNode.contentNode)
        }

        func updateLayout(size: CGSize) {
            self.clippingNode.frame = CGRect(origin: CGPoint(), size: size)
        }

        func beginAnimation() {
            let verticalDuration: Double = ChatMessageTransitionNode.animationDuration
            let horizontalDuration: Double = verticalDuration
            let delay: Double = 0.0

            var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace()
            updatedContentAreaInScreenSpace.size.width = updatedContentAreaInScreenSpace.origin.x + self.clippingNode.bounds.width
            updatedContentAreaInScreenSpace.origin.x = 0.0

            let clippingOffset = updatedContentAreaInScreenSpace.minY - self.clippingNode.frame.minY
            self.clippingNode.frame = CGRect(origin: CGPoint(x: 0.0, y: updatedContentAreaInScreenSpace.minY), size: CGSize(width: updatedContentAreaInScreenSpace.size.width, height: self.clippingNode.bounds.height))
            self.clippingNode.bounds = CGRect(origin: CGPoint(x: 0.0, y: clippingOffset), size: self.clippingNode.bounds.size)

            switch self.source {
            case let .textInput(initialTextInput, replyPanel):
                self.contextSourceNode.isExtractedToContextPreview = true
                self.contextSourceNode.isExtractedToContextPreviewUpdated?(true)

                var currentContentRect = self.contextSourceNode.contentRect
                let contextSourceNode = self.contextSourceNode
                self.contextSourceNode.layoutUpdated = { [weak self, weak contextSourceNode] size, _ in
                    guard let strongSelf = self, let contextSourceNode = contextSourceNode, strongSelf.contextSourceNode === contextSourceNode else {
                        return
                    }
                    let updatedContentRect = contextSourceNode.contentRect
                    let deltaY = updatedContentRect.height - currentContentRect.height
                    if !deltaY.isZero {
                        currentContentRect = updatedContentRect
                        strongSelf.addContentOffset(offset: deltaY, itemNode: nil)
                    }
                }

                self.containerNode.addSubnode(self.contextSourceNode.contentNode)

                let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view)

                let sourceRect = self.view.convert(initialTextInput.sourceRect, from: nil)
                let sourceBackgroundAbsoluteRect = initialTextInput.backgroundView.frame.offsetBy(dx: sourceRect.minX, dy: sourceRect.minY)
                let sourceAbsoluteRect = CGRect(origin: CGPoint(x: sourceBackgroundAbsoluteRect.minX, y: sourceBackgroundAbsoluteRect.maxY - self.contextSourceNode.contentRect.height), size: self.contextSourceNode.contentRect.size)

                let textInput = ChatMessageTransitionNode.Source.TextInput(backgroundView: initialTextInput.backgroundView, contentView: initialTextInput.contentView, sourceRect: sourceRect, scrollOffset: initialTextInput.scrollOffset)

                textInput.backgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: sourceAbsoluteRect.height - sourceBackgroundAbsoluteRect.height), size: textInput.backgroundView.bounds.size)
                textInput.contentView.frame = textInput.contentView.frame.offsetBy(dx: 0.0, dy: sourceAbsoluteRect.height - sourceBackgroundAbsoluteRect.height)

                var sourceReplyPanel: ReplyPanel?
                if let replyPanel = replyPanel, let replyPanelParentView = replyPanel.view.superview {
                    let replyPanelFrame = replyPanel.originalFrameBeforeDismissed ?? replyPanel.frame
                    var replySourceAbsoluteFrame = replyPanelParentView.convert(replyPanelFrame, to: self.view)

                    replySourceAbsoluteFrame.origin.x -= sourceAbsoluteRect.minX - self.contextSourceNode.contentRect.minX
                    replySourceAbsoluteFrame.origin.y -= sourceAbsoluteRect.minY - self.contextSourceNode.contentRect.minY

                    var globalTargetFrame = replySourceAbsoluteFrame.offsetBy(dx: 0.0, dy: replyPanelFrame.height)

                    globalTargetFrame.origin.x += sourceAbsoluteRect.minX - targetAbsoluteRect.minX
                    globalTargetFrame.origin.y += sourceAbsoluteRect.minY - targetAbsoluteRect.minY

                    sourceReplyPanel = ReplyPanel(titleNode: replyPanel.titleNode, textNode: replyPanel.textNode, lineNode: replyPanel.lineNode, imageNode: replyPanel.imageNode, relativeSourceRect: replySourceAbsoluteFrame, relativeTargetRect: globalTargetFrame)
                }

                self.itemNode.cancelInsertionAnimations()

                let horizontalCurve = ChatMessageTransitionNode.horizontalAnimationCurve
                let horizontalTransition: ContainedViewLayoutTransition = .animated(duration: horizontalDuration, curve: horizontalCurve)
                let verticalCurve = ChatMessageTransitionNode.verticalAnimationCurve
                let verticalTransition: ContainedViewLayoutTransition = .animated(duration: verticalDuration, curve: verticalCurve)

                let combinedTransition = CombinedTransition(horizontal: horizontalTransition, vertical: verticalTransition)

                self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY)
                self.contextSourceNode.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size)
                self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: verticalCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in
                    guard let strongSelf = self else {
                        return
                    }
                    strongSelf.endAnimation()
                })
                self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: horizontalCurve.mediaTimingFunction, additive: true)
                self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), horizontalCurve, horizontalDuration)
                self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), verticalCurve, verticalDuration)

                if let itemNode = self.itemNode as? ChatMessageBubbleItemNode {
                    itemNode.animateContentFromTextInputField(textInput: textInput, transition: combinedTransition)
                    if let sourceReplyPanel = sourceReplyPanel {
                        itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition)
                    }
                } else if let itemNode = self.itemNode as? ChatMessageAnimatedStickerItemNode {
                    itemNode.animateContentFromTextInputField(textInput: textInput, transition: combinedTransition)
                    if let sourceReplyPanel = sourceReplyPanel {
                        itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition)
                    }
                } else if let itemNode = self.itemNode as? ChatMessageStickerItemNode {
                    itemNode.animateContentFromTextInputField(textInput: textInput, transition: combinedTransition)
                    if let sourceReplyPanel = sourceReplyPanel {
                        itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition)
                    }
                }
            case let .stickerMediaInput(stickerMediaInput, replyPanel):
                self.itemNode.cancelInsertionAnimations()

                self.contextSourceNode.isExtractedToContextPreview = true
                self.contextSourceNode.isExtractedToContextPreviewUpdated?(true)

                self.containerNode.addSubnode(self.contextSourceNode.contentNode)

                let stickerSource: Sticker
                let sourceAbsoluteRect: CGRect
                switch stickerMediaInput {
                case let .inputPanel(sourceItemNode):
                    stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: sourceItemNode.placeholderNode, relativeSourceRect: sourceItemNode.imageNode.frame)
                    sourceAbsoluteRect = sourceItemNode.view.convert(stickerSource.imageNode.frame, to: self.view)
                case let .mediaPanel(sourceItemNode):
                    stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: sourceItemNode.placeholderNode, relativeSourceRect: sourceItemNode.imageNode.frame)
                    sourceAbsoluteRect = sourceItemNode.view.convert(stickerSource.imageNode.frame, to: self.view)
                case let .inputPanelSearch(sourceItemNode):
                    stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: nil, relativeSourceRect: sourceItemNode.imageNode.frame)
                    sourceAbsoluteRect = sourceItemNode.view.convert(stickerSource.imageNode.frame, to: self.view)
                case let .emptyPanel(sourceItemNode):
                    stickerSource = Sticker(imageNode: sourceItemNode.stickerNode.imageNode, animationNode: sourceItemNode.stickerNode.animationNode, placeholderNode: nil, relativeSourceRect: sourceItemNode.stickerNode.imageNode.frame)
                    sourceAbsoluteRect = sourceItemNode.stickerNode.view.convert(sourceItemNode.stickerNode.imageNode.frame, to: self.view)
                }

                let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view)

                var sourceReplyPanel: ReplyPanel?
                if let replyPanel = replyPanel, let replyPanelParentView = replyPanel.view.superview {
                    var replySourceAbsoluteFrame = replyPanelParentView.convert(replyPanel.originalFrameBeforeDismissed ?? replyPanel.frame, to: self.view)
                    replySourceAbsoluteFrame.origin.x -= sourceAbsoluteRect.midX - self.contextSourceNode.contentRect.midX
                    replySourceAbsoluteFrame.origin.y -= sourceAbsoluteRect.midY - self.contextSourceNode.contentRect.midY

                    sourceReplyPanel = ReplyPanel(titleNode: replyPanel.titleNode, textNode: replyPanel.textNode, lineNode: replyPanel.lineNode, imageNode: replyPanel.imageNode, relativeSourceRect: replySourceAbsoluteFrame, relativeTargetRect: replySourceAbsoluteFrame.offsetBy(dx: 0.0, dy: replySourceAbsoluteFrame.height))
                }

                let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve))

                if let itemNode = self.itemNode as? ChatMessageAnimatedStickerItemNode {
                    itemNode.animateContentFromStickerGridItem(stickerSource: stickerSource, transition: combinedTransition)
                    if let sourceAnimationNode = stickerSource.animationNode {
                        itemNode.animationNode?.setFrameIndex(sourceAnimationNode.currentFrameIndex)
                    }
                    if let sourceReplyPanel = sourceReplyPanel {
                        itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition)
                    }
                } else if let itemNode = self.itemNode as? ChatMessageStickerItemNode {
                    itemNode.animateContentFromStickerGridItem(stickerSource: stickerSource, transition: combinedTransition)
                    if let sourceReplyPanel = sourceReplyPanel {
                        itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition)
                    }
                }

                self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY)
                self.contextSourceNode.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size)
                self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in
                    guard let strongSelf = self else {
                        return
                    }
                    strongSelf.endAnimation()
                })
                self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), ChatMessageTransitionNode.horizontalAnimationCurve, horizontalDuration)
                self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), ChatMessageTransitionNode.verticalAnimationCurve, verticalDuration)
                self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true)

                switch stickerMediaInput {
                case .inputPanel:
                    break
                case let .mediaPanel(sourceItemNode):
                    sourceItemNode.isHidden = true
                case let .inputPanelSearch(sourceItemNode):
                    sourceItemNode.isHidden = true
                case let .emptyPanel(sourceItemNode):
                    sourceItemNode.isHidden = true
                }
            case let .audioMicInput(audioMicInput):
                if let (container, localRect) = audioMicInput.micButton.contentContainer {
                    let snapshotView = container.snapshotView(afterScreenUpdates: false)
                    if let snapshotView = snapshotView {
                        let sourceAbsoluteRect = container.convert(localRect, to: self.view)
                        snapshotView.frame = sourceAbsoluteRect

                        container.isHidden = true

                        let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve))

                        if let itemNode = self.itemNode as? ChatMessageBubbleItemNode {
                            if let contextContainer = itemNode.animateFromMicInput(micInputNode: snapshotView, transition: combinedTransition) {
                                self.containerNode.addSubnode(contextContainer.contentNode)

                                let targetAbsoluteRect = contextContainer.view.convert(contextContainer.contentRect, to: self.view)

                                self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -contextContainer.contentRect.minX, dy: -contextContainer.contentRect.minY)
                                contextContainer.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size)
                                self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self, weak contextContainer, weak container] _ in
                                    guard let strongSelf = self else {
                                        return
                                    }
                                    if let contextContainer = contextContainer {
                                        contextContainer.isExtractedToContextPreview = false
                                        contextContainer.isExtractedToContextPreviewUpdated?(false)
                                        contextContainer.addSubnode(contextContainer.contentNode)
                                    }

                                    container?.isHidden = false

                                    strongSelf.endAnimation()
                                })

                                self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true)
                            }
                        }
                    }
                }
            case let .videoMessage(videoMessage):
                let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve))

                if let itemNode = self.itemNode as? ChatMessageInstantVideoItemNode {
                    itemNode.cancelInsertionAnimations()

                    self.contextSourceNode.isExtractedToContextPreview = true
                    self.contextSourceNode.isExtractedToContextPreviewUpdated?(true)

                    self.containerNode.addSubnode(self.contextSourceNode.contentNode)

                    let sourceAbsoluteRect = videoMessage.view.frame
                    let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view)

                    videoMessage.view.frame = videoMessage.view.frame.offsetBy(dx: targetAbsoluteRect.midX - sourceAbsoluteRect.midX, dy: targetAbsoluteRect.midY - sourceAbsoluteRect.midY)

                    self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY)
                    self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true, force: true)

                    self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.verticalAnimationCurve.mediaTimingFunction, additive: true, completion: { [weak self] _ in
                        guard let strongSelf = self else {
                            return
                        }

                        strongSelf.endAnimation()
                    })

                    itemNode.animateFromSnapshot(snapshotView: videoMessage.view, transition: combinedTransition)
                }
            case let .mediaInput(mediaInput):
                if let snapshotView = mediaInput.extractSnapshot() {
                    if let itemNode = self.itemNode as? ChatMessageBubbleItemNode {
                        itemNode.cancelInsertionAnimations()

                        self.contextSourceNode.isExtractedToContextPreview = true
                        self.contextSourceNode.isExtractedToContextPreviewUpdated?(true)

                        self.containerNode.addSubnode(self.contextSourceNode.contentNode)

                        let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view)
                        let sourceBackgroundAbsoluteRect = snapshotView.frame
                        let sourceAbsoluteRect = CGRect(origin: CGPoint(x: sourceBackgroundAbsoluteRect.midX - self.contextSourceNode.contentRect.size.width / 2.0, y: sourceBackgroundAbsoluteRect.midY - self.contextSourceNode.contentRect.size.height / 2.0), size: self.contextSourceNode.contentRect.size)

                        let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve))

                        if let itemNode = self.itemNode as? ChatMessageBubbleItemNode {
                            itemNode.animateContentFromMediaInput(snapshotView: snapshotView, transition: combinedTransition)
                        }

                        self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY)

                        snapshotView.center = targetAbsoluteRect.center.offsetBy(dx: -self.containerNode.frame.minX, dy: -self.containerNode.frame.minY)
                        self.containerNode.view.addSubview(snapshotView)

                        self.contextSourceNode.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size)

                        self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true, force: true)
                        self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in
                            guard let strongSelf = self else {
                                return
                            }
                            strongSelf.endAnimation()
                        })

                        combinedTransition.horizontal.animateTransformScale(node: self.contextSourceNode.contentNode, from: CGPoint(x: sourceBackgroundAbsoluteRect.width / targetAbsoluteRect.width, y: sourceBackgroundAbsoluteRect.height / targetAbsoluteRect.height))

                        combinedTransition.horizontal.updateTransformScale(layer: snapshotView.layer, scale: CGPoint(x: 1.0 / (sourceBackgroundAbsoluteRect.width / targetAbsoluteRect.width), y: 1.0 / (sourceBackgroundAbsoluteRect.height / targetAbsoluteRect.height)))

                        snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak snapshotView] _ in
                            snapshotView?.removeFromSuperview()
                        })

                        self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), ChatMessageTransitionNode.horizontalAnimationCurve, horizontalDuration)
                        self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), ChatMessageTransitionNode.verticalAnimationCurve, verticalDuration)
                    }
                }
            }
        }

        private func endAnimation() {
            self.contextSourceNode.isExtractedToContextPreview = false
            self.contextSourceNode.isExtractedToContextPreviewUpdated?(false)
            
            self.animationEnded?()
        }

        func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?) {
            var applyOffset = false
            if let itemNode = itemNode {
                if itemNode === self.itemNode {
                    applyOffset = true
                }
            } else {
                applyOffset = true
            }
            if applyOffset {
                if transition.isAnimated {
                    assert(true)
                }
                self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: -offset)
                transition.animateOffsetAdditive(node: self.scrollingContainer, offset: offset)
            }
        }

        func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) {
            var applyOffset = false
            if let itemNode = itemNode {
                if itemNode === self.itemNode {
                    applyOffset = true
                }
            } else {
                applyOffset = true
            }
            if applyOffset {
                self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: offset)
            }
        }
    }
    
    private final class MessageReactionContext {
        private(set) weak var itemNode: ListViewItemNode?
        private(set) weak var contextController: ContextController?
        private(set) weak var standaloneReactionAnimation: StandaloneReactionAnimation?
        
        var isEmpty: Bool {
            return self.contextController == nil && self.standaloneReactionAnimation == nil
        }
        
        init(itemNode: ListViewItemNode, contextController: ContextController?, standaloneReactionAnimation: StandaloneReactionAnimation?) {
            self.itemNode = itemNode
            self.contextController = contextController
            self.standaloneReactionAnimation = standaloneReactionAnimation
        }
        
        func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?) {
            guard let currentItemNode = self.itemNode else {
                return
            }
            if itemNode == nil || itemNode === currentItemNode {
                if let contextController = self.contextController {
                    contextController.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition)
                }
                if let standaloneReactionAnimation = self.standaloneReactionAnimation {
                    standaloneReactionAnimation.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition)
                }
            }
        }

        func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) {
        }
        
        func dismiss() {
            if let contextController = self.contextController {
                contextController.cancelReactionAnimation()
                contextController.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak contextController] _ in
                    contextController?.dismissNow()
                })
            }
            if let standaloneReactionAnimation = self.standaloneReactionAnimation {
                standaloneReactionAnimation.cancel()
                standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in
                    standaloneReactionAnimation?.removeFromSupernode()
                })
            }
        }
    }

    private let listNode: ChatHistoryListNode
    private let getContentAreaInScreenSpace: () -> CGRect
    private let onTransitionEvent: (ContainedViewLayoutTransition) -> Void

    private var currentPendingItem: (Int64, Source, () -> Void)?

    private var animatingItemNodes: [AnimatingItemNode] = []
    private var decorationItemNodes: [DecorationItemNode] = []
    private var messageReactionContexts: [MessageReactionContext] = []

    var hasScheduledTransitions: Bool {
        return self.currentPendingItem != nil
    }

    var hasOngoingTransitions: Bool {
        return !self.animatingItemNodes.isEmpty
    }

    init(listNode: ChatHistoryListNode, getContentAreaInScreenSpace: @escaping () -> CGRect, onTransitionEvent: @escaping (ContainedViewLayoutTransition) -> Void) {
        self.listNode = listNode
        self.getContentAreaInScreenSpace = getContentAreaInScreenSpace
        self.onTransitionEvent = onTransitionEvent

        super.init()

        self.listNode.animationCorrelationMessageFound = { [weak self] itemNode, correlationId in
            guard let strongSelf = self, let (currentId, currentSource, initiated) = strongSelf.currentPendingItem else {
                return
            }
            if currentId == correlationId {
                strongSelf.currentPendingItem = nil
                strongSelf.beginAnimation(itemNode: itemNode, source: currentSource)
                initiated()
            }
        }
    }

    func add(correlationId: Int64, source: Source, initiated: @escaping () -> Void) {
        self.currentPendingItem = (correlationId, source, initiated)
        self.listNode.setCurrentSendAnimationCorrelationId(correlationId)
    }
    
    func add(decorationView: UIView, itemNode: ChatMessageItemView) -> DecorationItemNode {
        let decorationItemNode = DecorationItemNode(itemNode: itemNode, contentView: decorationView, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace)
        decorationItemNode.updateLayout(size: self.bounds.size)
       
        self.decorationItemNodes.append(decorationItemNode)
        self.addSubnode(decorationItemNode)
        
//        let overlayController = OverlayTransitionContainerController()
//        overlayController.displayNode.isUserInteractionEnabled = false
//        overlayController.displayNode.addSubnode(decorationItemNode)
//        decorationItemNode.overlayController = overlayController
//        itemNode.item?.context.sharedContext.mainWindow?.presentInGlobalOverlay(overlayController)
                
        return decorationItemNode
    }
    
    func remove(decorationNode: DecorationItemNode) {
        self.decorationItemNodes.removeAll(where: { $0 === decorationNode })
        decorationNode.removeFromSupernode()
        decorationNode.overlayController?.dismiss()
    }

    private func beginAnimation(itemNode: ChatMessageItemView, source: Source) {
        var contextSourceNode: ContextExtractedContentContainingNode?
        if let itemNode = itemNode as? ChatMessageBubbleItemNode {
            contextSourceNode = itemNode.mainContextSourceNode
        } else if let itemNode = itemNode as? ChatMessageStickerItemNode {
            contextSourceNode = itemNode.contextSourceNode
        } else if let itemNode = itemNode as? ChatMessageAnimatedStickerItemNode {
            contextSourceNode = itemNode.contextSourceNode
        } else if let itemNode = itemNode as? ChatMessageInstantVideoItemNode {
            contextSourceNode = itemNode.contextSourceNode
        }

        if let contextSourceNode = contextSourceNode {
            let animatingItemNode = AnimatingItemNode(itemNode: itemNode, contextSourceNode: contextSourceNode, source: source, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace)
            animatingItemNode.updateLayout(size: self.bounds.size)
            
            self.animatingItemNodes.append(animatingItemNode)
            switch source {
            case .audioMicInput, .videoMessage, .mediaInput:
                let overlayController = OverlayTransitionContainerController()
                overlayController.displayNode.addSubnode(animatingItemNode)
                animatingItemNode.overlayController = overlayController
                itemNode.item?.context.sharedContext.mainWindow?.presentInGlobalOverlay(overlayController)
            default:
                self.addSubnode(animatingItemNode)
            }

            animatingItemNode.animationEnded = { [weak self, weak animatingItemNode] in
                guard let strongSelf = self, let animatingItemNode = animatingItemNode else {
                    return
                }
                animatingItemNode.removeFromSupernode()
                animatingItemNode.overlayController?.dismiss()
                if let index = strongSelf.animatingItemNodes.firstIndex(where: { $0 === animatingItemNode }) {
                    strongSelf.animatingItemNodes.remove(at: index)
                }

                if animatingItemNode.updateAfterCompletion, let item = animatingItemNode.itemNode.item {
                    for (message, _) in item.content {
                        strongSelf.listNode.requestMessageUpdate(stableId: message.stableId)
                        break
                    }
                }
            }

            animatingItemNode.frame = self.bounds
            animatingItemNode.beginAnimation()

            self.onTransitionEvent(.animated(duration: ChatMessageTransitionNode.animationDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve))
        }
    }

    override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        return nil
    }
    
    private func removeEmptyMessageReactionContexts() {
        for i in (0 ..< self.messageReactionContexts.count).reversed() {
            if self.messageReactionContexts[i].isEmpty {
                self.messageReactionContexts.remove(at: i)
            }
        }
    }
    
    func dismissMessageReactionContexts(itemNode: ListViewItemNode? = nil) {
        for i in (0 ..< self.messageReactionContexts.count).reversed() {
            let messageReactionContext = self.messageReactionContexts[i]
            if itemNode == nil || messageReactionContext.itemNode === itemNode {
                self.messageReactionContexts.remove(at: i)
                messageReactionContext.dismiss()
            }
        }
    }
    
    func addMessageContextController(messageId: MessageId, contextController: ContextController) {
        self.addMessageReactionContextContext(messageId: messageId, contextController: contextController, standaloneReactionAnimation: nil)
    }
    
    func addMessageStandaloneReactionAnimation(messageId: MessageId, standaloneReactionAnimation: StandaloneReactionAnimation) {
        self.addMessageReactionContextContext(messageId: messageId, contextController: nil, standaloneReactionAnimation: standaloneReactionAnimation)
    }
    
    private func addMessageReactionContextContext(messageId: MessageId, contextController: ContextController?, standaloneReactionAnimation: StandaloneReactionAnimation?) {
        self.removeEmptyMessageReactionContexts()
        
        var messageItemNode: ListViewItemNode?
        self.listNode.forEachItemNode { itemNode in
            if let itemNode = itemNode as? ChatMessageItemView {
                if let item = itemNode.item {
                    for (message, _) in item.content {
                        if message.id == messageId {
                            messageItemNode = itemNode
                            break
                        }
                    }
                }
            }
        }
        
        if let messageItemNode = messageItemNode {
            for i in 0 ..< self.messageReactionContexts.count {
                if self.messageReactionContexts[i].itemNode === messageItemNode {
                    self.messageReactionContexts[i].dismiss()
                    self.messageReactionContexts.remove(at: i)
                    break
                }
            }
            self.messageReactionContexts.append(MessageReactionContext(itemNode: messageItemNode, contextController: contextController, standaloneReactionAnimation: standaloneReactionAnimation))
        }
    }

    func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?) {
        for animatingItemNode in self.animatingItemNodes {
            animatingItemNode.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode)
        }
        if itemNode == nil {
            for decorationItemNode in self.decorationItemNodes {
                decorationItemNode.addExternalOffset(offset: offset, transition: transition)
            }
        }
        for messageReactionContext in self.messageReactionContexts {
            messageReactionContext.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode)
        }
    }

    func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) {
        for animatingItemNode in self.animatingItemNodes {
            animatingItemNode.addContentOffset(offset: offset, itemNode: itemNode)
        }
        if itemNode == nil {
            for decorationItemNode in self.decorationItemNodes {
                decorationItemNode.addContentOffset(offset: offset)
            }
        }
        for messageReactionContext in self.messageReactionContexts {
            messageReactionContext.addContentOffset(offset: offset, itemNode: itemNode)
        }
    }

    func isAnimatingMessage(stableId: UInt32) -> Bool {
        for itemNode in self.animatingItemNodes {
            if let item = itemNode.itemNode.item {
                for (message, _) in item.content {
                    if message.stableId == stableId {
                        return true
                    }
                }
            }
        }
        return false
    }

    func scheduleUpdateMessageAfterAnimationCompleted(stableId: UInt32) {
        for itemNode in self.animatingItemNodes {
            if let item = itemNode.itemNode.item {
                for (message, _) in item.content {
                    if message.stableId == stableId {
                        itemNode.updateAfterCompletion = true
                    }
                }
            }
        }
    }

    func hasScheduledUpdateMessageAfterAnimationCompleted(stableId: UInt32) -> Bool {
        for itemNode in self.animatingItemNodes {
            if let item = itemNode.itemNode.item {
                for (message, _) in item.content {
                    if message.stableId == stableId {
                        return itemNode.updateAfterCompletion
                    }
                }
            }
        }
        return false
    }
}