import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ContextUI
import AnimatedStickerNode
import SwiftSignalKit
import ContextUI
import Postbox
import TelegramCore
import ReactionSelectionNode
import ChatControllerInteraction
import FeaturedStickersScreen
import ChatTextInputMediaRecordingButton
import ReplyAccessoryPanelNode
import ChatMessageStickerItemNode
import ChatMessageInstantVideoItemNode
import ChatMessageAnimatedStickerItemNode
import ChatMessageTransitionNode
import ChatMessageBubbleItemNode
import ChatEmptyNode
import ChatMediaInputStickerGridItem
import AccountContext

private func convertAnimatingSourceRect(_ rect: CGRect, fromView: UIView, toView: UIView?) -> CGRect {
    if let presentationLayer = fromView.layer.presentation() {
        return presentationLayer.convert(rect, to: toView?.layer)
    } else {
        return fromView.layer.convert(rect, to: toView?.layer)
    }
}

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 ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTransitionNode, ChatMessageTransitionProtocol {
    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: AnimatedStickerNode?
        let placeholderNode: ASDisplayNode?
        let imageLayer: CALayer?
        let relativeSourceRect: CGRect
        
        var sourceFrame: CGRect {
            if let imageNode = self.imageNode {
                return imageNode.frame
            } else if let imageLayer = self.imageLayer {
                return imageLayer.bounds
            } else {
                return CGRect(origin: CGPoint(), size: relativeSourceRect.size)
            }
        }
        
        var sourceLayer: CALayer? {
            if let imageNode = self.imageNode {
                return imageNode.layer
            } else if let imageLayer = self.imageLayer {
                return imageLayer
            } else {
                return nil
            }
        }

        init(imageNode: TransformImageNode?, animationNode: AnimatedStickerNode?, placeholderNode: ASDisplayNode?, imageLayer: CALayer?, relativeSourceRect: CGRect) {
            self.imageNode = imageNode
            self.animationNode = animationNode
            self.placeholderNode = placeholderNode
            self.imageLayer = imageLayer
            self.relativeSourceRect = relativeSourceRect
        }
        
        func snapshotContentTree() -> UIView? {
            if let animationNode = self.animationNode {
                return animationNode.view.snapshotContentTree()
            } else if let imageNode = self.imageNode {
                return imageNode.view.snapshotContentTree()
            } else if let sourceLayer = self.imageLayer {
                return sourceLayer.snapshotContentTreeAsView()
            } else {
                return nil
            }
        }
    }

    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 universal(sourceContainerView: UIView, sourceRect: CGRect, sourceLayer: CALayer)
            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
            }
        }
        
        final class GroupedMediaInput {
            let extractSnapshots: () -> [UIView]

            init(extractSnapshots: @escaping () -> [UIView]) {
                self.extractSnapshots = extractSnapshots
            }
        }

        case textInput(textInput: TextInput, replyPanel: ReplyAccessoryPanelNode?)
        case stickerMediaInput(input: StickerInput, replyPanel: ReplyAccessoryPanelNode?)
        case audioMicInput(AudioMicInput)
        case videoMessage(VideoMessage)
        case mediaInput(MediaInput)
        case groupedMediaInput(GroupedMediaInput)
    }
            
    final class DecorationItemNodeImpl: ASDisplayNode, ChatMessageTransitionNode.DecorationItemNode {
        let itemNode: ChatMessageItemNodeProtocol
        let contentView: UIView
        var globalPortalSourceView: PortalSourceView?
        let aboveEverything: Bool
        private let getContentAreaInScreenSpace: () -> CGRect
        
        private let scrollingContainer: ASDisplayNode
        private let containerNode: ASDisplayNode
        private let clippingNode: ASDisplayNode
        
        fileprivate weak var overlayController: OverlayTransitionContainerController?
        
        init(itemNode: ChatMessageItemNodeProtocol, contentView: UIView, aboveEverything: Bool, getContentAreaInScreenSpace: @escaping () -> CGRect) {
            self.itemNode = itemNode
            self.contentView = contentView
            self.aboveEverything = aboveEverything
            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)
            
            if aboveEverything {
                let globalPortalSourceView = PortalSourceView()
                globalPortalSourceView.needsGlobalPortal = true
                self.globalPortalSourceView = globalPortalSourceView
                globalPortalSourceView.addSubview(self.contentView)
                self.containerNode.view.addSubview(globalPortalSourceView)
            } else {
                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
            if let globalPortalSourceView = self.globalPortalSourceView {
                globalPortalSourceView.frame = CGRect(origin: CGPoint(), size: size)
            }
        }
        
        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)
        }
    }
    
    final class CustomOffsetHandlerImpl {
        weak var itemNode: ChatMessageItemNodeProtocol?
        let update: (CGFloat, ContainedViewLayoutTransition) -> Bool
        
        init(itemNode: ChatMessageItemNodeProtocol, update: @escaping (CGFloat, ContainedViewLayoutTransition) -> Bool) {
            self.itemNode = itemNode
            self.update = update
        }
    }

    private final class AnimatingItemNode: ASDisplayNode {
        let itemNode: ChatMessageItemNodeProtocol
        private let contextSourceNode: ContextExtractedContentContainingNode
        private let source: ChatMessageTransitionNodeImpl.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: ChatMessageItemNodeProtocol, contextSourceNode: ContextExtractedContentContainingNode, source: ChatMessageTransitionNodeImpl.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 = ChatMessageTransitionNodeImpl.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 = ChatMessageTransitionNodeImpl.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 = ChatMessageTransitionNodeImpl.horizontalAnimationCurve
                let horizontalTransition: ContainedViewLayoutTransition = .animated(duration: horizontalDuration, curve: horizontalCurve)
                let verticalCurve = ChatMessageTransitionNodeImpl.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: ChatMessageBubbleItemNode.AnimationTransitionTextInput(
                            backgroundView: textInput.backgroundView,
                            contentView: textInput.contentView,
                            sourceRect: textInput.sourceRect,
                            scrollOffset: textInput.scrollOffset
                        ),
                        transition: combinedTransition
                    )
                    if let sourceReplyPanel = sourceReplyPanel {
                        itemNode.animateReplyPanel(
                            sourceReplyPanel: ChatMessageBubbleItemNode.AnimationTransitionReplyPanel(
                                titleNode: sourceReplyPanel.titleNode,
                                textNode: sourceReplyPanel.textNode,
                                lineNode: sourceReplyPanel.lineNode,
                                imageNode: sourceReplyPanel.imageNode,
                                relativeSourceRect: sourceReplyPanel.relativeSourceRect,
                                relativeTargetRect: sourceReplyPanel.relativeTargetRect
                            ),
                            transition: combinedTransition
                        )
                    }
                } else if let itemNode = self.itemNode as? ChatMessageAnimatedStickerItemNode {
                    itemNode.animateContentFromTextInputField(
                        textInput: ChatMessageAnimatedStickerItemNode.AnimationTransitionTextInput(
                            backgroundView: textInput.backgroundView,
                            contentView: textInput.contentView,
                            sourceRect: textInput.sourceRect,
                            scrollOffset: textInput.scrollOffset
                        ),
                        transition: combinedTransition
                    )
                    if let sourceReplyPanel = sourceReplyPanel {
                        itemNode.animateReplyPanel(
                            sourceReplyPanel: ChatMessageAnimatedStickerItemNode.AnimationTransitionReplyPanel(
                                titleNode: sourceReplyPanel.titleNode,
                                textNode: sourceReplyPanel.textNode,
                                lineNode: sourceReplyPanel.lineNode,
                                imageNode: sourceReplyPanel.imageNode,
                                relativeSourceRect: sourceReplyPanel.relativeSourceRect,
                                relativeTargetRect: sourceReplyPanel.relativeTargetRect
                            ),
                            transition: combinedTransition
                        )
                    }
                } else if let itemNode = self.itemNode as? ChatMessageStickerItemNode {
                    itemNode.animateContentFromTextInputField(
                        textInput: ChatMessageStickerItemNode.AnimationTransitionTextInput(
                            backgroundView: textInput.backgroundView,
                            contentView: textInput.contentView,
                            sourceRect: textInput.sourceRect,
                            scrollOffset: textInput.scrollOffset
                        ),
                        transition: combinedTransition
                    )
                    if let sourceReplyPanel = sourceReplyPanel {
                        itemNode.animateReplyPanel(
                            sourceReplyPanel: ChatMessageStickerItemNode.AnimationTransitionReplyPanel(
                                titleNode: sourceReplyPanel.titleNode,
                                textNode: sourceReplyPanel.textNode,
                                lineNode: sourceReplyPanel.lineNode,
                                imageNode: sourceReplyPanel.imageNode,
                                relativeSourceRect: sourceReplyPanel.relativeSourceRect,
                                relativeTargetRect: sourceReplyPanel.relativeTargetRect
                            ),
                            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, imageLayer: nil, relativeSourceRect: sourceItemNode.imageNode.frame)
                    sourceAbsoluteRect = sourceItemNode.view.convert(sourceItemNode.imageNode.frame, to: self.view)
                case let .mediaPanel(sourceItemNode):
                    stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: sourceItemNode.placeholderNode, imageLayer: nil, relativeSourceRect: sourceItemNode.imageNode.frame)
                    sourceAbsoluteRect = sourceItemNode.view.convert(sourceItemNode.imageNode.frame, to: self.view)
                case let .universal(sourceContainerView, sourceRect, sourceLayer):
                    stickerSource = Sticker(imageNode: nil, animationNode: nil, placeholderNode: nil, imageLayer: sourceLayer, relativeSourceRect: sourceLayer.frame)
                    sourceAbsoluteRect = convertAnimatingSourceRect(sourceRect, fromView: sourceContainerView, toView: self.view)
                case let .emptyPanel(sourceItemNode):
                    stickerSource = Sticker(imageNode: sourceItemNode.stickerNode.imageNode, animationNode: sourceItemNode.stickerNode.animationNode, placeholderNode: nil, imageLayer: 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: ChatMessageTransitionNodeImpl.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNodeImpl.verticalAnimationCurve))

                if let itemNode = self.itemNode as? ChatMessageAnimatedStickerItemNode {
                    itemNode.animateContentFromStickerGridItem(
                        stickerSource: ChatMessageAnimatedStickerItemNode.AnimationTransitionSticker(
                            imageNode: stickerSource.imageNode,
                            animationNode: stickerSource.animationNode,
                            placeholderNode: stickerSource.placeholderNode,
                            imageLayer: stickerSource.imageLayer,
                            relativeSourceRect: stickerSource.relativeSourceRect
                        ),
                        transition: combinedTransition
                    )
                    if let sourceAnimationNode = stickerSource.animationNode {
                        itemNode.animationNode?.setFrameIndex(sourceAnimationNode.currentFrameIndex)
                    }
                    if let sourceReplyPanel = sourceReplyPanel {
                        itemNode.animateReplyPanel(
                            sourceReplyPanel: ChatMessageAnimatedStickerItemNode.AnimationTransitionReplyPanel(
                                titleNode: sourceReplyPanel.titleNode,
                                textNode: sourceReplyPanel.textNode,
                                lineNode: sourceReplyPanel.lineNode,
                                imageNode: sourceReplyPanel.imageNode,
                                relativeSourceRect: sourceReplyPanel.relativeSourceRect,
                                relativeTargetRect: sourceReplyPanel.relativeTargetRect
                            ),
                            transition: combinedTransition
                        )
                    }
                } else if let itemNode = self.itemNode as? ChatMessageStickerItemNode {
                    itemNode.animateContentFromStickerGridItem(
                        stickerSource: ChatMessageStickerItemNode.AnimationTransitionSticker(
                            imageNode: stickerSource.imageNode,
                            animationNode: stickerSource.animationNode,
                            placeholderNode: stickerSource.placeholderNode,
                            imageLayer: stickerSource.imageLayer,
                            relativeSourceRect: stickerSource.relativeSourceRect
                        ),
                        transition: combinedTransition
                    )
                    if let sourceReplyPanel = sourceReplyPanel {
                        itemNode.animateReplyPanel(
                            sourceReplyPanel: ChatMessageStickerItemNode.AnimationTransitionReplyPanel(
                                titleNode: sourceReplyPanel.titleNode,
                                textNode: sourceReplyPanel.textNode,
                                lineNode: sourceReplyPanel.lineNode,
                                imageNode: sourceReplyPanel.imageNode,
                                relativeSourceRect: sourceReplyPanel.relativeSourceRect,
                                relativeTargetRect: sourceReplyPanel.relativeTargetRect
                            ),
                            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: ChatMessageTransitionNodeImpl.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), ChatMessageTransitionNodeImpl.horizontalAnimationCurve, horizontalDuration)
                self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), ChatMessageTransitionNodeImpl.verticalAnimationCurve, verticalDuration)
                self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.horizontalAnimationCurve.mediaTimingFunction, additive: true)

                switch stickerMediaInput {
                case .inputPanel, .universal:
                    break
                case let .mediaPanel(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: ChatMessageTransitionNodeImpl.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNodeImpl.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: ChatMessageTransitionNodeImpl.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: ChatMessageTransitionNodeImpl.horizontalAnimationCurve.mediaTimingFunction, additive: true)
                            }
                        }
                    }
                }
            case let .videoMessage(videoMessage):
                let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNodeImpl.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNodeImpl.verticalAnimationCurve))

                if let itemNode = self.itemNode as? ChatMessageBubbleItemNode {
                    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: ChatMessageTransitionNodeImpl.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: ChatMessageTransitionNodeImpl.verticalAnimationCurve.mediaTimingFunction, additive: true, completion: { [weak self] _ in
                        guard let strongSelf = self else {
                            return
                        }

                        strongSelf.endAnimation()
                    })

                    itemNode.animateInstantVideoFromSnapshot(snapshotView: videoMessage.view, transition: combinedTransition)
                }
            case let .mediaInput(mediaInput):
                if let snapshotView = mediaInput.extractSnapshot() {
                    Queue.mainQueue().justDispatch {
                        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: ChatMessageTransitionNodeImpl.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNodeImpl.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: ChatMessageTransitionNodeImpl.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: ChatMessageTransitionNodeImpl.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), ChatMessageTransitionNodeImpl.horizontalAnimationCurve, horizontalDuration)
                            self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), ChatMessageTransitionNodeImpl.verticalAnimationCurve, verticalDuration)
                        }
                    }
                } else {
                    self.endAnimation()
                }
            case let .groupedMediaInput(groupedMediaInput):
                let snapshotViews = groupedMediaInput.extractSnapshots()
                if snapshotViews.isEmpty {
                    self.endAnimation()
                    return
                }
                Queue.mainQueue().justDispatch {
                    if let itemNode = self.itemNode as? ChatMessageBubbleItemNode {
                        itemNode.cancelInsertionAnimations()

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

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

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

                        var targetContentRects: [CGRect] = []
                        if let itemNode = self.itemNode as? ChatMessageBubbleItemNode {
                            targetContentRects = itemNode.animateContentFromGroupedMediaInput(transition: combinedTransition)
                        }
                        
                        let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view)

                        func boundingRect(for views: [UIView]) -> CGRect {
                            var minX: CGFloat = .greatestFiniteMagnitude
                            var minY: CGFloat = .greatestFiniteMagnitude
                            var maxX: CGFloat = .leastNonzeroMagnitude
                            var maxY: CGFloat = .leastNonzeroMagnitude

                            for view in views {
                                let rect = view.frame
                                if rect.minX < minX {
                                    minX = rect.minX
                                }
                                if rect.minY < minY {
                                    minY = rect.minY
                                }
                                if rect.maxX > maxX {
                                    maxX = rect.maxX
                                }
                                if rect.maxY > maxY {
                                    maxY = rect.maxY
                                }
                            }
                            return CGRect(origin: CGPoint(x: minX, y: minY), size: CGSize(width: maxX - minX, height: maxY - minY))
                        }

                        let sourceBackgroundAbsoluteRect = boundingRect(for: snapshotViews)
                        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)

                        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: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.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: ChatMessageTransitionNodeImpl.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))

                        var index = 0
                        for snapshotView in snapshotViews {
                            let targetContentRect = targetContentRects[index]
                            let targetAbsoluteContentRect = targetContentRect.offsetBy(dx: targetAbsoluteRect.minX, dy: targetAbsoluteRect.minY)
                            
                            snapshotView.center = targetAbsoluteContentRect.center.offsetBy(dx: -self.containerNode.frame.minX, dy: -self.containerNode.frame.minY)
                            self.containerNode.view.addSubview(snapshotView)
                        
                            combinedTransition.horizontal.updateTransformScale(layer: snapshotView.layer, scale: CGPoint(x: 1.0 / (snapshotView.frame.width / targetContentRect.width), y: 1.0 / (snapshotView.frame.height / targetContentRect.height)))
                            
                            snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak snapshotView] _ in
                                snapshotView?.removeFromSuperview()
                            })
                            
                            index += 1
                        }
                        
                        self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), ChatMessageTransitionNodeImpl.horizontalAnimationCurve, horizontalDuration)
                        self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), ChatMessageTransitionNodeImpl.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?, isRotated: Bool) {
            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: ChatHistoryListNodeImpl
    private let getContentAreaInScreenSpace: () -> CGRect
    private let onTransitionEvent: (ContainedViewLayoutTransition) -> Void

    private var currentPendingItems: [Int64: (Source, () -> Void)] = [:]

    private var animatingItemNodes: [AnimatingItemNode] = []
    private var decorationItemNodes: [DecorationItemNodeImpl] = []
    private var messageReactionContexts: [MessageReactionContext] = []
    private var customOffsetHandlers: [CustomOffsetHandlerImpl] = []

    var hasScheduledTransitions: Bool {
        return !self.currentPendingItems.isEmpty
    }

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

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

        super.init()

        self.listNode.animationCorrelationMessagesFound = { [weak self] itemNodeAndCorrelationIds in
            guard let strongSelf = self else {
                return
            }
            
            for (correlationId, itemNode) in itemNodeAndCorrelationIds {
                if let (currentSource, initiated) = strongSelf.currentPendingItems[correlationId] {
                    strongSelf.beginAnimation(itemNode: itemNode, source: currentSource)
                    initiated()
                }
            }
            
            if itemNodeAndCorrelationIds.count == strongSelf.currentPendingItems.count {
                strongSelf.currentPendingItems = [:]
            }
        }
    }

    func add(correlationId: Int64, source: Source, initiated: @escaping () -> Void) {
        self.currentPendingItems = [correlationId: (source, initiated)]
        self.listNode.setCurrentSendAnimationCorrelationIds(Set([correlationId]))
    }
    
    func add(grouped: [(correlationId: Int64, source: Source, initiated: () -> Void)]) {
        var currentPendingItems: [Int64: (Source, () -> Void)] = [:]
        var correlationIds = Set<Int64>()
        for (correlationId, source, initiated) in grouped {
            currentPendingItems[correlationId] = (source, initiated)
            correlationIds.insert(correlationId)
        }
        
        self.currentPendingItems = currentPendingItems
        self.listNode.setCurrentSendAnimationCorrelationIds(correlationIds)
    }
    
    public func add(decorationView: UIView, itemNode: ChatMessageItemNodeProtocol, aboveEverything: Bool) -> DecorationItemNode {
        let decorationItemNode = DecorationItemNodeImpl(itemNode: itemNode, contentView: decorationView, aboveEverything: aboveEverything, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace)
        decorationItemNode.updateLayout(size: self.bounds.size)
       
        self.decorationItemNodes.append(decorationItemNode)
        self.addSubnode(decorationItemNode)
        
        return decorationItemNode
    }
    
    public func remove(decorationNode: DecorationItemNode) {
        self.decorationItemNodes.removeAll(where: { $0 === decorationNode })
        decorationNode.removeFromSupernode()
        if let decorationNode = decorationNode as? DecorationItemNodeImpl {
            decorationNode.overlayController?.dismiss()
        }
    }
    
    public func addCustomOffsetHandler(itemNode: ChatMessageItemNodeProtocol, update: @escaping (CGFloat, ContainedViewLayoutTransition) -> Bool) -> Disposable {
        let handler = CustomOffsetHandlerImpl(itemNode: itemNode, update: update)
        self.customOffsetHandlers.append(handler)
        
        return ActionDisposable { [weak self, weak handler] in
            Queue.mainQueue().async {
                guard let self, let handler else {
                    return
                }
                self.customOffsetHandlers.removeAll(where: { $0 === handler })
            }
        }
    }

    private func beginAnimation(itemNode: ChatMessageItemNodeProtocol, 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, .groupedMediaInput:
                let overlayController = OverlayTransitionContainerController()
                overlayController.displayNode.addSubnode(animatingItemNode)
                animatingItemNode.overlayController = overlayController
                self.listNode.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 {
                    for message in animatingItemNode.itemNode.messages() {
                        strongSelf.listNode.requestMessageUpdate(stableId: message.stableId)
                        break
                    }
                }
            }

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

            self.onTransitionEvent(.animated(duration: ChatMessageTransitionNodeImpl.animationDuration, curve: ChatMessageTransitionNodeImpl.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? ChatMessageItemNodeProtocol {
                if itemNode.matchesMessage(id: messageId) {
                    messageItemNode = itemNode
                }
            }
        }
        
        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?, isRotated: Bool) {
        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)
            }
            var removeCustomOffsetHandlers: [CustomOffsetHandlerImpl] = []
            for customOffsetHandler in self.customOffsetHandlers {
                if !customOffsetHandler.update(offset, transition) {
                    removeCustomOffsetHandlers.append(customOffsetHandler)
                }
            }
            for customOffsetHandler in removeCustomOffsetHandlers {
                self.customOffsetHandlers.removeAll(where: { $0 ===  customOffsetHandler})
            }
        }
        for messageReactionContext in self.messageReactionContexts {
            messageReactionContext.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode, isRotated: isRotated)
        }
    }

    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)
            }
            var removeCustomOffsetHandlers: [CustomOffsetHandlerImpl] = []
            for customOffsetHandler in self.customOffsetHandlers {
                if !customOffsetHandler.update(offset, .immediate) {
                    removeCustomOffsetHandlers.append(customOffsetHandler)
                }
            }
            for customOffsetHandler in removeCustomOffsetHandlers {
                self.customOffsetHandlers.removeAll(where: { $0 ===  customOffsetHandler})
            }
        }
        for messageReactionContext in self.messageReactionContexts {
            messageReactionContext.addContentOffset(offset: offset, itemNode: itemNode)
        }
    }

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

    func scheduleUpdateMessageAfterAnimationCompleted(stableId: UInt32) {
        for itemNode in self.animatingItemNodes {
            for message in itemNode.itemNode.messages() {
                if message.stableId == stableId {
                    itemNode.updateAfterCompletion = true
                }
            }
        }
    }

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