import Foundation
import UIKit
import Postbox
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import RadialStatusNode
import AnimatedCountLabelNode
import AnimatedAvatarSetNode
import ReactionButtonListComponent
import AccountContext
import WallpaperBackgroundNode
import ChatControllerInteraction

func canViewMessageReactionList(message: Message) -> Bool {
    var found = false
    var canViewList = false
    for attribute in message.attributes {
        if let attribute = attribute as? ReactionsMessageAttribute {
            canViewList = attribute.canViewList
            found = true
            break
        }
    }
    
    if !found {
        return false
    }
    
    if let peer = message.peers[message.id.peerId] {
        if let channel = peer as? TelegramChannel {
            if case .broadcast = channel.info {
                return false
            } else {
                return canViewList
            }
        } else if let _ = peer as? TelegramGroup {
            return canViewList
        } else if let _ = peer as? TelegramUser {
            return true
        } else {
            return false
        }
    } else {
        return false
    }
}

final class MessageReactionButtonsNode: ASDisplayNode {
    enum DisplayType {
        case incoming
        case outgoing
        case freeform
    }
    
    enum DisplayAlignment {
        case left
        case right
    }
    
    private var bubbleBackgroundNode: WallpaperBubbleBackgroundNode?
    private let container: ReactionButtonsAsyncLayoutContainer
    private var backgroundMaskView: UIView?
    private var backgroundMaskButtons: [MessageReaction.Reaction: UIView] = [:]
    
