import Foundation
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TelegramPresentationData
import UIKit
import AnimatedAvatarSetNode
import ContextUI
import AvatarNode
import ReactionImageComponent

private let avatarFont = avatarPlaceholderFont(size: 16.0)

public final class ReactionListContextMenuContent: ContextControllerItemsContent {
    private final class BackButtonNode: HighlightTrackingButtonNode {
        let highlightBackgroundNode: ASDisplayNode
        let titleLabelNode: ImmediateTextNode
        let separatorNode: ASDisplayNode
        let iconNode: ASImageNode
        
        var action: (() -> Void)?
        
        private var theme: PresentationTheme?
        
        init() {
            self.highlightBackgroundNode = ASDisplayNode()
            self.highlightBackgroundNode.isAccessibilityElement = false
            self.highlightBackgroundNode.alpha = 0.0
            
            self.titleLabelNode = ImmediateTextNode()
            self.titleLabelNode.isAccessibilityElement = false
            self.titleLabelNode.maximumNumberOfLines = 1
            self.titleLabelNode.isUserInteractionEnabled = false
            
            self.iconNode = ASImageNode()
            self.iconNode.isAccessibilityElement = false
            
            self.separatorNode = ASDisplayNode()
            self.separatorNode.isAccessibilityElement = false
            
            super.init()
            
            self.addSubnode(self.separatorNode)
            self.addSubnode(self.highlightBackgroundNode)
            self.addSubnode(self.titleLabelNode)
            self.addSubnode(self.iconNode)
            
            self.isAccessibilityElement = true
            
            self.highligthedChanged = { [weak self] highlighted in
                guard let strongSelf = self else {
                    return
                }
                if highlighted {
                    strongSelf.highlightBackgroundNode.alpha = 1.0
                } else {
                    let previousAlpha = strongSelf.highlightBackgroundNode.alpha
                    strongSelf.highlightBackgroundNode.alpha = 0.0
                    strongSelf.highlightBackgroundNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2)
                }
            }
            
            self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
        }
        
        @objc private func pressed() {
            self.action?()
        }
        
        func update(size: CGSize, presentationData: PresentationData, isLast: Bool) {
            let standardIconWidth: CGFloat = 32.0
            let sideInset: CGFloat = 16.0
            let iconSideInset: CGFloat = 12.0
            
            if self.theme !== presentationData.theme {
                self.theme = presentationData.theme
                self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: presentationData.theme.contextMenu.primaryColor)
                
                self.accessibilityLabel = presentationData.strings.Common_Back
            }
            
            self.highlightBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
            self.separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
            
