import Foundation
import UIKit
import Postbox
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramStringFormatting
import TextFormat
import ChatMessageDateAndStatusNode
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import MessageInlineBlockBackgroundView
import TextSelectionNode
import Geocoding
import UrlEscaping

private func generateMaskImage() -> UIImage? {
    return generateImage(CGSize(width: 140, height: 30), rotatedContext: { size, context in
        context.clear(CGRect(origin: .zero, size: size))
        
        context.setFillColor(UIColor.white.cgColor)
        context.fill(CGRect(origin: .zero, size: size))
        
        var locations: [CGFloat] = [0.0, 0.5, 1.0]
        let colors: [CGColor] = [UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.withAlphaComponent(0.0).cgColor]
        
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
        
        context.setBlendMode(.copy)
        context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 8.0), size: CGSize(width: 130.0, height: 22.0)))
        context.drawLinearGradient(gradient, start: CGPoint(x: 10.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
    })?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 22.0, right: 130.0))
}

public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode {
    private var backgroundView: MessageInlineBlockBackgroundView?
    
    private var titleNode: TextNode
    private var titleBadgeLabel: TextNode
    private var titleBadgeButton: HighlightTrackingButtonNode?
    private let textClippingNode: ASDisplayNode
    private let textNode: TextNode
    private let additionalTextNode: TextNode
    private var linkHighlightingNode: LinkHighlightingNode?
    private var textSelectionNode: TextSelectionNode?
    
    private let lineNode: ASDisplayNode
    
    private var maskView: UIImageView?
    private var maskOverlayView: UIView?
    
    private var expandIcon: ASImageNode
    
    private let statusNode: ChatMessageDateAndStatusNode
    
    private var isExpanded: Bool = false
    private var appliedIsExpanded: Bool = false
    
    private var countryName: String?
    
    required public init() {
        self.titleNode = TextNode()
        self.titleBadgeLabel = TextNode()
        self.textClippingNode = ASDisplayNode()
        self.textNode = TextNode()
        self.additionalTextNode = TextNode()
        self.expandIcon = ASImageNode()
        self.statusNode = ChatMessageDateAndStatusNode()
        self.lineNode = ASDisplayNode()

        super.init()
        
        self.textClippingNode.clipsToBounds = true
        self.addSubnode(self.textClippingNode)
        
        self.titleNode.isUserInteractionEnabled = false
        self.titleNode.contentMode = .topLeft
        self.titleNode.contentsScale = UIScreenScale
        self.titleNode.displaysAsynchronously = false
        self.addSubnode(self.titleNode)
        
        self.textNode.isUserInteractionEnabled = false
        self.textNode.contentMode = .topLeft
        self.textNode.contentsScale = UIScreenScale
        self.textNode.displaysAsynchronously = false
        self.textClippingNode.addSubnode(self.textNode)
        
        self.additionalTextNode.isUserInteractionEnabled = false
        self.additionalTextNode.contentMode = .topLeft
        self.additionalTextNode.contentsScale = UIScreenScale
        self.additionalTextNode.displaysAsynchronously = false
        self.textClippingNode.addSubnode(self.additionalTextNode)
        
        self.textClippingNode.addSubnode(self.lineNode)
        
        self.titleBadgeLabel.isUserInteractionEnabled = false
        self.titleBadgeLabel.contentMode = .topLeft
        self.titleBadgeLabel.contentsScale = UIScreenScale
        self.titleBadgeLabel.displaysAsynchronously = false
        self.addSubnode(self.titleBadgeLabel)
        
        self.expandIcon.displaysAsynchronously = false
        self.addSubnode(self.expandIcon)
    }
    
    required public init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
        
    public override func didLoad() {
        self.maskView = UIImageView()
        
        let maskOverlayView = UIView()
        maskOverlayView.alpha = 0.0
        maskOverlayView.backgroundColor = .white
        self.maskOverlayView = maskOverlayView
        
        self.maskView?.addSubview(maskOverlayView)
    }
    
    @objc private func badgePressed() {
        guard let item = self.item, let countryName = self.countryName else {
            return
        }
        
        item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_FactCheck_Description(countryName).string, true, self.titleBadgeButton, nil)
    }
    
    @objc private func expandPressed() {
        self.isExpanded = !self.isExpanded
        guard let item = self.item else{
            return
        }
        let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false)
    }
    
    public override func willUpdateIsExtractedToContextPreview(_ value: Bool) {
        if !value {
            if let textSelectionNode = self.textSelectionNode {
                self.textSelectionNode = nil
                textSelectionNode.highlightAreaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
                textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in
                    textSelectionNode?.highlightAreaNode.removeFromSupernode()
                    textSelectionNode?.removeFromSupernode()
                })
            }
        }
    }
    
    public override func updateIsExtractedToContextPreview(_ value: Bool) {
        if value {
            if self.textSelectionNode == nil, let item = self.item, let rootNode = item.controllerInteraction.chatControllerNode() {
                let selectionColor: UIColor = item.presentationData.theme.theme.chat.message.incoming.textSelectionColor
                let knobColor: UIColor = item.presentationData.theme.theme.chat.message.incoming.textSelectionKnobColor
                
                let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor, isDark: item.presentationData.theme.theme.overallDarkAppearance), strings: item.presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in
                    self?.updateIsTextSelectionActive?(value)
                }, present: { [weak self] c, a in
                    self?.item?.controllerInteraction.presentGlobalOverlayController(c, a)
                }, rootNode: { [weak rootNode] in
                    return rootNode
                }, performAction: { [weak self] text, action in
                    guard let strongSelf = self, let item = strongSelf.item else {
                        return
                    }
                    item.controllerInteraction.performTextSelectionAction(item.message, true, text, action)
                })
                textSelectionNode.enableQuote = false
                self.textSelectionNode = textSelectionNode
                self.addSubnode(textSelectionNode)
                self.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textClippingNode)
                textSelectionNode.frame = self.textClippingNode.view.convert(self.textNode.frame, to: self.view)
                textSelectionNode.highlightAreaNode.frame = textSelectionNode.frame
            }
        } else {
            if let textSelectionNode = self.textSelectionNode {
                self.textSelectionNode = nil
                self.updateIsTextSelectionActive?(false)
                textSelectionNode.highlightAreaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
                textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in
                    textSelectionNode?.highlightAreaNode.removeFromSupernode()
                    textSelectionNode?.removeFromSupernode()
                })
            }
        }
    }
    
    public override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
        if let titleBadgeButton = self.titleBadgeButton, titleBadgeButton.frame.contains(point) {
            return ChatMessageBubbleContentTapAction(content: .ignore)
        }
        
        if self.statusNode.supernode != nil, let _ = self.statusNode.hitTest(self.view.convert(point, to: self.statusNode.view), with: nil) {
            return ChatMessageBubbleContentTapAction(content: .ignore)
        }
        
        let textNodeFrame = self.textClippingNode.frame
        if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
            if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
                return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: false)))
            } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
                return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false))
            } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
                return ChatMessageBubbleContentTapAction(content: .textMention(peerName))
            } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
                return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand))
            } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
                return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag))
            }
        }
        if let backgroundView = self.backgroundView, backgroundView.frame.contains(point), case .tap = gesture {
            return ChatMessageBubbleContentTapAction(content: .custom({ [weak self] in
                self?.expandPressed()
            }), hasLongTapAction: false)
        }
        return ChatMessageBubbleContentTapAction(content: .none)
    }
    
    public override func updateTouchesAtPoint(_ point: CGPoint?) {
        guard let item = self.item else {
            return
        }
        var rects: [CGRect]?
        if let point = point {
            let textNodeFrame = self.textClippingNode.frame
            if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
                let possibleNames: [String] = [
                    TelegramTextAttributes.URL,
                    TelegramTextAttributes.PeerMention,
                    TelegramTextAttributes.PeerTextMention,
                    TelegramTextAttributes.BotCommand,
                    TelegramTextAttributes.Hashtag,
                    TelegramTextAttributes.BankCard
                ]
                for name in possibleNames {
                    if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
                        rects = self.textNode.attributeRects(name: name, at: index)
                        break
                    }
                }
            }
        }
        
        if let rects {
            let linkHighlightingNode: LinkHighlightingNode
            if let current = self.linkHighlightingNode {
                linkHighlightingNode = current
            } else {
                linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor)
                self.linkHighlightingNode = linkHighlightingNode
                self.insertSubnode(linkHighlightingNode, belowSubnode: self.textClippingNode)
            }
            linkHighlightingNode.frame = self.textClippingNode.frame
            linkHighlightingNode.updateRects(rects)
        } else if let linkHighlightingNode = self.linkHighlightingNode {
            self.linkHighlightingNode = nil
            linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
                linkHighlightingNode?.removeFromSupernode()
            })
        }
    }
    
    override public 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 titleLayout = TextNode.asyncLayout(self.titleNode)
        let titleBadgeLayout = TextNode.asyncLayout(self.titleBadgeLabel)
        let textLayout = TextNode.asyncLayout(self.textNode)
        let additionalTextLayout = TextNode.asyncLayout(self.additionalTextNode)
        let measureTextLayout = TextNode.asyncLayout(nil)
        let statusLayout = self.statusNode.asyncLayout()
        
        let currentIsExpanded = self.isExpanded
        let currentCountryName = self.countryName
        
        return { item, layoutConstants, _, _, _, _ in
            let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
            
            return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
                let message = item.message
                
                let incoming = item.message.effectivelyIncoming(item.context.account.peerId)
                
                let maxTextWidth = CGFloat.greatestFiniteMagnitude
                
                let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
                let textConstrainedSize = CGSize(width: min(maxTextWidth, constrainedSize.width - (horizontalInset - 2.0) * 2.0), height: constrainedSize.height)
                
                var edited = false
                if item.attributes.updatingMedia != nil {
                    edited = true
                }
                var viewCount: Int?
                var rawText = ""
                var rawEntities: [MessageTextEntity] = []
                var dateReplies = 0
                var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message)
                if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) {
                    dateReactionsAndPeers = ([], [])
                }
                for attribute in item.message.attributes {
                    if let attribute = attribute as? EditedMessageAttribute {
                        edited = !attribute.isHidden
                    } else if let attribute = attribute as? ViewCountMessageAttribute {
                        viewCount = attribute.count
                    } else if let attribute = attribute as? FactCheckMessageAttribute, case let .Loaded(text, entities, _) = attribute.content {
                        rawText = text
                        rawEntities = entities
                    } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation {
                        if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info {
                            dateReplies = Int(attribute.count)
                        }
                    }
                }
                
                let dateFormat: MessageTimestampStatusFormat
                if item.presentationData.isPreview {
                    dateFormat = .full
                } else if let subject = item.associatedData.subject, case .messageOptions = subject {
                    dateFormat = .minimal
                } else {
                    dateFormat = .regular
                }
                let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData)
                
                let statusType: ChatMessageDateAndStatusType?
                if case .customChatContents = item.associatedData.subject {
                    statusType = nil
                } else {
                    switch position {
                    case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
                        if incoming {
                            statusType = .BubbleIncoming
                        } else {
                            if message.flags.contains(.Failed) {
                                statusType = .BubbleOutgoing(.Failed)
                            } else if message.flags.isSending && !message.isSentOrAcknowledged {
                                statusType = .BubbleOutgoing(.Sending)
                            } else {
                                statusType = .BubbleOutgoing(.Sent(read: item.read))
                            }
                        }
                    default:
                        statusType = nil
                    }
                }
                                
                let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
                                
                let fontSize = floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)
                let textFont = Font.regular(fontSize)
                let textBoldFont = Font.semibold(fontSize)
                let textItalicFont = Font.italic(fontSize)
                let textBoldItalicFont = Font.semiboldItalic(fontSize)
                let textFixedFont = Font.regular(fontSize)
                let textBlockQuoteFont = Font.regular(fontSize)
                let badgeFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 11.0 / 17.0))
                
                let attributedText = stringWithAppliedEntities(rawText, entities: rawEntities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil)
                
                let textInsets = UIEdgeInsets(top: 2.0, left: 0.0, bottom: 5.0, right: 0.0)
                
                var backgroundInsets = UIEdgeInsets()
                backgroundInsets.left += layoutConstants.text.bubbleInsets.left
                backgroundInsets.right += layoutConstants.text.bubbleInsets.right
                
                let mainColor = messageTheme.scamColor
                
                let (titleLayout, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Message_FactCheck, font: textBoldFont, textColor: mainColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: mainColor))
                
                let titleBadgePadding: CGFloat = 5.0
                let titleBadgeSpacing: CGFloat = 5.0
                let titleBadgeString = NSAttributedString(string: item.presentationData.strings.Message_FactCheck_WhatIsThis, font: badgeFont, textColor: mainColor)
                let (titleBadgeLayout, titleBadgeApply) = titleBadgeLayout(TextNodeLayoutArguments(attributedString: titleBadgeString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize))
                
                let countryName: String
                if let currentCountryName {
                    countryName = currentCountryName
                } else {
                    if let attribute = item.message.factCheckAttribute, case let .Loaded(_, _, countryIdValue) = attribute.content {
                        let locale = localeWithStrings(item.presentationData.strings)
                        countryName = displayCountryName(countryIdValue, locale: locale)
                    } else {
                        countryName = ""
                    }
                }
                
                let finalAttributedText = stringWithAppliedEntities(rawText, entities: rawEntities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil) as! NSMutableAttributedString
                finalAttributedText.append(NSAttributedString(string: "__", font: textFont, textColor: .clear))
                
                let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: finalAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor))
                
                let additionalAttributedText = NSMutableAttributedString(string: item.presentationData.strings.Conversation_FactCheck_InnerDescription(countryName).string, font: badgeFont, textColor: mainColor)
                additionalAttributedText.append(NSAttributedString(string: "__", font: badgeFont, textColor: .clear))
                
                let (additionalTextLayout, additionalTextApply) = additionalTextLayout(TextNodeLayoutArguments(attributedString: additionalAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor))
            
                var canExpand = false
                var clippedTextHeight: CGFloat = textLayout.size.height
                if textLayout.numberOfLines > 4 {
                    let (measuredTextLayout, _) = measureTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 4, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor))
                    canExpand = true
                    
                    if !currentIsExpanded {
                        clippedTextHeight = measuredTextLayout.size.height
                    }
                }
                
                var titleFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: titleLayout.size)
                titleFrame = titleFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left * 2.0 - 2.0, dy: layoutConstants.text.bubbleInsets.top - 3.0)
                var titleFrameWithoutInsets = CGRect(origin: CGPoint(x: titleFrame.origin.x + textInsets.left, y: titleFrame.origin.y + textInsets.top), size: CGSize(width: titleFrame.width - textInsets.left - textInsets.right, height: titleFrame.height - textInsets.top - textInsets.bottom))
                titleFrameWithoutInsets = titleFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
                
                let topInset: CGFloat = 5.0
                let textSpacing: CGFloat = 3.0
                
                let textFrame = CGRect(origin: CGPoint(x: titleFrame.origin.x, y: -textInsets.top + titleFrameWithoutInsets.height + textSpacing), size: textLayout.size)
                var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: clippedTextHeight - textInsets.top - textInsets.bottom))
                textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
                
                let additionalTextFrame = CGRect(origin: CGPoint(x: titleFrame.origin.x, y: textFrame.maxY), size: additionalTextLayout.size)
                var additionalTextFrameWithoutInsets = CGRect(origin: CGPoint(x: additionalTextFrame.origin.x + textInsets.left, y: additionalTextFrame.origin.y + textInsets.top), size: CGSize(width: additionalTextFrame.width - textInsets.left - textInsets.right, height: additionalTextFrame.height - textInsets.top - textInsets.bottom))
                additionalTextFrameWithoutInsets = additionalTextFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
                
                var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
                if let statusType = statusType {
                    var isReplyThread = false
                    if case .replyThread = item.chatLocation {
                        isReplyThread = true
                    }
                    
                    statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
                        context: item.context,
                        presentationData: item.presentationData,
                        edited: edited && !item.presentationData.isPreview,
                        impressionCount: !item.presentationData.isPreview ? viewCount : nil,
                        dateText: dateText,
                        type: statusType,
                        layoutInput: .trailingContent(contentWidth: nil, reactionSettings: item.presentationData.isPreview ? nil : ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions), preferAdditionalInset: false)),
                        constrainedSize: textConstrainedSize,
                        availableReactions: item.associatedData.availableReactions,
                        savedMessageTags: item.associatedData.savedMessageTags,
                        reactions: dateReactionsAndPeers.reactions,
                        reactionPeers: dateReactionsAndPeers.peers,
                        displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
                        areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId),
                        messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects),
                        replyCount: dateReplies,
                        isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
                        hasAutoremove: item.message.isSelfExpiring,
                        canViewReactionList: canViewMessageReactionList(message: item.topMessage),
                        animationCache: item.controllerInteraction.presentationContext.animationCache,
                        animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
                    ))
                }
                
                var suggestedBoundingWidth: CGFloat = max(textFrameWithoutInsets.width, titleFrameWithoutInsets.width + titleBadgeLayout.size.width + titleBadgeSpacing + titleBadgePadding * 2.0)
                if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
                    suggestedBoundingWidth = max(suggestedBoundingWidth, statusSuggestedWidthAndContinue.0)
                }
                suggestedBoundingWidth = max(suggestedBoundingWidth, additionalTextFrameWithoutInsets.width)
                let sideInsets = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
                suggestedBoundingWidth += (sideInsets - 2.0) * 2.0
                
                return (suggestedBoundingWidth, { boundingWidth in
                    var boundingSize: CGSize
                    
                    let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right)
                    
                    var contentHeight = titleFrameWithoutInsets.height + textSpacing + textFrameWithoutInsets.size.height
                    if canExpand && !currentIsExpanded {
                    } else {
                        contentHeight += textSpacing * 2.0 + 1.0 + additionalTextFrameWithoutInsets.height
                    }
                    contentHeight += textSpacing
                    boundingSize = CGSize(width: boundingWidth, height: topInset + contentHeight - textSpacing)
                    if let statusSizeAndApply = statusSizeAndApply {
                        boundingSize.height += statusSizeAndApply.0.height
                    }
                    boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
                    boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
                    
                    return (boundingSize, { [weak self] animation, _, info in
                        if let strongSelf = self {
                            info?.setInvertOffsetDirection()
                            
                            let isFirstTime = strongSelf.item == nil
                            let themeUpdated = strongSelf.item?.presentationData.theme.theme !== item.presentationData.theme.theme
                            
                            strongSelf.item = item
                            strongSelf.countryName = countryName
     
                            let backgroundView: MessageInlineBlockBackgroundView
                            if let current = strongSelf.backgroundView {
                                backgroundView = current
                            } else {
                                backgroundView = MessageInlineBlockBackgroundView()
                                strongSelf.view.insertSubview(backgroundView, at: 0)
                                strongSelf.backgroundView = backgroundView
                            }
                            
                            if themeUpdated {
                                strongSelf.lineNode.backgroundColor = mainColor.withAlphaComponent(0.15)
                            }
                            
                            var isExpandedUpdated = false
                            if strongSelf.appliedIsExpanded != currentIsExpanded {
                                strongSelf.appliedIsExpanded = currentIsExpanded
                                info?.setInvertOffsetDirection()
                                isExpandedUpdated = true
                                
                                animation.transition.updateTransformRotation(node: strongSelf.expandIcon, angle: currentIsExpanded ? .pi : 0.0)
                                if let maskOverlayView = strongSelf.maskOverlayView {
                                    animation.transition.updateAlpha(layer: maskOverlayView.layer, alpha: currentIsExpanded ? 1.0 : 0.0)
                                }
                            }
                            
                            let cachedLayout = strongSelf.textNode.cachedLayout
                            
                            if case .System = animation, !isExpandedUpdated {
                                if let cachedLayout = cachedLayout {
                                    if !cachedLayout.areLinesEqual(to: textLayout) {
                                        if let textContents = strongSelf.textNode.contents {
                                            let fadeNode = ASDisplayNode()
                                            fadeNode.displaysAsynchronously = false
                                            fadeNode.contents = textContents
                                            fadeNode.frame = strongSelf.textNode.frame
                                            fadeNode.isLayerBacked = true
                                            strongSelf.textClippingNode.addSubnode(fadeNode)
                                            fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in
                                                fadeNode?.removeFromSupernode()
                                            })
                                            strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
                                        }
                                    }
                                }
                            }
                            
                            if themeUpdated {
                                strongSelf.expandIcon.image = generateImage(CGSize(width: 15.0, height: 9.0), rotatedContext: { size, context in
                                    context.clear(CGRect(origin: CGPoint(), size: size))
                                    context.setStrokeColor(mainColor.cgColor)
                                    context.setLineWidth(2.0 - UIScreenPixel)
                                    context.setLineCap(.round)
                                    context.setLineJoin(.round)
                                    context.beginPath()
                                    context.move(to: CGPoint(x: 1.0 + UIScreenPixel, y: 1.0))
                                    context.addLine(to: CGPoint(x: size.width / 2.0, y: size.height - 2.0))
                                    context.addLine(to: CGPoint(x: size.width - 1.0 - UIScreenPixel, y: 1.0))
                                    context.strokePath()
                                })
                            }
                            
                            let _ = titleApply()
                            strongSelf.titleNode.frame = titleFrame.offsetBy(dx: 0.0, dy: topInset)
                            let _ = titleBadgeApply()

                            let _ = textApply()
                            strongSelf.textNode.frame = CGRect(origin: .zero, size: textFrame.size)
                            
                            let _ = additionalTextApply()
                            strongSelf.additionalTextNode.frame = CGRect(origin: CGPoint(x: 0.0, y: textFrame.height - textInsets.bottom + textSpacing + 1.0), size: additionalTextFrame.size)
                            
                            let clippingTextFrame = CGRect(origin: textFrame.origin.offsetBy(dx: 0.0, dy: topInset), size: CGSize(width: boundingWidth, height: contentHeight - titleFrame.height + textSpacing))
                             
                            var titleLineWidth: CGFloat = 0.0
                            if let firstLine = titleLayout.linesRects().first {
                                titleLineWidth = firstLine.width
                            } else {
                                titleLineWidth = titleFrame.width
                            }
                            
                            let titleBadgeFrame = CGRect(origin: CGPoint(x: titleFrame.minX + titleLineWidth + titleBadgeSpacing + titleBadgePadding, y: topInset + floorToScreenPixels(titleFrame.midY - titleBadgeLayout.size.height / 2.0) - 1.0), size: titleBadgeLayout.size)
                            let badgeBackgroundFrame = titleBadgeFrame.insetBy(dx: -titleBadgePadding, dy: -1.0 + UIScreenPixel)
                            
                            strongSelf.titleBadgeLabel.frame = titleBadgeFrame
                            
                            let titleBadgeButton: HighlightTrackingButtonNode
                            if let current = strongSelf.titleBadgeButton {
                                titleBadgeButton = current
                                titleBadgeButton.bounds = CGRect(origin: .zero, size: badgeBackgroundFrame.size)
                                animation.animator.updatePosition(layer: titleBadgeButton.layer, position: badgeBackgroundFrame.center, completion: nil)
                            } else {
                                titleBadgeButton = HighlightTrackingButtonNode()
                                titleBadgeButton.addTarget(self, action: #selector(strongSelf.badgePressed), forControlEvents: .touchUpInside)
                                titleBadgeButton.frame = badgeBackgroundFrame
                                titleBadgeButton.highligthedChanged = { [weak self, weak titleBadgeButton] highlighted in
                                    if let strongSelf = self, let titleBadgeButton {
                                        if highlighted {
                                            titleBadgeButton.layer.removeAnimation(forKey: "opacity")
                                            titleBadgeButton.alpha = 0.4
                                            strongSelf.titleBadgeLabel.layer.removeAnimation(forKey: "opacity")
                                            strongSelf.titleBadgeLabel.alpha = 0.4
                                        } else {
                                            titleBadgeButton.alpha = 1.0
                                            titleBadgeButton.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
                                            strongSelf.titleBadgeLabel.alpha = 1.0
                                            strongSelf.titleBadgeLabel.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
                                        }
                                    }
                                }
                                strongSelf.titleBadgeButton = titleBadgeButton
                                strongSelf.addSubnode(titleBadgeButton)
                            }
                            
                            titleBadgeButton.isHidden = item.presentationData.isPreview
                            strongSelf.titleBadgeLabel.isHidden = item.presentationData.isPreview
                            
                            if themeUpdated || titleBadgeButton.backgroundImage(for: .normal) == nil {
                                titleBadgeButton.setBackgroundImage(generateFilledCircleImage(diameter: badgeBackgroundFrame.height, color: mainColor.withMultipliedAlpha(0.1))?.stretchableImage(withLeftCapWidth: Int(badgeBackgroundFrame.height / 2), topCapHeight: Int(badgeBackgroundFrame.height / 2)), for: .normal)
                            }
                            
                            let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top + topInset), size: CGSize(width: boundingWidth - backgroundInsets.left - backgroundInsets.right, height: contentHeight))
                            
                            if isFirstTime {
                                strongSelf.textClippingNode.frame = clippingTextFrame
                            } else {
                                animation.animator.updateFrame(layer: strongSelf.textClippingNode.layer, frame: clippingTextFrame, completion: nil)
                            }
                            if let maskView = strongSelf.maskView, let maskOverlayView = strongSelf.maskOverlayView {
                                animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil)
                                animation.animator.updateFrame(layer: maskOverlayView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil)
                            }
                            
                            if isFirstTime {
                                backgroundView.frame = backgroundFrame
                            } else {
                                animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil)
                            }
                            backgroundView.update(size: backgroundFrame.size, isTransparent: false, primaryColor: mainColor, secondaryColor: nil, thirdColor: nil, backgroundColor: nil, pattern: nil, patternTopRightPosition: nil, animation: isFirstTime ? .None : animation)
                            
                            animation.animator.updateFrame(layer: strongSelf.lineNode.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: textFrame.height - textSpacing + 1.0), size: CGSize(width: backgroundFrame.width - 9.0 - 6.0, height: 1.0 - UIScreenPixel)), completion: nil)
                            
                            if canExpand {
                                let wasHidden = strongSelf.expandIcon.isHidden
                                strongSelf.expandIcon.isHidden = false
                                if strongSelf.maskView?.image == nil {
                                    strongSelf.maskView?.image = generateMaskImage()
                                }
                                strongSelf.textClippingNode.view.mask = strongSelf.maskView
                                
                                var expandIconFrame: CGRect = .zero
                                if let icon = strongSelf.expandIcon.image {
                                    expandIconFrame = CGRect(origin: CGPoint(x: boundingWidth - icon.size.width - 19.0, y: backgroundFrame.maxY - icon.size.height - 6.0), size: icon.size)
                                    if wasHidden || isFirstTime {
                                        strongSelf.expandIcon.position = expandIconFrame.center
                                    } else {
                                        animation.animator.updatePosition(layer: strongSelf.expandIcon.layer, position: expandIconFrame.center, completion: nil)
                                    }
                                    strongSelf.expandIcon.bounds = CGRect(origin: .zero, size: expandIconFrame.size)
                                }
                            } else {
                                strongSelf.expandIcon.isHidden = true
                                strongSelf.textClippingNode.view.mask = nil
                            }
                            
                            if let textSelectionNode = strongSelf.textSelectionNode {
                                let shouldUpdateLayout = textSelectionNode.frame.size != textFrame.size
                                textSelectionNode.frame = strongSelf.textClippingNode.view.convert(strongSelf.textNode.frame, to: strongSelf.view)
                                textSelectionNode.highlightAreaNode.frame = textSelectionNode.frame
                                
                                if shouldUpdateLayout {
                                    textSelectionNode.updateLayout()
                                }
                            }
                            
                            if let statusSizeAndApply = statusSizeAndApply {
                                strongSelf.statusNode.reactionSelected = { [weak strongSelf] _, value, sourceView in
                                    guard let strongSelf, let item = strongSelf.item else {
                                        return
                                    }
                                    item.controllerInteraction.updateMessageReaction(item.topMessage, .reaction(value), false, sourceView)
                                }
                                strongSelf.statusNode.openReactionPreview = { [weak strongSelf] gesture, sourceNode, value in
                                    guard let strongSelf, let item = strongSelf.item else {
                                        gesture?.cancel()
                                        return
                                    }
                                    
                                    item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value)
                                }
                                
                                let statusFrame = CGRect(origin: CGPoint(x: boundingWidth - layoutConstants.text.bubbleInsets.right - statusSizeAndApply.0.width, y: backgroundFrame.maxY + 4.0), size: statusSizeAndApply.0)
                                if isFirstTime {
                                    strongSelf.statusNode.frame = statusFrame
                                } else {
                                    animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: statusFrame, completion: nil)
                                }
                                
                                if strongSelf.statusNode.supernode == nil {
                                    strongSelf.addSubnode(strongSelf.statusNode)
                                    statusSizeAndApply.1(.None)
                                } else {
                                    statusSizeAndApply.1(animation)
                                }
                            } else if strongSelf.statusNode.supernode != nil {
                                strongSelf.statusNode.removeFromSupernode()
                            }
                        }
                    })
                })
            })
        }
    }
    
    override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
        self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
        self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
    }
    
    override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
        self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
        self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
    }
    
    override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
        self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
        self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
    }
}