import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import AccountContext
import LocalizedPeerData
import ContextUI
import ChatListUI
import TelegramPresentationData
import SwiftSignalKit
import ChatControllerInteraction
import ChatMessageItemCommon
import TextFormat
import ChatMessageItem

public func chatMessageItemLayoutConstants(_ constants: (ChatMessageItemLayoutConstants, ChatMessageItemLayoutConstants), params: ListViewItemLayoutParams, presentationData: ChatPresentationData) -> ChatMessageItemLayoutConstants {
    var result: ChatMessageItemLayoutConstants
    if params.width > 680.0 {
        result = constants.1
    } else {
        result = constants.0
    }
    result.image.defaultCornerRadius = presentationData.chatBubbleCorners.mainRadius
    result.image.mergedCornerRadius = (presentationData.chatBubbleCorners.mergeBubbleCorners && result.image.defaultCornerRadius >= 10.0) ?  presentationData.chatBubbleCorners.auxiliaryRadius : presentationData.chatBubbleCorners.mainRadius
    let minRadius: CGFloat = 4.0
    let maxRadius: CGFloat = 16.0
    let radiusTransition = (presentationData.chatBubbleCorners.mainRadius - minRadius) / (maxRadius - minRadius)
    let minInset: CGFloat = result.text.bubbleInsets.left
    let maxInset: CGFloat = 11.0
    let textInset: CGFloat = min(maxInset, ceil(maxInset * radiusTransition + minInset * (1.0 - radiusTransition)))
    result.text.bubbleInsets.left = textInset
    result.text.bubbleInsets.right = textInset
    result.instantVideo.dimensions = params.width > 320.0 ? constants.1.instantVideo.dimensions : constants.0.instantVideo.dimensions
    return result
}

public enum ChatMessageItemBottomNeighbor {
    case none
    case merged(semi: Bool)
}

private let voiceMessageDurationFormatter: DateComponentsFormatter = {
    let formatter = DateComponentsFormatter()
    formatter.unitsStyle = .spellOut
    formatter.allowedUnits = [.minute, .second]
    return formatter
}()

private let musicDurationFormatter: DateComponentsFormatter = {
    let formatter = DateComponentsFormatter()
    formatter.unitsStyle = .spellOut
    formatter.allowedUnits = [.hour, .minute, .second]
    return formatter
}()

private let fileSizeFormatter: ByteCountFormatter = {
    let formatter = ByteCountFormatter()
    formatter.allowsNonnumericFormatting = true
    return formatter
}()

public enum ChatMessageAccessibilityCustomActionType {
    case reply
    case options
}

public final class ChatMessageAccessibilityCustomAction: UIAccessibilityCustomAction {
    public let action: ChatMessageAccessibilityCustomActionType
    
    public init(name: String, target: Any?, selector: Selector, action: ChatMessageAccessibilityCustomActionType) {
        self.action = action
        
        super.init(name: name, target: target, selector: selector)
    }
}

public final class ChatMessageAccessibilityData {
    public let label: String?
    public let value: String?
    public let hint: String?
    public let traits: UIAccessibilityTraits
    public let customActions: [ChatMessageAccessibilityCustomAction]?
    public let singleUrl: String?
    