    var reactionSelected: ((MessageReaction.Reaction) -> Void)?
    var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingView, MessageReaction.Reaction) -> Void)?
    
    override init() {
        self.container = ReactionButtonsAsyncLayoutContainer()
        
        super.init()
    }
    
    deinit {
        
    }
    
    func update() {
    }
    
    func prepareUpdate(
        context: AccountContext,
        presentationData: ChatPresentationData,
        presentationContext: ChatPresentationContext,
        availableReactions: AvailableReactions?,
        reactions: ReactionsMessageAttribute,
        accountPeer: EnginePeer?,
        message: Message,
        alignment: DisplayAlignment,
        constrainedWidth: CGFloat,
        type: DisplayType
    ) -> (proposedWidth: CGFloat, continueLayout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> Void)) {
        let reactionColors: ReactionButtonComponent.Colors
        let themeColors: PresentationThemeBubbleColorComponents
        switch type {
        case .incoming:
            themeColors = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty)
            reactionColors = ReactionButtonComponent.Colors(
                deselectedBackground: themeColors.reactionInactiveBackground.argb,
                selectedBackground: themeColors.reactionActiveBackground.argb,
                deselectedForeground: themeColors.reactionInactiveForeground.argb,
                selectedForeground: themeColors.reactionActiveForeground.argb,
                extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb,
                extractedForeground:  presentationData.theme.theme.contextMenu.primaryColor.argb,
                deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
                selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb
            )
        case .outgoing:
            themeColors = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty)
            reactionColors = ReactionButtonComponent.Colors(
                deselectedBackground: themeColors.reactionInactiveBackground.argb,
                selectedBackground: themeColors.reactionActiveBackground.argb,
                deselectedForeground: themeColors.reactionInactiveForeground.argb,
                selectedForeground: themeColors.reactionActiveForeground.argb,
                extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb,
                extractedForeground:  presentationData.theme.theme.contextMenu.primaryColor.argb,
                deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
                selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb
            )
        case .freeform:
            if presentationData.theme.wallpaper.isEmpty {
                themeColors = presentationData.theme.theme.chat.message.freeform.withoutWallpaper
            } else {
                themeColors = presentationData.theme.theme.chat.message.freeform.withWallpaper
            }
            
            reactionColors = ReactionButtonComponent.Colors(
                deselectedBackground: selectReactionFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper).argb,
                selectedBackground: themeColors.reactionActiveBackground.argb,
                deselectedForeground: themeColors.reactionInactiveForeground.argb,
                selectedForeground: themeColors.reactionActiveForeground.argb,
                extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb,
                extractedForeground:  presentationData.theme.theme.contextMenu.primaryColor.argb,
                deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
                selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb
            )
        }
        
        var totalReactionCount: Int = 0
        for reaction in reactions.reactions {
            totalReactionCount += Int(reaction.count)
        }
        
        let reactionButtonsResult = self.container.update(
            context: context,
            action: { [weak self] value in
                guard let strongSelf = self else {
                    return
                }
                strongSelf.reactionSelected?(value)
            },
            reactions: reactions.reactions.map { reaction in
                var centerAnimation: TelegramMediaFile?
                var animationFileId: Int64?
                
                switch reaction.value {
                case .builtin:
                    if let availableReactions = availableReactions {
                        for availableReaction in availableReactions.reactions {
                            if availableReaction.value == reaction.value {
                                centerAnimation = availableReaction.centerAnimation
                                break
                            }
                        }
                    }
                case let .custom(fileId):
                    animationFileId = fileId
                }
                
                var peers: [EnginePeer] = []
                
                if message.id.peerId.namespace == Namespaces.Peer.CloudUser {
                    if reaction.isSelected, let accountPeer = accountPeer {
                        peers.append(accountPeer)
                    }
                    if !reaction.isSelected || reaction.count >= 2 {
                        if let peer = message.peers[message.id.peerId] {
                            peers.append(EnginePeer(peer))
                        }
                    }
                } else {
                    if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
                    } else {
                        for recentPeer in reactions.recentPeers {
                            if recentPeer.value == reaction.value {
                                if let peer = message.peers[recentPeer.peerId] {
                                    peers.append(EnginePeer(peer))
                                }
                            }
                        }
                    }
                    
                    if peers.count != Int(reaction.count) || totalReactionCount != reactions.recentPeers.count {
                        peers.removeAll()
                    }
                }
                
                return ReactionButtonsAsyncLayoutContainer.Reaction(
                    reaction: ReactionButtonComponent.Reaction(
                        value: reaction.value,
                        centerAnimation: centerAnimation,
                        animationFileId: animationFileId
                    ),
                    count: Int(reaction.count),
                    peers: peers,
                    chosenOrder: reaction.chosenOrder
                )
            },
            colors: reactionColors,
            constrainedWidth: constrainedWidth
        )
        
        var reactionButtonsSize = CGSize()
        var currentRowWidth: CGFloat = 0.0
        for item in reactionButtonsResult.items {
            if currentRowWidth + item.size.width > constrainedWidth {
                reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
                if !reactionButtonsSize.height.isZero {
                    reactionButtonsSize.height += 6.0
                }
                reactionButtonsSize.height += item.size.height
                currentRowWidth = 0.0
            }
            
            if !currentRowWidth.isZero {
                currentRowWidth += 6.0
            }
            currentRowWidth += item.size.width
        }
        if !currentRowWidth.isZero && !reactionButtonsResult.items.isEmpty {
            reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
            if !reactionButtonsSize.height.isZero {
                reactionButtonsSize.height += 6.0
            }
            reactionButtonsSize.height += reactionButtonsResult.items[0].size.height
        }
        
        let topInset: CGFloat = 0.0
        let bottomInset: CGFloat = 2.0
        
        return (proposedWidth: reactionButtonsSize.width, continueLayout: { [weak self] boundingWidth in
            let size = CGSize(width: boundingWidth, height: topInset + reactionButtonsSize.height + bottomInset)
            return (size: size, apply: { animation in
                guard let strongSelf = self else {
                    return
                }
                
                if strongSelf.backgroundMaskView == nil {
                    strongSelf.backgroundMaskView = UIView()
                }
                
                let backgroundInsets: CGFloat = 10.0
                
                switch type {
                case .freeform:
                    if let backgroundNode = presentationContext.backgroundNode, backgroundNode.hasBubbleBackground(for: .free) {
                        let bubbleBackgroundFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -backgroundInsets, dy: -backgroundInsets)
                        if let bubbleBackgroundNode = strongSelf.bubbleBackgroundNode {
                            animation.animator.updateFrame(layer: bubbleBackgroundNode.layer, frame: bubbleBackgroundFrame, completion: nil)
                            if let (rect, containerSize) = strongSelf.absoluteRect {
                                bubbleBackgroundNode.update(rect: rect, within: containerSize, transition: animation.transition)
                            }
                        } else if strongSelf.bubbleBackgroundNode == nil {
                            if let bubbleBackgroundNode = backgroundNode.makeBubbleBackground(for: .free) {
                                strongSelf.bubbleBackgroundNode = bubbleBackgroundNode
                                bubbleBackgroundNode.view.mask = strongSelf.backgroundMaskView
                                strongSelf.insertSubnode(bubbleBackgroundNode, at: 0)
                                bubbleBackgroundNode.frame = bubbleBackgroundFrame
                            }
                        }
                    } else {
                        if let bubbleBackgroundNode = strongSelf.bubbleBackgroundNode {
                            strongSelf.bubbleBackgroundNode = nil
                            bubbleBackgroundNode.removeFromSupernode()
                        }
                    }
                case .incoming, .outgoing:
                    if let bubbleBackgroundNode = strongSelf.bubbleBackgroundNode {
                        strongSelf.bubbleBackgroundNode = nil
                        bubbleBackgroundNode.removeFromSupernode()
                    }
                }
                
                var reactionButtonPosition: CGPoint
                switch alignment {
                case .left:
                    reactionButtonPosition = CGPoint(x: -1.0, y: topInset)
                case .right:
                    reactionButtonPosition = CGPoint(x: size.width + 1.0, y: topInset)
                }
                
                let reactionButtons = reactionButtonsResult.apply(
                    animation,
                    ReactionButtonsAsyncLayoutContainer.Arguments(
                        animationCache: presentationContext.animationCache,
                        animationRenderer: presentationContext.animationRenderer
                    )
                )
                
                var validIds = Set<MessageReaction.Reaction>()
                for item in reactionButtons.items {
                    validIds.insert(item.value)
                    
                    switch alignment {
                    case .left:
                        if reactionButtonPosition.x + item.size.width > boundingWidth {
                            reactionButtonPosition.x = -1.0
                            reactionButtonPosition.y += item.size.height + 6.0
                        }
                    case .right:
                        if reactionButtonPosition.x - item.size.width < -1.0 {
                            reactionButtonPosition.x = size.width + 1.0
                            reactionButtonPosition.y += item.size.height + 6.0
                        }
                    }
                    
                    let itemFrame: CGRect
                    switch alignment {
                    case .left:
                        itemFrame = CGRect(origin: reactionButtonPosition, size: item.size)
                        reactionButtonPosition.x += item.size.width + 6.0
                    case .right:
                        itemFrame = CGRect(origin: CGPoint(x: reactionButtonPosition.x - item.size.width, y: reactionButtonPosition.y), size: item.size)
                        reactionButtonPosition.x -= item.size.width + 6.0
                    }
                    
                    let itemMaskFrame = itemFrame.offsetBy(dx: backgroundInsets, dy: backgroundInsets)
                    
                    let itemMaskView: UIView
                    if let current = strongSelf.backgroundMaskButtons[item.value] {
                        itemMaskView = current
                    } else {
                        itemMaskView = UIView()
                        itemMaskView.backgroundColor = .black
                        itemMaskView.clipsToBounds = true
                        itemMaskView.layer.cornerRadius = 15.0
                        strongSelf.backgroundMaskButtons[item.value] = itemMaskView
                    }
                    
                    if item.node.view.superview != strongSelf.view {
                        assert(item.node.view.superview == nil)
                        strongSelf.view.addSubview(item.node.view)
                        if animation.isAnimated {
                            item.node.view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
                            item.node.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
                        }
                        item.node.view.frame = itemFrame
                    } else {
                        animation.animator.updateFrame(layer: item.node.view.layer, frame: itemFrame, completion: nil)
                    }
                    
                    let itemValue = item.value
                    let itemNode = item.node
                    item.node.view.isGestureEnabled = true
                    let canViewReactionList = canViewMessageReactionList(message: message)
                    item.node.view.activateAfterCompletion = !canViewReactionList
                    item.node.view.activated = { [weak itemNode] gesture, _ in
                        guard let strongSelf = self, let itemNode = itemNode else {
                            gesture.cancel()
                            return
                        }
                        if !canViewReactionList {
                            return
                        }
                        strongSelf.openReactionPreview?(gesture, itemNode.view.containerView, itemValue)
                    }
                    item.node.view.additionalActivationProgressLayer = itemMaskView.layer
                    
                    if let backgroundMaskView = strongSelf.backgroundMaskView {
                        if itemMaskView.superview != backgroundMaskView {
                            assert(itemMaskView.superview == nil)
                            backgroundMaskView.addSubview(itemMaskView)
                            if animation.isAnimated {
                                itemMaskView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
                                itemMaskView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
                            }
                            itemMaskView.frame = itemMaskFrame
                        } else {
                            animation.animator.updateFrame(layer: itemMaskView.layer, frame: itemMaskFrame, completion: nil)
                        }
                    }
                }
                
                var removeMaskIds: [MessageReaction.Reaction] = []
                for (id, view) in strongSelf.backgroundMaskButtons {
                    if !validIds.contains(id) {
                        removeMaskIds.append(id)
                        if animation.isAnimated {
                            view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
                            view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in
                                view?.layer.removeAllAnimations()
                                view?.removeFromSuperview()
                            })
                        } else {
                            view.removeFromSuperview()
                        }
                    }
                }
                for id in removeMaskIds {
                    strongSelf.backgroundMaskButtons.removeValue(forKey: id)
                }
                
                for node in reactionButtons.removedNodes {
                    if animation.isAnimated {
                        node.view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
                        node.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
                            node.view.removeFromSuperview()
                        })
                    } else {
                        node.view.removeFromSuperview()
                    }
                }
            })
        })
    }
    
    private var absoluteRect: (CGRect, CGSize)?
    
    func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
        self.absoluteRect = (rect, containerSize)
        
        if let bubbleBackgroundNode = self.bubbleBackgroundNode {
            bubbleBackgroundNode.update(rect: rect, within: containerSize, transition: transition)
        }
    }
    
    func update(rect: CGRect, within containerSize: CGSize, transition: CombinedTransition) {
        self.absoluteRect = (rect, containerSize)
        
        if let bubbleBackgroundNode = self.bubbleBackgroundNode {
            bubbleBackgroundNode.update(rect: rect, within: containerSize, transition: transition)
        }
    }
    
    func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
        if let bubbleBackgroundNode = self.bubbleBackgroundNode {
            bubbleBackgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration)
        }
    }
    
    func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
        if let bubbleBackgroundNode = self.bubbleBackgroundNode {
            bubbleBackgroundNode.offsetSpring(value: value, duration: duration, damping: damping)
        }
    }
    
    func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
        for (key, button) in self.container.buttons {
            if key == value {
                return button.view.iconView
            }
        }
        return nil
    }
    
    func animateIn(animation: ListViewItemUpdateAnimation) {
        for (_, button) in self.container.buttons {
            animation.animator.animateScale(layer: button.view.layer, from: 0.01, to: 1.0, completion: nil)
        }
    }
    
    func animateOut(animation: ListViewItemUpdateAnimation) {
        for (_, button) in self.container.buttons {
            animation.animator.updateScale(layer: button.view.layer, scale: 0.01, completion: nil)
        }
    }
    
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        for (_, button) in self.container.buttons {
            if button.view.frame.contains(point) {
                if let result = button.view.hitTest(self.view.convert(point, to: button.view), with: event) {
                    return result
                }
            }
        }
        
        return nil
    }
}

