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

struct ChatMessageItemWidthFill {
    var compactInset: CGFloat
    var compactWidthBoundary: CGFloat
    var freeMaximumFillFactor: CGFloat
    
    func widthFor(_ width: CGFloat) -> CGFloat {
        if width <= self.compactWidthBoundary {
            return max(1.0, width - self.compactInset)
        } else {
            return max(1.0, floor(width * self.freeMaximumFillFactor))
        }
    }
}

struct ChatMessageItemBubbleLayoutConstants {
    var edgeInset: CGFloat
    var defaultSpacing: CGFloat
    var mergedSpacing: CGFloat
    var maximumWidthFill: ChatMessageItemWidthFill
    var minimumSize: CGSize
    var contentInsets: UIEdgeInsets
    var borderInset: CGFloat
    var strokeInsets: UIEdgeInsets
}

struct ChatMessageItemTextLayoutConstants {
    var bubbleInsets: UIEdgeInsets
}

struct ChatMessageItemImageLayoutConstants {
    var bubbleInsets: UIEdgeInsets
    var statusInsets: UIEdgeInsets
    var defaultCornerRadius: CGFloat
    var mergedCornerRadius: CGFloat
    var contentMergedCornerRadius: CGFloat
    var maxDimensions: CGSize
    var minDimensions: CGSize
}

struct ChatMessageItemVideoLayoutConstants {
    var maxHorizontalHeight: CGFloat
    var maxVerticalHeight: CGFloat
}

struct ChatMessageItemInstantVideoConstants {
    var insets: UIEdgeInsets
    var dimensions: CGSize
}

struct ChatMessageItemFileLayoutConstants {
    var bubbleInsets: UIEdgeInsets
}

struct ChatMessageItemWallpaperLayoutConstants {
    var maxTextWidth: CGFloat
}

struct ChatMessageItemLayoutConstants {
    var avatarDiameter: CGFloat
    var timestampHeaderHeight: CGFloat
    
    var bubble: ChatMessageItemBubbleLayoutConstants
    var image: ChatMessageItemImageLayoutConstants
    var video: ChatMessageItemVideoLayoutConstants
    var text: ChatMessageItemTextLayoutConstants
    var file: ChatMessageItemFileLayoutConstants
    var instantVideo: ChatMessageItemInstantVideoConstants
    var wallpapers: ChatMessageItemWallpaperLayoutConstants
    
    static var `default`: ChatMessageItemLayoutConstants {
        return self.compact
    }
    
    fileprivate static var compact: ChatMessageItemLayoutConstants {
        let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.85), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0))
        let text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 12.0, bottom: 6.0 - UIScreenPixel, right: 12.0))
        let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 0.0, maxDimensions: CGSize(width: 300.0, height: 380.0), minDimensions: CGSize(width: 170.0, height: 74.0))
        let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0)
        let file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0))
        let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 212.0, height: 212.0))
        let wallpapers = ChatMessageItemWallpaperLayoutConstants(maxTextWidth: 180.0)
        
        return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers)
    }
    
    fileprivate static var regular: ChatMessageItemLayoutConstants {
        let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.65), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0))
        let text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 12.0, bottom: 6.0 - UIScreenPixel, right: 12.0))
        let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 440.0, height: 440.0), minDimensions: CGSize(width: 170.0, height: 74.0))
        let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0)
        let file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0))
        let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 240.0, height: 240.0))
        let wallpapers = ChatMessageItemWallpaperLayoutConstants(maxTextWidth: 180.0)
        
        return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers)
    }
}

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 = 9.0
    let maxInset: CGFloat = 12.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
}

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

enum ChatMessagePeekPreviewContent {
    case media(Media)
    case url(ASDisplayNode, CGRect, String, 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
}()

enum ChatMessageAccessibilityCustomActionType {
    case reply
    case options
}

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

final class ChatMessageAccessibilityData {
    let label: String?
    let value: String?
    let hint: String?
    let traits: UIAccessibilityTraits
    let customActions: [ChatMessageAccessibilityCustomAction]?
    let singleUrl: String?
    
    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 class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
    let layoutConstants = (ChatMessageItemLayoutConstants.compact, ChatMessageItemLayoutConstants.regular)
    
    var item: ChatMessageItem?
    var accessibilityData: ChatMessageAccessibilityData?
    var safeInsets = UIEdgeInsets()
    
    var awaitingAppliedReaction: (MessageReaction.Reaction?, () -> Void)?
    
    public required convenience init() {
        self.init(layerBacked: false)
    }
    
    public init(layerBacked: Bool) {
        super.init(layerBacked: layerBacked, dynamicBounce: true, rotated: true)
        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 public func reuse() {
        super.reuse()
        
        self.item = nil
        self.frame = CGRect()
    }
    
    func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) {
        self.item = item
    }
    
    func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) {
        self.accessibilityData = accessibilityData
    }
    
    override public 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)
            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)
        }
    }
    
    func cancelInsertionAnimations() {
    }
    
    override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
        if short {
            //self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height, to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
        } else {
            self.transitionOffset = -self.bounds.size.height * 1.6
            self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp)
        }
    }
    
    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
                
            })
        }
    }
    
    func transitionNode(id: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
        return nil
    }
    
    func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? {
        return nil
    }
    
    func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? {
        return nil
    }
    
    func updateHiddenMedia() {
    }
    
    func updateSelectionState(animated: Bool) {
    }
    
    func updateSearchTextHighlightState() {
    }
    
    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
    }
    
    func updateAutomaticMediaDownloadSettings() {
    }
    
    func updateStickerSettings(forceStopAnimations: Bool) {
    }
    
    func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
        return nil
    }
    
    override public func headers() -> [ListViewItemHeader]? {
        if let item = self.item {
            return item.headers
        } else {
            return nil
        }
    }
    
    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(url, concealed, nil, nil)
                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).start(next: { peer in
                        if let peer = peer {
                            item.controllerInteraction.openPeer(peer, .info, nil, .default)
                        }
                    })
                case let .openWebView(url, simple):
                    item.controllerInteraction.openWebView(button.title, url, simple, .generic)
                case .requestPeer:
                    break
            }
        }
    }
    
    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
            }
        }
    }
    
    func openMessageContextMenu() {
    }
    
    public func targetReactionView(value: MessageReaction.Reaction) -> UIView? {
        return nil
    }
    
    public func targetForStoryTransition(id: StoryId) -> UIView? {
        return nil
    }
    
    func getStatusNode() -> ASDisplayNode? {
        return nil
    }

    private var attachedAvatarNodeOffset: CGFloat = 0.0

    override public 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)
                }
            }
        }
    }

    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))
            }
        }
    }
    
    func unreadMessageRangeUpdated() {
    }
    
    public func contentFrame() -> CGRect {
        return self.bounds
    }
}