    public init(item: ChatMessageItem, isSelected: Bool?) {
        var hint: String?
        var traits: UIAccessibilityTraits = []
        var singleUrl: String?
        
        var customActions: [ChatMessageAccessibilityCustomAction] = []
        
        let isIncoming = item.message.effectivelyIncoming(item.context.account.peerId)
        var announceIncomingAuthors = false
        if let peer = item.message.peers[item.message.id.peerId] {
            if peer is TelegramGroup {
                announceIncomingAuthors = true
            } else if let channel = peer as? TelegramChannel, case .group = channel.info {
                announceIncomingAuthors = true
            }
        }
        
        let dataForMessage: (Message, Bool) -> (String, String) = { message, isReply -> (String, String) in
            var label: String = ""
            var value: String = ""
            
            if let chatPeer = message.peers[item.message.id.peerId] {
                let authorName = message.author.flatMap(EnginePeer.init)?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
                
                let (_, _, messageText, _, _) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, contentSettings: item.context.currentContentSettings.with { $0 }, messages: [EngineMessage(message)], chatPeer: EngineRenderedPeer(peer: EnginePeer(chatPeer)), accountPeerId: item.context.account.peerId)
                
                var text = messageText
                
                loop: for media in message.media {
                    if let _ = media as? TelegramMediaImage {
                        traits.insert(.image)
                        if isIncoming {
                            if announceIncomingAuthors, let authorName = authorName {
                                label = item.presentationData.strings.VoiceOver_Chat_PhotoFrom(authorName).string
                            } else {
                                label = item.presentationData.strings.VoiceOver_Chat_Photo
                            }
                        } else {
                            label = item.presentationData.strings.VoiceOver_Chat_YourPhoto
                        }
                        text = ""
                        if !message.text.isEmpty {
                            text.append("\n")
                            
                            text.append(item.presentationData.strings.VoiceOver_Chat_Caption(message.text).string)
                        }
                    } else if let file = media as? TelegramMediaFile {
                        var isSpecialFile = false
                        
                        let isVideo = file.isInstantVideo
                        
                        for attribute in file.attributes {
                            switch attribute {
                                case let .Sticker(displayText, _, _):
                                    isSpecialFile = true
                                    text = displayText
                                    if file.mimeType == "application/x-tgsticker" {
                                        if isIncoming {
                                            if announceIncomingAuthors, let authorName = authorName {
                                                label = item.presentationData.strings.VoiceOver_Chat_AnimatedStickerFrom(authorName).string
                                            } else {
                                                label = item.presentationData.strings.VoiceOver_Chat_AnimatedSticker
                                            }
                                        } else {
                                            label = item.presentationData.strings.VoiceOver_Chat_YourAnimatedSticker
                                        }
                                    } else {
                                        if isIncoming {
                                            if announceIncomingAuthors, let authorName = authorName {
                                                label = item.presentationData.strings.VoiceOver_Chat_StickerFrom(authorName).string
                                            } else {
                                                label = item.presentationData.strings.VoiceOver_Chat_Sticker
                                            }
                                        } else {
                                            label = item.presentationData.strings.VoiceOver_Chat_YourSticker
                                        }
                                    }
                                case let .Audio(isVoice, duration, title, performer, _):
                                    if isVideo {
                                        continue
                                    }
                                    isSpecialFile = true
                                    if isSelected == nil {
                                        hint = item.presentationData.strings.VoiceOver_Chat_PlayHint
                                    }
                                    traits.insert(.startsMediaSession)
                                    if isVoice {
                                        let durationString = voiceMessageDurationFormatter.string(from: Double(duration)) ?? ""
                                        if isIncoming {
                                            if announceIncomingAuthors, let authorName = authorName {
                                                label = item.presentationData.strings.VoiceOver_Chat_VoiceMessageFrom(authorName).string
                                            } else {
                                                label = item.presentationData.strings.VoiceOver_Chat_VoiceMessage
                                            }
                                        } else {
                                            label = item.presentationData.strings.VoiceOver_Chat_YourVoiceMessage
                                        }
                                        text = item.presentationData.strings.VoiceOver_Chat_Duration(durationString).string
                                    } else {
                                        let durationString = musicDurationFormatter.string(from: Double(duration)) ?? ""
                                        if isIncoming {
                                            if announceIncomingAuthors, let authorName = authorName {
                                                label = item.presentationData.strings.VoiceOver_Chat_MusicFrom(authorName).string
                                            } else {
                                                label = item.presentationData.strings.VoiceOver_Chat_Music
                                            }
                                        } else {
                                            label = item.presentationData.strings.VoiceOver_Chat_YourMusic
                                        }
                                        let performer = performer ?? "Unknown"
                                        let title = title ?? "Unknown"
                                        
                                        text = item.presentationData.strings.VoiceOver_Chat_MusicTitle(title, performer).string
                                        text.append(item.presentationData.strings.VoiceOver_Chat_Duration(durationString).string)
                                    }
                                case let .Video(duration, _, flags, _):
                                    isSpecialFile = true
                                    if isSelected == nil {
                                        hint = item.presentationData.strings.VoiceOver_Chat_PlayHint
                                    }
                                    traits.insert(.startsMediaSession)
                                    let durationString = voiceMessageDurationFormatter.string(from: Double(duration)) ?? ""
                                    if flags.contains(.instantRoundVideo) {
                                        if isIncoming {
                                            if announceIncomingAuthors, let authorName = authorName {
                                                label = item.presentationData.strings.VoiceOver_Chat_VideoMessageFrom(authorName).string
                                            } else {
                                                label = item.presentationData.strings.VoiceOver_Chat_VideoMessage
                                            }
                                        } else {
                                            label = item.presentationData.strings.VoiceOver_Chat_YourVideoMessage
                                        }
                                    } else {
                                        if isIncoming {
                                            if announceIncomingAuthors, let authorName = authorName {
                                                label = item.presentationData.strings.VoiceOver_Chat_VideoFrom(authorName).string
                                            } else {
                                                label = item.presentationData.strings.VoiceOver_Chat_Video
                                            }
                                        } else {
                                            label = item.presentationData.strings.VoiceOver_Chat_YourVideo
                                        }
                                    }
                                    text = item.presentationData.strings.VoiceOver_Chat_Duration(durationString).string
                                default:
                                    break
                            }
                        }
                        if !isSpecialFile {
                            if isSelected == nil {
                                hint = item.presentationData.strings.VoiceOver_Chat_OpenHint
                            }
                            let sizeString = fileSizeFormatter.string(fromByteCount: Int64(file.size ?? 0))
                            if isIncoming {
                                if announceIncomingAuthors, let authorName = authorName {
                                    label = item.presentationData.strings.VoiceOver_Chat_FileFrom(authorName).string
                                } else {
                                    label = item.presentationData.strings.VoiceOver_Chat_File
                                }
                            } else {
                                label = item.presentationData.strings.VoiceOver_Chat_YourFile
                            }
                            text = "\(file.fileName ?? ""). "
                            text.append(item.presentationData.strings.VoiceOver_Chat_Size(sizeString).string)
                        }
                        if !message.text.isEmpty {
                            text.append("\n")
                            text.append(item.presentationData.strings.VoiceOver_Chat_Caption(message.text).string)
                        }
                        break loop
                    } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
                        var contentText = item.presentationData.strings.VoiceOver_Chat_PagePreview + ". "
                        if let title = content.title, !title.isEmpty {
                            contentText.append(item.presentationData.strings.VoiceOver_Chat_Title(title).string)
                            contentText.append(". ")
                        }
                        if let text = content.text, !text.isEmpty {
                            contentText.append(text)
                        }
                        text = "\(message.text)\n\(contentText)"
                    } else if let contact = media as? TelegramMediaContact {
                        if isIncoming {
                            if announceIncomingAuthors, let authorName = authorName {
                                label = item.presentationData.strings.VoiceOver_Chat_ContactFrom(authorName).string
                            } else {
                                label = item.presentationData.strings.VoiceOver_Chat_Contact
                            }
                        } else {
                            label = item.presentationData.strings.VoiceOver_Chat_YourContact
                        }
                        var displayName = ""
                        if !contact.firstName.isEmpty {
                            displayName.append(contact.firstName)
                        }
                        if !contact.lastName.isEmpty {
                            if !displayName.isEmpty {
                                displayName.append(" ")
                            }
                            displayName.append(contact.lastName)
                        }
                        var phoneNumbersString = ""
                        var phoneNumberCount = 0
                        var emailAddressesString = ""
                        var emailAddressCount = 0
                        var organizationString = ""
                        if let vCard = contact.vCardData, let vCardData = vCard.data(using: .utf8), let contactData = DeviceContactExtendedData(vcard: vCardData) {
                            if displayName.isEmpty && !contactData.organization.isEmpty {
                                displayName = contactData.organization
                            }
                            if !contactData.basicData.phoneNumbers.isEmpty {
                                for phone in contactData.basicData.phoneNumbers {
                                    if !phoneNumbersString.isEmpty {
                                        phoneNumbersString.append(", ")
                                    }
                                    for c in phone.value {
                                        phoneNumbersString.append(c)
                                        phoneNumbersString.append(" ")
                                    }
                                    phoneNumberCount += 1
                                }
                            } else {
                                for c in contact.phoneNumber {
                                    phoneNumbersString.append(c)
                                    phoneNumbersString.append(" ")
                                }
                                phoneNumberCount += 1
                            }
                            
                            for email in contactData.emailAddresses {
                                if !emailAddressesString.isEmpty {
                                    emailAddressesString.append(", ")
                                }
                                emailAddressesString.append("\(email.value)")
                                emailAddressCount += 1
                            }
                            if !contactData.organization.isEmpty && displayName != contactData.organization {
                                organizationString = contactData.organization
                            }
                        } else {
                            phoneNumbersString.append("\(contact.phoneNumber)")
                        }
                        text = "\(displayName)."
                        if !phoneNumbersString.isEmpty {
                            if phoneNumberCount > 1 {
                                text.append(item.presentationData.strings.VoiceOver_Chat_ContactPhoneNumberCount(Int32(phoneNumberCount)))
                                text.append(": ")
                            } else {
                                text.append(item.presentationData.strings.VoiceOver_Chat_ContactPhoneNumber)
                            }
                            text.append("\(phoneNumbersString). ")
                        }
                        if !emailAddressesString.isEmpty {
                            if emailAddressCount > 1 {
                                text.append(item.presentationData.strings.VoiceOver_Chat_ContactEmailCount(Int32(emailAddressCount)))
                                text.append(": ")
                            } else {
                                text.append(item.presentationData.strings.VoiceOver_Chat_ContactEmail)
                                text.append(": ")
                            }
                            text.append("\(emailAddressesString). ")
                        }
                        if !organizationString.isEmpty {
                            text.append(item.presentationData.strings.VoiceOver_Chat_ContactOrganization(organizationString).string)
                            text.append(".")
                        }
                    } else if let poll = media as? TelegramMediaPoll {
                        if isIncoming {
                            if announceIncomingAuthors, let authorName = authorName {
                                label = item.presentationData.strings.VoiceOver_Chat_AnonymousPollFrom(authorName).string
                            } else {
                                label = item.presentationData.strings.VoiceOver_Chat_AnonymousPoll
                            }
                        } else {
                            label = item.presentationData.strings.VoiceOver_Chat_YourAnonymousPoll
                        }
                        
                        var optionVoterCount: [Int: Int32] = [:]
                        var maxOptionVoterCount: Int32 = 0
                        var totalVoterCount: Int32 = 0
                        let voters: [TelegramMediaPollOptionVoters]?
                        if poll.isClosed {
                            voters = poll.results.voters ?? []
                        } else {
                            voters = poll.results.voters
                        }
                        var selectedOptionId: Data?
                        if let voters = voters, let totalVoters = poll.results.totalVoters {
                            var didVote = false
                            for voter in voters {
                                if voter.selected {
                                    didVote = true
                                    selectedOptionId = voter.opaqueIdentifier
                                }
                            }
                            totalVoterCount = totalVoters
                            if didVote || poll.isClosed {
                                for i in 0 ..< poll.options.count {
                                    inner: for optionVoters in voters {
                                        if optionVoters.opaqueIdentifier == poll.options[i].opaqueIdentifier {
                                            optionVoterCount[i] = optionVoters.count
                                            maxOptionVoterCount = max(maxOptionVoterCount, optionVoters.count)
                                            break inner
                                        }
                                    }
                                }
                            }
                        }
                        
                        var optionVoterCounts: [Int]
                        if totalVoterCount != 0 {
                            optionVoterCounts = countNicePercent(votes: (0 ..< poll.options.count).map({ Int(optionVoterCount[$0] ?? 0) }), total: Int(totalVoterCount))
                        } else {
                            optionVoterCounts = Array(repeating: 0, count: poll.options.count)
                        }
                        
                        text = item.presentationData.strings.VoiceOver_Chat_Title(poll.text).string
                        text.append(". ")
                        
                        text.append(item.presentationData.strings.VoiceOver_Chat_PollOptionCount(Int32(poll.options.count)))
                        text.append(": ")
                        var optionsText = ""
                        for i in 0 ..< poll.options.count {
                            let option = poll.options[i]
                            
                            if !optionsText.isEmpty {
                                optionsText.append(", ")
                            }
                            optionsText.append(option.text)
                            if let selectedOptionId = selectedOptionId, selectedOptionId == option.opaqueIdentifier {
                                optionsText.append(", ")
                                optionsText.append(item.presentationData.strings.VoiceOver_Chat_OptionSelected)
                            }
                            
                            if let _ = optionVoterCount[i] {
                                if maxOptionVoterCount != 0 && totalVoterCount != 0 {
                                    optionsText.append(", \(optionVoterCounts[i])%")
                                }
                            }
                        }
                        text.append("\(optionsText). ")
                        if totalVoterCount != 0 {
                            text.append(item.presentationData.strings.VoiceOver_Chat_PollVotes(Int32(totalVoterCount)))
                        } else {
                            text.append(item.presentationData.strings.VoiceOver_Chat_PollNoVotes)
                        }
                        if poll.isClosed {
                            text.append(item.presentationData.strings.VoiceOver_Chat_PollFinalResults)
                        }
                    }
                }
                
                var result = ""
                
                if let isSelected = isSelected {
                    if isSelected {
                        result += item.presentationData.strings.VoiceOver_Chat_Selected
                        result += "\n"
                    }
                    traits.insert(.startsMediaSession)
                }
                
                result += "\(text)"
                
                let dateString = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(message.timestamp)), dateStyle: .medium, timeStyle: .short)
                
                result += "\n\(dateString)"
                if !isIncoming && !isReply {
                    result += "\n"
                    if item.sending {
                        result += item.presentationData.strings.VoiceOver_Chat_Sending
                    } else if item.failed {
                        result += item.presentationData.strings.VoiceOver_Chat_Failed
                    } else {
                        if item.read {
                            if announceIncomingAuthors {
                                result += item.presentationData.strings.VoiceOver_Chat_SeenByRecipients
                            } else {
                                result += item.presentationData.strings.VoiceOver_Chat_SeenByRecipient
                            }
                        }
                        for attribute in message.attributes {
                            if let attribute = attribute as? ConsumableContentMessageAttribute {
                                if !attribute.consumed {
                                    if announceIncomingAuthors {
                                        result += item.presentationData.strings.VoiceOver_Chat_NotPlayedByRecipients
                                    } else {
                                        result += item.presentationData.strings.VoiceOver_Chat_NotPlayedByRecipient
                                    }
                                } else {
                                    if announceIncomingAuthors {
                                        result += item.presentationData.strings.VoiceOver_Chat_PlayedByRecipients
                                    } else {
                                        result += item.presentationData.strings.VoiceOver_Chat_PlayedByRecipient
                                    }
                                }
                            }
                        }
                    }
                }
                value = result
            } else {
                value = ""
            }
            
            if label.isEmpty {
                if let author = message.author {
                    if isIncoming {
                        label = EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
                    } else {
                        label = item.presentationData.strings.VoiceOver_Chat_YourMessage
                    }
                } else {
                    label = item.presentationData.strings.VoiceOver_Chat_Message
                }
            }
            
            return (label, value)
        }
        
        var (label, value) = dataForMessage(item.message, false)
        var replyValue: String?
        
        for attribute in item.message.attributes {
            if let attribute = attribute as? TextEntitiesMessageAttribute {
                var hasUrls = false
                loop: for entity in attribute.entities {
                    switch entity.type {
                        case .Url:
                            if hasUrls {
                                singleUrl = nil
                                break loop
                            } else {
                                if let range = Range<String.Index>(NSRange(location: entity.range.lowerBound, length: entity.range.count), in: item.message.text) {
                                    singleUrl = String(item.message.text[range])
                                    hasUrls = true
                                }
                            }
                        case let .TextUrl(url):
                            if hasUrls {
                                singleUrl = nil
                                break loop
                            } else {
                                singleUrl = url
                                hasUrls = true
                            }
                        default:
                            break
                    }
                }
            } else if let attribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[attribute.messageId] {
                var replyLabel: String
                if replyMessage.flags.contains(.Incoming) {
                    if let author = replyMessage.author {
                        replyLabel = item.presentationData.strings.VoiceOver_Chat_ReplyFrom(EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).string
                    } else {
                        replyLabel = item.presentationData.strings.VoiceOver_Chat_Reply
                    }
                } else {
                    replyLabel = item.presentationData.strings.VoiceOver_Chat_ReplyToYourMessage
                }
                
                let (_, replyMessageValue) = dataForMessage(replyMessage, true)
                replyValue = replyMessageValue
                
                label = "\(replyLabel) . \(label)"
            }
        }
        
        if hint == nil && singleUrl != nil {
            hint = item.presentationData.strings.VoiceOver_Chat_OpenLinkHint
        }
        
        if let forwardInfo = item.message.forwardInfo {
            let forwardLabel: String
            if let author = forwardInfo.author, author.id == item.context.account.peerId {
                forwardLabel = item.presentationData.strings.VoiceOver_Chat_ForwardedFromYou
            } else {
                let peerString: String
                if let peer = forwardInfo.author {
                    if let authorName = forwardInfo.authorSignature {
                        peerString = "\(EnginePeer(peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)) (\(authorName))"
                    } else {
                        peerString = EnginePeer(peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
                    }
                } else if let authorName = forwardInfo.authorSignature {
                    peerString = authorName
                } else {
                    peerString = ""
                }
                forwardLabel = item.presentationData.strings.VoiceOver_Chat_ForwardedFrom(peerString).string
            }
            label = "\(forwardLabel). \(label)"
        }
        
        if isSelected == nil {
            var canReply = item.controllerInteraction.canSetupReply(item.message) == .reply
            for media in item.content.firstMessage.media {
                if let _ = media as? TelegramMediaExpiredContent {
                    canReply = false
                }
                else if let media = media as? TelegramMediaAction {
                    if case .phoneCall = media.action {
                    } else {
                        canReply = false
                    }
                }
            }
            
            if canReply {
                customActions.append(ChatMessageAccessibilityCustomAction(name: item.presentationData.strings.VoiceOver_MessageContextReply, target: nil, selector: #selector(self.noop), action: .reply))
            }
            customActions.append(ChatMessageAccessibilityCustomAction(name: item.presentationData.strings.VoiceOver_MessageContextOpenMessageMenu, target: nil, selector: #selector(self.noop), action: .options))
        }
        
        if let replyValue {
            value = "\(value). \(item.presentationData.strings.VoiceOver_Chat_ReplyingToMessage(replyValue).string)"
        }
        
        self.label = label
        self.value = value
        self.hint = hint
        self.traits = traits
        self.customActions = customActions.isEmpty ? nil : customActions
        self.singleUrl = singleUrl
    }
    
    @objc private func noop() {
    }
}

public enum InternalBubbleTapAction {
    public struct Action {
        public var action: () -> Void
        public var contextMenuOnLongPress: Bool
        
        public init(_ action: @escaping () -> Void, contextMenuOnLongPress: Bool = false) {
            self.action = action
            self.contextMenuOnLongPress = contextMenuOnLongPress
        }
    }
    
    public struct OpenContextMenu {
        public var tapMessage: Message
        public var selectAll: Bool
        public var subFrame: CGRect
        public var disableDefaultPressAnimation: Bool
        
        public init(tapMessage: Message, selectAll: Bool, subFrame: CGRect, disableDefaultPressAnimation: Bool = false) {
            self.tapMessage = tapMessage
            self.selectAll = selectAll
            self.subFrame = subFrame
            self.disableDefaultPressAnimation = disableDefaultPressAnimation
        }
    }
    
    case action(Action)
    case optionalAction(() -> Void)
    case openContextMenu(OpenContextMenu)
}

open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
    public let layoutConstants = (ChatMessageItemLayoutConstants.compact, ChatMessageItemLayoutConstants.regular)
    
    open var item: ChatMessageItem?
    open var accessibilityData: ChatMessageAccessibilityData?
    open var safeInsets = UIEdgeInsets()
    
    open var awaitingAppliedReaction: (MessageReaction.Reaction?, () -> Void)?
    
    public required init(rotated: Bool) {
        super.init(layerBacked: false, dynamicBounce: true, rotated: rotated)
        if rotated {
            self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
        }
    }

    required public init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override open func reuse() {
        super.reuse()
        
        self.item = nil
        self.frame = CGRect()
    }
    
    open func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) {
        self.item = item
    }
    
    open func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) {
        self.accessibilityData = accessibilityData
    }
    
    override open func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
        if let item = item as? ChatMessageItem {
            let doLayout = self.asyncLayout()
            let merged = item.mergedWithItems(top: previousItem, bottom: nextItem, isRotated: item.controllerInteraction.chatIsRotated)
            let (layout, apply) = doLayout(item, params, merged.top, merged.bottom, merged.dateAtBottom)
            self.contentSize = layout.contentSize
            self.insets = layout.insets
            apply(.None, ListViewItemApply(isOnScreen: false), false)
        }
    }
    
    open func cancelInsertionAnimations() {
    }
    
    override open func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
        if options.short {
            //self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height, to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
        } else {
            self.transitionOffset = options.invertOffsetDirection ? self.bounds.size.height * 1.4 : -self.bounds.size.height * 1.6
            self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp)
        }
    }
    
    open func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) {
        return { _, _, _, _, _ in
            return (ListViewItemNodeLayout(contentSize: CGSize(width: 32.0, height: 32.0), insets: UIEdgeInsets()), { _, _, _ in
                
            })
        }
    }
    
    open func transitionNode(id: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
        return nil
    }
    
    open func makeContentSnapshot() -> (UIImage, CGRect)? {
        return nil
    }
    
    open func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? {
        return nil
    }
    
    open func updateHiddenMedia() {
    }
    
    open func updateSelectionState(animated: Bool) {
    }
    
    open func updateSearchTextHighlightState() {
    }
    
    open func updateHighlightedState(animated: Bool) {
        var isHighlightedInOverlay = false
        if let item = self.item, let contextHighlightedState = item.controllerInteraction.contextHighlightedState {
            switch item.content {
                case let .message(message, _, _, _, _):
                    if contextHighlightedState.messageStableId == message.stableId {
                        isHighlightedInOverlay = true
                    }
                case let .group(messages):
                    for (message, _, _, _, _) in messages {
                        if contextHighlightedState.messageStableId == message.stableId {
                            isHighlightedInOverlay = true
                            break
                        }
                    }
            }
        }
        self.isHighlightedInOverlay = isHighlightedInOverlay
    }
    
    open func updateAutomaticMediaDownloadSettings() {
    }
    
    open func updateStickerSettings(forceStopAnimations: Bool) {
    }
    
    open func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
        return nil
    }
    
    override open func headers() -> [ListViewItemHeader]? {
        if let item = self.item {
            return item.headers
        } else {
            return nil
        }
    }
    
    open func performMessageButtonAction(button: ReplyMarkupButton) {
        if let item = self.item {
            switch button.action {
                case .text:
                    item.controllerInteraction.sendMessage(button.title)
                case let .url(url):
                    var concealed = true
                    if url.hasPrefix("tg://") {
                        concealed = false
                    }
                item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: concealed))
                case .requestMap:
                    item.controllerInteraction.shareCurrentLocation()
                case .requestPhone:
                    item.controllerInteraction.shareAccountContact()
                case .openWebApp:
                    item.controllerInteraction.requestMessageActionCallback(item.message.id, nil, true, false)
                case let .callback(requiresPassword, data):
                    item.controllerInteraction.requestMessageActionCallback(item.message.id, data, false, requiresPassword)
                case let .switchInline(samePeer, query, peerTypes):
                    var botPeer: Peer?
                    
                    var found = false
                    for attribute in item.message.attributes {
                        if let attribute = attribute as? InlineBotMessageAttribute {
                            if let peerId = attribute.peerId {
                                botPeer = item.message.peers[peerId]
                                found = true
                            }
                        }
                    }
                    if !found {
                        botPeer = item.message.author
                    }
                    
                    var peerId: PeerId?
                    if samePeer {
                        peerId = item.message.id.peerId
                    }
                    if let botPeer = botPeer, let addressName = botPeer.addressName {
                        item.controllerInteraction.activateSwitchInline(peerId, "@\(addressName) \(query)", peerTypes)
                    }
                case .payment:
                    item.controllerInteraction.openCheckoutOrReceipt(item.message.id)
                case let .urlAuth(url, buttonId):
                    item.controllerInteraction.requestMessageActionUrlAuth(url, .message(id: item.message.id, buttonId: buttonId))
                case .setupPoll:
                    break
                case let .openUserProfile(peerId):
                    let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
                    |> deliverOnMainQueue).startStandalone(next: { peer in
                        if let peer = peer {
                            item.controllerInteraction.openPeer(peer, .info(nil), nil, .default)
                        }
                    })
                case let .openWebView(url, simple):
                    item.controllerInteraction.openWebView(button.title, url, simple, .generic)
                case .requestPeer:
                    break
            }
        }
    }
    
    open func presentMessageButtonContextMenu(button: ReplyMarkupButton) {
        if let item = self.item {
            switch button.action {
                case let .url(url):
                    item.controllerInteraction.longTap(.url(url), item.message)
                default:
                    break
            }
        }
    }
    
    open func openMessageContextMenu() {
    }
    
    open func targetReactionView(value: MessageReaction.Reaction) -> UIView? {
        return nil
    }
    
    open func targetForStoryTransition(id: StoryId) -> UIView? {
        return nil
    }
    
    open func getStatusNode() -> ASDisplayNode? {
        return nil
    }

    private var attachedAvatarNodeOffset: CGFloat = 0.0

    override open func attachedHeaderNodesUpdated() {
        if !self.attachedAvatarNodeOffset.isZero {
            self.updateAttachedAvatarNodeOffset(offset: self.attachedAvatarNodeOffset, transition: .immediate)
        } else {
            for headerNode in self.attachedHeaderNodes {
                if let headerNode = headerNode as? ChatMessageAvatarHeaderNode {
                    headerNode.updateSelectionState(animated: false)
                }
            }
        }
    }

    open func updateAttachedAvatarNodeOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
        self.attachedAvatarNodeOffset = offset
        for headerNode in self.attachedHeaderNodes {
            if let headerNode = headerNode as? ChatMessageAvatarHeaderNode {
                transition.updateSublayerTransformOffset(layer: headerNode.layer, offset: CGPoint(x: offset, y: 0.0))
            }
        }
    }
    
    open func unreadMessageRangeUpdated() {
    }
    
    open func contentFrame() -> CGRect {
        return self.bounds
    }
    
    open func playMessageEffect() {
    }
}