final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode {
    private let buttonsNode: MessageReactionButtonsNode
    
    required init() {
        self.buttonsNode = MessageReactionButtonsNode()
        
        super.init()
        
        self.addSubnode(self.buttonsNode)
        
        self.buttonsNode.reactionSelected = { [weak self] value in
            guard let strongSelf = self, let item = strongSelf.item else {
                return
            }
            item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
        }
        
        self.buttonsNode.openReactionPreview = { [weak self] gesture, sourceNode, value in
            guard let strongSelf = self, let item = strongSelf.item else {
                gesture?.cancel()
                return
            }
            
            item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value)
        }
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
        let buttonsNode = self.buttonsNode
        
        return { item, layoutConstants, preparePosition, _, constrainedSize, _ in
            let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
            
            //let displaySeparator: Bool
            let topOffset: CGFloat
            if case let .linear(top, _) = preparePosition, case .Neighbour(_, .media, _) = top {
                //displaySeparator = false
                topOffset = 4.0
            } else {
                //displaySeparator = true
                topOffset = 0.0
            }
            
            return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
                let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: [])
                let buttonsUpdate = buttonsNode.prepareUpdate(
                    context: item.context,
                    presentationData: item.presentationData,
                    presentationContext: item.controllerInteraction.presentationContext,
                    availableReactions: item.associatedData.availableReactions, reactions: reactionsAttribute, accountPeer: item.associatedData.accountPeer, message: item.message, alignment: .left, constrainedWidth: constrainedSize.width - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, type: item.message.effectivelyIncoming(item.context.account.peerId) ? .incoming : .outgoing)
                     
                return (layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + buttonsUpdate.proposedWidth, { boundingWidth in
                    var boundingSize = CGSize()
                    
                    let buttonsSizeAndApply = buttonsUpdate.continueLayout(boundingWidth - (layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right))
                    
                    boundingSize = buttonsSizeAndApply.size
                    
                    boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
                    boundingSize.height += topOffset + 2.0
                    
                    return (boundingSize, { [weak self] animation, synchronousLoad, _ in
                        if let strongSelf = self {
                            strongSelf.item = item
                            
                            animation.animator.updateFrame(layer: strongSelf.buttonsNode.layer, frame: CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.left, y: topOffset - 2.0), size: buttonsSizeAndApply.size), completion: nil)
                            buttonsSizeAndApply.apply(animation)
                            
                            let _ = synchronousLoad
                        }
                    })
                })
            })
        }
    }
    
    override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
        self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
    }
    
    override func animateAdded(_ currentTimestamp: Double, duration: Double) {
        self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
    }
    
    override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
        self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
        self.buttonsNode.animateOut(animation: ListViewItemUpdateAnimation.System(duration: 0.25, transition: ControlledTransition(duration: 0.25, curve: .spring, interactive: false)))
    }
    
    override func animateInsertionIntoBubble(_ duration: Double) {
        self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
        self.layer.animatePosition(from: CGPoint(x: 0.0, y: -self.bounds.height / 2.0), to: CGPoint(), duration: duration, removeOnCompletion: true, additive: true)
    }
    
    override func animateRemovalFromBubble(_ duration: Double, completion: @escaping () -> Void) {
        self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -self.bounds.height / 2.0), duration: duration, removeOnCompletion: false, additive: true)
        self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
            completion()
        })
        self.buttonsNode.animateOut(animation: ListViewItemUpdateAnimation.System(duration: 0.25, transition: ControlledTransition(duration: 0.25, curve: .spring, interactive: false)))
    }
    
    override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
        if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: nil), result !== self.buttonsNode.view {
            return .ignore
        }
        return .none
    }
    
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: event), result !== self.buttonsNode.view {
            return result
        }
        return nil
    }
    
    override func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
        return self.buttonsNode.reactionTargetView(value: value)
    }
}