            self.highlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: size)
            
            self.titleLabelNode.attributedText = NSAttributedString(string: presentationData.strings.Common_Back, font: Font.regular(17.0), textColor: presentationData.theme.contextMenu.primaryColor)
            let titleSize = self.titleLabelNode.updateLayout(CGSize(width: size.width - sideInset - standardIconWidth, height: 100.0))
            self.titleLabelNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
            
            if let iconImage = self.iconNode.image {
                let iconWidth = max(standardIconWidth, iconImage.size.width)
                let iconFrame = CGRect(origin: CGPoint(x: size.width - iconSideInset - iconWidth + floor((iconWidth - iconImage.size.width) / 2.0), y: floor((size.height - iconImage.size.height) / 2.0)), size: iconImage.size)
                self.iconNode.frame = iconFrame
            }
            
            self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))
            self.separatorNode.isHidden = isLast
        }
    }
    
    private final class ReactionTabListNode: ASDisplayNode {
        private final class ItemNode: ASDisplayNode {
            let context: AccountContext
            let reaction: String?
            let count: Int
            
            let titleLabelNode: ImmediateTextNode
            let iconNode: ASImageNode?
            let reactionIconNode: ReactionImageNode?
            
            private var theme: PresentationTheme?
            
            var action: ((String?) -> Void)?
            
            init(context: AccountContext, availableReactions: AvailableReactions?, reaction: String?, count: Int) {
                self.context = context
                self.reaction = reaction
                self.count = count
                
                self.titleLabelNode = ImmediateTextNode()
                self.titleLabelNode.isUserInteractionEnabled = false
                
                if let reaction = reaction {
                    self.reactionIconNode = ReactionImageNode(context: context, availableReactions: availableReactions, reaction: reaction, displayPixelSize: CGSize(width: 30.0 * UIScreenScale, height: 30.0 * UIScreenScale))
                    self.reactionIconNode?.isUserInteractionEnabled = false
                    self.iconNode = nil
                } else {
                    self.reactionIconNode = nil
                    self.iconNode = ASImageNode()
                    self.iconNode?.isUserInteractionEnabled = false
                }
                
                super.init()
                
                self.addSubnode(self.titleLabelNode)
                if let iconNode = self.iconNode {
                    self.addSubnode(iconNode)
                }
                if let reactionIconNode = self.reactionIconNode {
                    self.addSubnode(reactionIconNode)
                }
                
                self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
            }
            
            @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
                if case .ended = recognizer.state {
                    self.action?(self.reaction)
                }
            }
            
            func update(presentationData: PresentationData, constrainedSize: CGSize, isSelected: Bool) -> CGSize {
                if presentationData.theme !== self.theme {
                    self.theme = presentationData.theme
                    
                    if let iconNode = self.iconNode {
                        iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: presentationData.theme.contextMenu.primaryColor)
                    }
                }
                
                let sideInset: CGFloat = 12.0
                let iconSpacing: CGFloat = 4.0
                
                var iconSize = CGSize(width: 22.0, height: 22.0)
                if let _ = self.reactionIconNode {
                } else if let iconNode = self.iconNode, let image = iconNode.image {
                    iconSize = image.size.aspectFitted(iconSize)
                }
                
                self.titleLabelNode.attributedText = NSAttributedString(string: "\(count)", font: Font.medium(11.0), textColor: presentationData.theme.contextMenu.primaryColor)
                let titleSize = self.titleLabelNode.updateLayout(constrainedSize)
                
                let contentSize = CGSize(width: sideInset * 2.0 + titleSize.width + iconSize.width + iconSpacing, height: titleSize.height)
                
                self.titleLabelNode.frame = CGRect(origin: CGPoint(x: sideInset + iconSize.width + iconSpacing, y: floorToScreenPixels((constrainedSize.height - titleSize.height) / 2.0)), size: titleSize)
                
                if let reactionIconNode = self.reactionIconNode {
                    reactionIconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize)
                    reactionIconNode.update(size: iconSize)
                } else if let iconNode = self.iconNode {
                    iconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize)
                }
                
                return CGSize(width: contentSize.width, height: constrainedSize.height)
            }
        }
        
        private let scrollNode: ASScrollNode
        private let selectionHighlightNode: ASDisplayNode
        private let itemNodes: [ItemNode]
        
        struct ScrollToTabReaction {
            var value: String?
        }
        var scrollToTabReaction: ScrollToTabReaction?
        
        var action: ((String?) -> Void)?
        
        init(context: AccountContext, availableReactions: AvailableReactions?, reactions: [(String?, Int)], message: EngineMessage) {
            self.scrollNode = ASScrollNode()
            self.scrollNode.canCancelAllTouchesInViews = true
            self.scrollNode.view.delaysContentTouches = false
            self.scrollNode.view.showsVerticalScrollIndicator = false
            self.scrollNode.view.showsHorizontalScrollIndicator = false
            if #available(iOS 11.0, *) {
                self.scrollNode.view.contentInsetAdjustmentBehavior = .never
            }
            self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
            
            self.itemNodes = reactions.map { reaction, count in
                return ItemNode(context: context, availableReactions: availableReactions, reaction: reaction, count: count)
            }
            
            self.selectionHighlightNode = ASDisplayNode()
            
            super.init()
            
            self.addSubnode(self.scrollNode)
            
            self.scrollNode.addSubnode(self.selectionHighlightNode)
            
            for itemNode in self.itemNodes {
                self.scrollNode.addSubnode(itemNode)
                itemNode.action = { [weak self] reaction in
                    guard let strongSelf = self else {
                        return
                    }
                    strongSelf.scrollToTabReaction = ScrollToTabReaction(value: reaction)
                    strongSelf.action?(reaction)
                }
            }
        }
        
        func update(size: CGSize, presentationData: PresentationData, selectedReaction: String?, transition: ContainedViewLayoutTransition) {
            let sideInset: CGFloat = 11.0
            let spacing: CGFloat = 0.0
            let verticalInset: CGFloat = 7.0
            
            self.selectionHighlightNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
            let highlightHeight: CGFloat = size.height - verticalInset * 2.0
            self.selectionHighlightNode.cornerRadius = highlightHeight / 2.0
            
            var contentWidth: CGFloat = sideInset
            for i in 0 ..< self.itemNodes.count {
                if i != 0 {
                    contentWidth += spacing
                }
                
                let itemNode = self.itemNodes[i]
                let itemSize = itemNode.update(presentationData: presentationData, constrainedSize: CGSize(width: size.width, height: size.height), isSelected: itemNode.reaction == selectedReaction)
                let itemFrame = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: itemSize)
                itemNode.frame = itemFrame
                
                if itemNode.reaction == selectedReaction {
                    transition.updateFrame(node: self.selectionHighlightNode, frame: CGRect(origin: CGPoint(x: itemFrame.minX, y: verticalInset), size: CGSize(width: itemFrame.width, height: highlightHeight)))
                }
                
                contentWidth += itemSize.width
            }
            contentWidth += sideInset
            
            self.scrollNode.frame = CGRect(origin: CGPoint(), size: size)
            
            let contentSize = CGSize(width: contentWidth, height: size.height)
            if self.scrollNode.view.contentSize != contentSize {
                self.scrollNode.view.contentSize = contentSize
            }
            
            if let scrollToTabReaction = self.scrollToTabReaction {
                self.scrollToTabReaction = nil
                for itemNode in self.itemNodes {
                    if itemNode.reaction == scrollToTabReaction.value {
                        self.scrollNode.view.scrollRectToVisible(itemNode.frame.insetBy(dx: -sideInset - 8.0, dy: 0.0), animated: transition.isAnimated)
                        break
                    }
                }
            }
        }
    }
    
    private final class ReactionsTabNode: ASDisplayNode, UIScrollViewDelegate {
        private final class ItemNode: HighlightTrackingButtonNode {
            let context: AccountContext
            let availableReactions: AvailableReactions?
            let highlightBackgroundNode: ASDisplayNode
            let avatarNode: AvatarNode
            let titleLabelNode: ImmediateTextNode
            let separatorNode: ASDisplayNode
            var reactionIconNode: ReactionImageNode?
            let action: () -> Void
            
            private var item: EngineMessageReactionListContext.Item?
            
            init(context: AccountContext, availableReactions: AvailableReactions?, action: @escaping () -> Void) {
                self.action = action
                self.context = context
                self.availableReactions = availableReactions
                
                self.avatarNode = AvatarNode(font: avatarFont)
                self.avatarNode.isAccessibilityElement = false
                
                self.highlightBackgroundNode = ASDisplayNode()
                self.highlightBackgroundNode.isAccessibilityElement = false
                self.highlightBackgroundNode.alpha = 0.0
                
                self.titleLabelNode = ImmediateTextNode()
                self.titleLabelNode.isAccessibilityElement = false
                self.titleLabelNode.maximumNumberOfLines = 1
                self.titleLabelNode.isUserInteractionEnabled = false
                
                self.separatorNode = ASDisplayNode()
                self.separatorNode.isAccessibilityElement = false
                
                super.init()
                
                self.isAccessibilityElement = true
                
                self.addSubnode(self.separatorNode)
                self.addSubnode(self.highlightBackgroundNode)
                self.addSubnode(self.avatarNode)
                self.addSubnode(self.titleLabelNode)
                
                self.highligthedChanged = { [weak self] highlighted in
                    guard let strongSelf = self else {
                        return
                    }
                    if highlighted {
                        strongSelf.highlightBackgroundNode.alpha = 1.0
                    } else {
                        let previousAlpha = strongSelf.highlightBackgroundNode.alpha
                        strongSelf.highlightBackgroundNode.alpha = 0.0
                        strongSelf.highlightBackgroundNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2)
                    }
                }
                
                self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
            }
            
            @objc private func pressed() {
                self.action()
            }
            
            func update(size: CGSize, presentationData: PresentationData, item: EngineMessageReactionListContext.Item, isLast: Bool, syncronousLoad: Bool) {
                let avatarInset: CGFloat = 12.0
                let avatarSpacing: CGFloat = 8.0
                let avatarSize: CGFloat = 28.0
                let sideInset: CGFloat = 16.0
                
                let reaction: String? = item.reaction
                if let reaction = reaction {
                    if self.reactionIconNode == nil {
                        let reactionIconNode = ReactionImageNode(context: self.context, availableReactions: self.availableReactions, reaction: reaction, displayPixelSize: CGSize(width: 30.0 * UIScreenScale, height: 30.0 * UIScreenScale))
                        self.reactionIconNode = reactionIconNode
                        self.addSubnode(reactionIconNode)
                    }
                } else if let reactionIconNode = self.reactionIconNode {
                    reactionIconNode.removeFromSupernode()
                }
                
                if self.item != item {
                    self.item = item
                    
                    self.accessibilityLabel = "\(item.peer.debugDisplayTitle) \(item.reaction ?? "")"
                }
                
                self.highlightBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
                self.separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
                
                self.highlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: size)
                
                self.avatarNode.frame = CGRect(origin: CGPoint(x: avatarInset, y: floor((size.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
                self.avatarNode.setPeer(context: self.context, theme: presentationData.theme, peer: item.peer, synchronousLoad: true)
                
                self.titleLabelNode.attributedText = NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.regular(17.0), textColor: presentationData.theme.contextMenu.primaryColor)
                var maxTextWidth: CGFloat = size.width - avatarInset - avatarSize - avatarSpacing - sideInset
                if reactionIconNode != nil {
                    maxTextWidth -= 32.0
                }
                let titleSize = self.titleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 100.0))
                self.titleLabelNode.frame = CGRect(origin: CGPoint(x: avatarInset + avatarSize + avatarSpacing, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
                
                if let reactionIconNode = self.reactionIconNode {
                    let reactionSize = CGSize(width: 22.0, height: 22.0)
                    reactionIconNode.frame = CGRect(origin: CGPoint(x: size.width - 32.0 - floor((32.0 - reactionSize.width) / 2.0), y: floor((size.height - reactionSize.height) / 2.0)), size: reactionSize)
                    reactionIconNode.update(size: reactionSize)
                }
                
                self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: CGSize(width: size.width, height: UIScreenPixel))
                self.separatorNode.isHidden = isLast
            }
        }
        
        private struct ItemsState {
            let listState: EngineMessageReactionListContext.State
            let readStats: MessageReadStats?
            
            let mergedItems: [EngineMessageReactionListContext.Item]
            
            init(listState: EngineMessageReactionListContext.State, readStats: MessageReadStats?) {
                self.listState = listState
                self.readStats = readStats
                
                var mergedItems: [EngineMessageReactionListContext.Item] = listState.items
                if !listState.canLoadMore, let readStats = readStats {
                    var existingPeers = Set(mergedItems.map(\.peer.id))
                    for peer in readStats.peers {
                        if !existingPeers.contains(peer.id) {
                            existingPeers.insert(peer.id)
                            mergedItems.append(EngineMessageReactionListContext.Item(peer: peer, reaction: nil))
                        }
                    }
                }
                self.mergedItems = mergedItems
            }
            
            var totalCount: Int {
                if !self.listState.canLoadMore {
                    return self.mergedItems.count
                } else {
                    let reactionCount = self.listState.totalCount
                    var value = reactionCount
                    if let readStats = self.readStats {
                        if reactionCount < readStats.peers.count && self.listState.hasOutgoingReaction {
                            value = readStats.peers.count + 1
                        } else {
                            value = max(reactionCount, readStats.peers.count)
                        }
                    }
                    return value
                }
            }
            
            var canLoadMore: Bool {
                return self.listState.canLoadMore
            }
            
            func item(at index: Int) -> EngineMessageReactionListContext.Item? {
                if index < self.mergedItems.count {
                    return self.mergedItems[index]
                } else {
                    return nil
                }
            }
        }
        
        private let context: AccountContext
        private let availableReactions: AvailableReactions?
        let reaction: String?
        private let requestUpdate: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void
        private let requestUpdateApparentHeight: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void
        private let openPeer: (PeerId) -> Void
        
        private var hasMore: Bool = false
        
        private let scrollNode: ASScrollNode
        private var ignoreScrolling: Bool = false
        private var animateIn: Bool = false
        
        private var presentationData: PresentationData?
        private var currentSize: CGSize?
        private var apparentHeight: CGFloat = 0.0
        
        private let listContext: EngineMessageReactionListContext
        private var state: ItemsState
        private var stateDisposable: Disposable?
        
        private var itemNodes: [Int: ItemNode] = [:]
        
        private var placeholderItemImage: UIImage?
        private var placeholderLayers: [Int: SimpleLayer] = [:]
        
        init(
            context: AccountContext,
            availableReactions: AvailableReactions?,
            message: EngineMessage,
            reaction: String?,
            readStats: MessageReadStats?,
            requestUpdate: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void,
            requestUpdateApparentHeight: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void,
            openPeer: @escaping (PeerId) -> Void
        ) {
            self.context = context
            self.availableReactions = availableReactions
            self.reaction = reaction
            self.requestUpdate = requestUpdate
            self.requestUpdateApparentHeight = requestUpdateApparentHeight
            self.openPeer = openPeer
            
            self.listContext = context.engine.messages.messageReactionList(message: message, reaction: reaction)
            self.state = ItemsState(listState: EngineMessageReactionListContext.State(message: message, reaction: reaction), readStats: readStats)
            
            self.scrollNode = ASScrollNode()
            self.scrollNode.canCancelAllTouchesInViews = true
            self.scrollNode.view.delaysContentTouches = false
            self.scrollNode.view.showsVerticalScrollIndicator = false
            if #available(iOS 11.0, *) {
                self.scrollNode.view.contentInsetAdjustmentBehavior = .never
            }
            self.scrollNode.clipsToBounds = false
            
            super.init()
            
            self.addSubnode(self.scrollNode)
            self.scrollNode.view.delegate = self
            
            self.clipsToBounds = true
            
            self.stateDisposable = (self.listContext.state
            |> deliverOnMainQueue).start(next: { [weak self] state in
                guard let strongSelf = self else {
                    return
                }
                let updatedState = ItemsState(listState: state, readStats: strongSelf.state.readStats)
                var animateIn = false
                if strongSelf.state.item(at: 0) == nil && updatedState.item(at: 0) != nil {
                    animateIn = true
                }
                strongSelf.state = updatedState
                strongSelf.animateIn = true
                strongSelf.requestUpdate(strongSelf, animateIn ? .animated(duration: 0.2, curve: .easeInOut) : .immediate)
                if animateIn {
                    for (_, itemNode) in strongSelf.itemNodes {
                        itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
                    }
                }
            })
        }
        
        deinit {
            self.stateDisposable?.dispose()
        }
        
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            if self.ignoreScrolling {
                return
            }
            self.updateVisibleItems(animated: false, syncronousLoad: false)
            
            if let size = self.currentSize {
                var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height
                apparentHeight = max(apparentHeight, 44.0)
                apparentHeight = min(apparentHeight, size.height + 100.0)
                if self.apparentHeight != apparentHeight {
                    self.apparentHeight = apparentHeight
                    
                    self.requestUpdateApparentHeight(self, .immediate)
                }
            }
        }
        
        private func updateVisibleItems(animated: Bool, syncronousLoad: Bool) {
            guard let size = self.currentSize else {
                return
            }
            guard let presentationData = self.presentationData else {
                return
            }
            let itemHeight: CGFloat = 44.0
            let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -180.0)
            
            var validIds = Set<Int>()
            var validPlaceholderIds = Set<Int>()
            
            let minVisibleIndex = max(0, Int(floor(visibleBounds.minY / itemHeight)))
            let maxVisibleIndex = Int(ceil(visibleBounds.maxY / itemHeight))
            
            if minVisibleIndex <= maxVisibleIndex {
                for index in minVisibleIndex ... maxVisibleIndex {
                    let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(index) * itemHeight), size: CGSize(width: size.width, height: itemHeight))
                    
                    if let item = self.state.item(at: index) {
                        validIds.insert(index)
                        
                        let itemNode: ItemNode
                        if let current = self.itemNodes[index] {
                            itemNode = current
                        } else {
                            let openPeer = self.openPeer
                            let peerId = item.peer.id
                            itemNode = ItemNode(context: self.context, availableReactions: self.availableReactions, action: {
                                openPeer(peerId)
                            })
                            self.itemNodes[index] = itemNode
                            self.scrollNode.addSubnode(itemNode)
                        }
                        
                        itemNode.update(size: itemFrame.size, presentationData: presentationData, item: item, isLast: self.state.item(at: index + 1) == nil, syncronousLoad: syncronousLoad)
                        itemNode.frame = itemFrame
                    } else if index < self.state.totalCount {
                        validPlaceholderIds.insert(index)
                        
                        let placeholderLayer: SimpleLayer
                        if let current = self.placeholderLayers[index] {
                            placeholderLayer = current
                        } else {
                            placeholderLayer = SimpleLayer()
                            if let placeholderItemImage = self.placeholderItemImage {
                                ASDisplayNodeSetResizableContents(placeholderLayer, placeholderItemImage)
                            }
                            self.placeholderLayers[index] = placeholderLayer
                            self.scrollNode.layer.addSublayer(placeholderLayer)
                        }
                        
                        placeholderLayer.frame = itemFrame
                    }
                }
            }
            
            var removeIds: [Int] = []
            for (id, itemNode) in self.itemNodes {
                if !validIds.contains(id) {
                    removeIds.append(id)
                    itemNode.removeFromSupernode()
                }
            }
            for id in removeIds {
                self.itemNodes.removeValue(forKey: id)
            }
            
            var removePlaceholderIds: [Int] = []
            for (id, placeholderLayer) in self.placeholderLayers {
                if !validPlaceholderIds.contains(id) {
                    removePlaceholderIds.append(id)
                    if animated || self.animateIn {
                        placeholderLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak placeholderLayer] _ in
                            placeholderLayer?.removeFromSuperlayer()
                        })
                    } else {
                        placeholderLayer.removeFromSuperlayer()
                    }
                }
            }
            for id in removePlaceholderIds {
                self.placeholderLayers.removeValue(forKey: id)
            }
            
            if self.state.canLoadMore && maxVisibleIndex >= self.state.listState.items.count - 16 {
                self.listContext.loadMore()
            }
        }
        
        func update(presentationData: PresentationData, constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> (height: CGFloat, apparentHeight: CGFloat) {
            let itemHeight: CGFloat = 44.0
            
            if self.presentationData?.theme !== presentationData.theme {
                let sideInset: CGFloat = 40.0
                let avatarInset: CGFloat = 12.0
                let avatarSpacing: CGFloat = 8.0
                let avatarSize: CGFloat = 28.0
                let lineHeight: CGFloat = 8.0
                
                let shimmeringForegroundColor: UIColor
                let shimmeringColor: UIColor
                if presentationData.theme.overallDarkAppearance {
                    let backgroundColor = presentationData.theme.contextMenu.backgroundColor.blitOver(presentationData.theme.list.plainBackgroundColor, alpha: 1.0)

                    shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.blitOver(backgroundColor, alpha: 0.1)
                    shimmeringColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3)
                } else {
                    shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.07)
                    shimmeringColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3)
                }
                let _ = shimmeringColor
                
                self.placeholderItemImage = generateImage(CGSize(width: avatarInset + avatarSize + avatarSpacing + lineHeight + 2.0 + sideInset, height: itemHeight), rotatedContext: { size, context in
                    context.clear(CGRect(origin: CGPoint(), size: size))
                    context.setFillColor(shimmeringForegroundColor.cgColor)
                    context.fillEllipse(in: CGRect(origin: CGPoint(x: avatarInset, y: floor((size.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)))
                    
                    context.fillEllipse(in: CGRect(origin: CGPoint(x: avatarInset + avatarSize + avatarSpacing, y: floor((size.height - lineHeight) / 2.0)), size: CGSize(width: lineHeight + 2.0, height: lineHeight)))
                })?.stretchableImage(withLeftCapWidth: Int(avatarInset + avatarSize + avatarSpacing + lineHeight / 2.0 + 1.0), topCapHeight: 0)
                
                if let placeholderItemImage = self.placeholderItemImage {
                    for (_, placeholderLayer) in self.placeholderLayers {
                        ASDisplayNodeSetResizableContents(placeholderLayer, placeholderItemImage)
                    }
                }
            }
            self.presentationData = presentationData
            
            let size = CGSize(width: constrainedSize.width, height: CGFloat(self.state.totalCount) * itemHeight)
            
            let containerSize = CGSize(width: size.width, height: min(constrainedSize.height, size.height))
            self.currentSize = containerSize
            
            self.ignoreScrolling = true
            
            if self.scrollNode.frame != CGRect(origin: CGPoint(), size: containerSize) {
                self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize)
            }
            if self.scrollNode.view.contentSize != size {
                self.scrollNode.view.contentSize = size
            }
            self.ignoreScrolling = false
            
            self.updateVisibleItems(animated: transition.isAnimated, syncronousLoad: !transition.isAnimated)
            
            self.animateIn = false
            
            var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height
            apparentHeight = max(apparentHeight, 44.0)
            apparentHeight = min(apparentHeight, containerSize.height + 100.0)
            self.apparentHeight = apparentHeight
            
            return (containerSize.height, apparentHeight)
        }
    }
    
    final class ItemsNode: ASDisplayNode, ContextControllerItemsNode, UIGestureRecognizerDelegate {
        private let context: AccountContext
        private let availableReactions: AvailableReactions?
        private let message: EngineMessage
        private let readStats: MessageReadStats?
        private let reactions: [(String?, Int)]
        private let requestUpdate: (ContainedViewLayoutTransition) -> Void
        private let requestUpdateApparentHeight: (ContainedViewLayoutTransition) -> Void
        
        private var presentationData: PresentationData
        
        private var backButtonNode: BackButtonNode?
        private var separatorNode: ASDisplayNode?
        private var tabListNode: ReactionTabListNode?
        
        private var currentTabIndex: Int = 0
        private var visibleTabNodes: [Int: ReactionsTabNode] = [:]
        
        private struct InteractiveTransitionState {
            var toIndex: Int
            var progress: CGFloat
        }
        private var interactiveTransitionState: InteractiveTransitionState?
        
        private let openPeer: (PeerId) -> Void
        
        private(set) var apparentHeight: CGFloat = 0.0
        
        init(
            context: AccountContext,
            availableReactions: AvailableReactions?,
            message: EngineMessage,
            reaction: String?,
            readStats: MessageReadStats?,
            requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
            requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void,
            back: (() -> Void)?,
            openPeer: @escaping (PeerId) -> Void
        ) {
            self.context = context
            self.availableReactions = availableReactions
            self.message = message
            self.readStats = readStats
            self.openPeer = openPeer
            self.presentationData = context.sharedContext.currentPresentationData.with({ $0 })
            
            self.requestUpdate = requestUpdate
            self.requestUpdateApparentHeight = requestUpdateApparentHeight
            
            //var requestUpdateTab: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)?
            //var requestUpdateTabApparentHeight: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)?
            
            if let back = back {
                self.backButtonNode = BackButtonNode()
                self.backButtonNode?.action = {
                    back()
                }
            }
            
            var reactions: [(String?, Int)] = []
            var totalCount: Int = 0
            if let reactionsAttribute = message._asMessage().reactionsAttribute {
                for listReaction in reactionsAttribute.reactions {
                    if reaction == nil || listReaction.value == reaction {
                        totalCount += Int(listReaction.count)
                        reactions.append((listReaction.value, Int(listReaction.count)))
                    }
                }
            }
            if reaction == nil {
                reactions.insert((nil, totalCount), at: 0)
            }
            
            if reactions.count > 2 && totalCount > 10 {
                self.tabListNode = ReactionTabListNode(context: context, availableReactions: availableReactions, reactions: reactions, message: message)
            }
            
            self.reactions = reactions
            
            super.init()
            
            if self.backButtonNode != nil || self.tabListNode != nil {
                self.separatorNode = ASDisplayNode()
            }
            
            if let backButtonNode = self.backButtonNode {
                self.addSubnode(backButtonNode)
            }
            if let tabListNode = self.tabListNode {
                self.addSubnode(tabListNode)
            }
            if let separatorNode = self.separatorNode {
                self.addSubnode(separatorNode)
            }
            
            self.tabListNode?.action = { [weak self] reaction in
                guard let strongSelf = self else {
                    return
                }
                guard let tabIndex = strongSelf.reactions.firstIndex(where: { $0.0 == reaction }) else {
                    return
                }
                guard strongSelf.currentTabIndex != tabIndex else {
                    return
                }
                strongSelf.tabListNode?.scrollToTabReaction = ReactionTabListNode.ScrollToTabReaction(value: reaction)
                strongSelf.currentTabIndex = tabIndex
                
                /*let currentTabNode = ReactionsTabNode(
                    context: context,
                    availableReactions: availableReactions,
                    message: message,
                    reaction: reaction,
                    readStats: nil,
                    requestUpdate: { tab, transition in
                        requestUpdateTab?(tab, transition)
                    },
                    requestUpdateApparentHeight: { tab, transition in
                        requestUpdateTabApparentHeight?(tab, transition)
                    },
                    openPeer: { id in
                        openPeer(id)
                    }
                )
                strongSelf.currentTabNode = currentTabNode
                strongSelf.addSubnode(currentTabNode)*/
                strongSelf.requestUpdate(.animated(duration: 0.45, curve: .spring))
            }
            
            /*requestUpdateTab = { [weak self] tab, transition in
                guard let strongSelf = self else {
                    return
                }
                if strongSelf.visibleTabNodes.contains(where: { $0.value === tab }) {
                    strongSelf.requestUpdate(transition)
                }
            }
            
            requestUpdateTabApparentHeight = { [weak self] tab, transition in
                guard let strongSelf = self else {
                    return
                }
                if strongSelf.visibleTabNodes.contains(where: { $0.value === tab }) {
                    strongSelf.requestUpdateApparentHeight(transition)
                }
            }*/
            
            let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in
                guard let strongSelf = self else {
                    return []
                }
                if strongSelf.currentTabIndex == 0 {
                    return .left
                }
                return [.left, .right]
            })
            panRecognizer.delegate = self
            self.view.addGestureRecognizer(panRecognizer)
        }
        
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            return false
        }
        
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
                return false
            }
            if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
                return true
            }
            return false
        }
        
        @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
            switch recognizer.state {
            case .began:
                break
            case .changed:
                let translation = recognizer.translation(in: self.view)
                if !self.bounds.isEmpty {
                    let progress = translation.x / self.bounds.width
                    var toIndex: Int
                    if progress < 0.0 {
                        toIndex = self.currentTabIndex + 1
                    } else {
                        toIndex = self.currentTabIndex - 1
                    }
                    toIndex = max(0, min(toIndex, self.reactions.count - 1))
                    self.interactiveTransitionState = InteractiveTransitionState(toIndex: toIndex, progress: abs(progress))
                    self.requestUpdate(.immediate)
                }
            case .cancelled, .ended:
                if let interactiveTransitionState = self.interactiveTransitionState {
                    self.interactiveTransitionState = nil
                    if interactiveTransitionState.progress >= 0.2 {
                        self.currentTabIndex = interactiveTransitionState.toIndex
                        self.tabListNode?.scrollToTabReaction = ReactionTabListNode.ScrollToTabReaction(value: self.reactions[self.currentTabIndex].0)
                    }
                    self.requestUpdate(.animated(duration: 0.45, curve: .spring))
                }
            default:
                break
            }
        }
        
        func update(presentationData: PresentationData, constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) {
            let constrainedSize = CGSize(width: min(260.0, constrainedWidth), height: maxHeight)
            
            var topContentHeight: CGFloat = 0.0
            if let backButtonNode = self.backButtonNode {
                let backButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 44.0))
                backButtonNode.update(size: backButtonFrame.size, presentationData: self.presentationData, isLast: self.tabListNode == nil)
                transition.updateFrame(node: backButtonNode, frame: backButtonFrame)
                topContentHeight += backButtonFrame.height
            }
            if let tabListNode = self.tabListNode {
                let tabListFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 44.0))
                let selectedReaction: String? = self.reactions[self.currentTabIndex].0
                tabListNode.update(size: tabListFrame.size, presentationData: self.presentationData, selectedReaction: selectedReaction, transition: transition)
                transition.updateFrame(node: tabListNode, frame: tabListFrame)
                topContentHeight += tabListFrame.height
            }
            if let separatorNode = self.separatorNode {
                let separatorFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 7.0))
                separatorNode.backgroundColor = self.presentationData.theme.contextMenu.sectionSeparatorColor
                transition.updateFrame(node: separatorNode, frame: separatorFrame)
                topContentHeight += separatorFrame.height
            }
            
            var tabLayouts: [Int: (height: CGFloat, apparentHeight: CGFloat)] = [:]
            
            var visibleIndices: [Int] = []
            visibleIndices.append(self.currentTabIndex)
            if let interactiveTransitionState = self.interactiveTransitionState {
                visibleIndices.append(interactiveTransitionState.toIndex)
            }
            
            let previousVisibleTabFrames: [(Int, CGRect)] = self.visibleTabNodes.map { key, value -> (Int, CGRect) in
                return (key, value.frame)
            }
            
            for index in visibleIndices {
                var tabTransition = transition
                let tabNode: ReactionsTabNode
                var initialReferenceFrame: CGRect?
                if let current = self.visibleTabNodes[index] {
                    tabNode = current
                } else {
                    for (previousIndex, previousFrame) in previousVisibleTabFrames {
                        if index > previousIndex {
                            initialReferenceFrame = previousFrame.offsetBy(dx: constrainedSize.width, dy: 0.0)
                        } else {
                            initialReferenceFrame = previousFrame.offsetBy(dx: -constrainedSize.width, dy: 0.0)
                        }
                        break
                    }
                    
                    tabNode = ReactionsTabNode(
                        context: self.context,
                        availableReactions: self.availableReactions,
                        message: self.message,
                        reaction: self.reactions[index].0,
                        readStats: self.reactions[index].0 == nil ? self.readStats : nil,
                        requestUpdate: { [weak self] tab, transition in
                            guard let strongSelf = self else {
                                return
                            }
                            if strongSelf.visibleTabNodes.contains(where: { $0.value === tab }) {
                                var transition = transition
                                if strongSelf.interactiveTransitionState != nil {
                                    transition = .immediate
                                }
                                strongSelf.requestUpdate(transition)
                            }
                        },
                        requestUpdateApparentHeight: { [weak self] tab, transition in
                            guard let strongSelf = self else {
                                return
                            }
                            if strongSelf.visibleTabNodes.contains(where: { $0.value === tab }) {
                                var transition = transition
                                if strongSelf.interactiveTransitionState != nil {
                                    transition = .immediate
                                }
                                strongSelf.requestUpdateApparentHeight(transition)
                            }
                        },
                        openPeer: self.openPeer
                    )
                    self.addSubnode(tabNode)
                    self.visibleTabNodes[index] = tabNode
                    tabTransition = .immediate
                }
                
                let tabLayout = tabNode.update(presentationData: presentationData, constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - topContentHeight), transition: tabTransition)
                tabLayouts[index] = tabLayout
                let currentFractionalTabIndex: CGFloat
                if let interactiveTransitionState = self.interactiveTransitionState {
                    currentFractionalTabIndex = CGFloat(self.currentTabIndex) * (1.0 - interactiveTransitionState.progress) + CGFloat(interactiveTransitionState.toIndex) * interactiveTransitionState.progress
                } else {
                    currentFractionalTabIndex = CGFloat(self.currentTabIndex)
                }
                let xOffset: CGFloat = (CGFloat(index) - currentFractionalTabIndex) * constrainedSize.width
                let tabFrame = CGRect(origin: CGPoint(x: xOffset, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: tabLayout.height + 100.0))
                tabTransition.updateFrame(node: tabNode, frame: tabFrame)
                if let initialReferenceFrame = initialReferenceFrame {
                    transition.animatePositionAdditive(node: tabNode, offset: CGPoint(x: initialReferenceFrame.minX - tabFrame.minX, y: 0.0))
                }
            }
            
            var removedIndices: [Int] = []
            for (index, tabNode) in self.visibleTabNodes {
                if tabLayouts[index] == nil {
                    removedIndices.append(index)
                    
                    var xOffset: CGFloat
                    if index > self.currentTabIndex {
                        xOffset = constrainedSize.width
                    } else {
                        xOffset = -constrainedSize.width
                    }
                    transition.updateFrame(node: tabNode, frame: CGRect(origin: CGPoint(x: xOffset, y: tabNode.frame.minY), size: tabNode.bounds.size), completion: { [weak tabNode] _ in
                        tabNode?.removeFromSupernode()
                    })
                }
            }
            for index in removedIndices {
                self.visibleTabNodes.removeValue(forKey: index)
            }
            
            /*var currentTabTransition = transition
            if self.currentTabNode.bounds.isEmpty {
                currentTabTransition = .immediate
            }
            let currentTabLayout = self.currentTabNode.update(presentationData: presentationData, constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - topContentHeight), transition: currentTabTransition)
            currentTabTransition.updateFrame(node: self.currentTabNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: currentTabLayout.size.width, height: currentTabLayout.size.height + 100.0)))
            
            if let dismissedTabNode = self.dismissedTabNode {
                self.dismissedTabNode = nil
                if let previousIndex = self.reactions.firstIndex(where: { $0.0 == dismissedTabNode.reaction }), let currentIndex = self.reactions.firstIndex(where: { $0.0 == self.currentTabNode.reaction }) {
                    let offset = previousIndex < currentIndex ? currentTabLayout.size.width : -currentTabLayout.size.width
                    transition.updateFrame(node: dismissedTabNode, frame: dismissedTabNode.frame.offsetBy(dx: -offset, dy: 0.0), completion: { [weak dismissedTabNode] _ in
                        dismissedTabNode?.removeFromSupernode()
                    })
                    transition.animatePositionAdditive(node: self.currentTabNode, offset: CGPoint(x: offset, y: 0.0))
                } else {
                    dismissedTabNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak dismissedTabNode] _ in
                        dismissedTabNode?.removeFromSupernode()
                    })
                    self.currentTabNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
                }
            }*/
            
            var contentSize = CGSize(width: constrainedSize.width, height: topContentHeight)
            var apparentHeight = topContentHeight
            
            if let interactiveTransitionState = self.interactiveTransitionState, let fromTabLayout = tabLayouts[self.currentTabIndex], let toTabLayout = tabLayouts[interactiveTransitionState.toIndex] {
                let megedTabLayoutHeight = fromTabLayout.height * (1.0 - interactiveTransitionState.progress) + toTabLayout.height * interactiveTransitionState.progress
                let megedTabLayoutApparentHeight = fromTabLayout.apparentHeight * (1.0 - interactiveTransitionState.progress) + toTabLayout.apparentHeight * interactiveTransitionState.progress
                
                contentSize.height += megedTabLayoutHeight
                apparentHeight += megedTabLayoutApparentHeight
            } else if let tabLayout = tabLayouts[self.currentTabIndex] {
                contentSize.height += tabLayout.height
                apparentHeight += tabLayout.apparentHeight
            }
            
            return (contentSize, apparentHeight)
        }
    }
    
    let context: AccountContext
    let availableReactions: AvailableReactions?
    let message: EngineMessage
    let reaction: String?
    let readStats: MessageReadStats?
    let back: (() -> Void)?
    let openPeer: (PeerId) -> Void
    
    public init(
        context: AccountContext,
        availableReactions: AvailableReactions?,
        message: EngineMessage,
        reaction: String?,
        readStats: MessageReadStats?,
        back: (() -> Void)?,
        openPeer: @escaping (PeerId) -> Void
    ) {
        self.context = context
        self.availableReactions = availableReactions
        self.message = message
        self.reaction = reaction
        self.readStats = readStats
        self.back = back
        self.openPeer = openPeer
    }
    
    public func node(
        requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
        requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
    ) -> ContextControllerItemsNode {
        return ItemsNode(
            context: self.context,
            availableReactions: self.availableReactions,
            message: self.message,
            reaction: self.reaction,
            readStats: self.readStats,
            requestUpdate: requestUpdate,
            requestUpdateApparentHeight: requestUpdateApparentHeight,
            back: self.back,
            openPeer: self.openPeer
        )
    }
}