final class ChatMessageReactionButtonsNode: ASDisplayNode {
    final class Arguments {
        let context: AccountContext
        let presentationData: ChatPresentationData
        let presentationContext: ChatPresentationContext
        let availableReactions: AvailableReactions?
        let reactions: ReactionsMessageAttribute
        let message: Message
        let accountPeer: EnginePeer?
        let isIncoming: Bool
        let constrainedWidth: CGFloat
        
        init(
            context: AccountContext,
            presentationData: ChatPresentationData,
            presentationContext: ChatPresentationContext,
            availableReactions: AvailableReactions?,
            reactions: ReactionsMessageAttribute,
            message: Message,
            accountPeer: EnginePeer?,
            isIncoming: Bool,
            constrainedWidth: CGFloat
        ) {
            self.context = context
            self.presentationData = presentationData
            self.presentationContext = presentationContext
            self.availableReactions = availableReactions
            self.reactions = reactions
            self.message = message
            self.accountPeer = accountPeer
            self.isIncoming = isIncoming
            self.constrainedWidth = constrainedWidth
        }
    }
    
    private let buttonsNode: MessageReactionButtonsNode
    
    var reactionSelected: ((MessageReaction.Reaction) -> Void)?
    var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingView, MessageReaction.Reaction) -> Void)?
    
    override init() {
        self.buttonsNode = MessageReactionButtonsNode()
        
        super.init()
        
        self.addSubnode(self.buttonsNode)
        
        self.buttonsNode.reactionSelected = { [weak self] value in
            self?.reactionSelected?(value)
        }
        
        self.buttonsNode.openReactionPreview = { [weak self] gesture, sourceNode, value in
            self?.openReactionPreview?(gesture, sourceNode, value)
        }
    }
    
    class func asyncLayout(_ maybeNode: ChatMessageReactionButtonsNode?) -> (_ arguments: ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)) {
        return { arguments in
            let node = maybeNode ?? ChatMessageReactionButtonsNode()
            
            let buttonsUpdate = node.buttonsNode.prepareUpdate(
                context: arguments.context,
                presentationData: arguments.presentationData,
                presentationContext: arguments.presentationContext,
                availableReactions: arguments.availableReactions,
                reactions: arguments.reactions,
                accountPeer: arguments.accountPeer,
                message: arguments.message,
                alignment: arguments.isIncoming ? .left : .right,
                constrainedWidth: arguments.constrainedWidth,
                type: .freeform
            )
            
            return (buttonsUpdate.proposedWidth, { constrainedWidth in
                let buttonsResult = buttonsUpdate.continueLayout(constrainedWidth)
                
                return (CGSize(width: constrainedWidth, height: buttonsResult.size.height), { animation in
                    node.buttonsNode.frame = CGRect(origin: CGPoint(), size: buttonsResult.size)
                    buttonsResult.apply(animation)
                    
                    return node
                })
            })
        }
    }
    
    func animateIn(animation: ListViewItemUpdateAnimation) {
        self.buttonsNode.animateIn(animation: animation)
        self.buttonsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
    }
    
    func animateOut(animation: ListViewItemUpdateAnimation, completion: @escaping () -> Void) {
        self.buttonsNode.animateOut(animation: animation)
        animation.animator.updateAlpha(layer: self.buttonsNode.layer, alpha: 0.0, completion: { _ in
            completion()
        })
        animation.animator.updateFrame(layer: self.buttonsNode.layer, frame: self.buttonsNode.layer.frame.offsetBy(dx: 0.0, dy: -self.buttonsNode.layer.bounds.height / 2.0), completion: nil)
    }
    
    func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
        return self.buttonsNode.reactionTargetView(value: value)
    }
    
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: event), result !== self.buttonsNode.view {
            return result
        }
        return nil
    }
    
    func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
        self.buttonsNode.update(rect: rect, within: containerSize, transition: transition)
    }
    
    func update(rect: CGRect, within containerSize: CGSize, transition: CombinedTransition) {
        self.buttonsNode.update(rect: rect, within: containerSize, transition: transition)
    }
    
    func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
        self.buttonsNode.offset(value: value, animationCurve: animationCurve, duration: duration)
    }
    
    func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
        self.buttonsNode.offsetSpring(value: value, duration: duration, damping: damping)
    }
}