import Foundation

public struct ChatListEntryMessageTagSummaryKey: Hashable {
    public var tag: MessageTags
    public var actionType: PendingMessageActionType
    
    public init(tag: MessageTags, actionType: PendingMessageActionType) {
        self.tag = tag
        self.actionType = actionType
    }
}

public struct ChatListEntryMessageTagSummaryComponent: Equatable {
    public let namespace: MessageId.Namespace
    
    public init(namespace: MessageId.Namespace) {
        self.namespace = namespace
    }
}

public struct ChatListEntryPendingMessageActionsSummaryComponent: Equatable {
    public let namespace: MessageId.Namespace
    
    public init(namespace: MessageId.Namespace) {
        self.namespace = namespace
    }
}

public struct ChatListEntrySummaryComponents: Equatable {
    public struct Component: Equatable {
        public let tagSummary: ChatListEntryMessageTagSummaryComponent?
        public let actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent?
        
        public init(tagSummary: ChatListEntryMessageTagSummaryComponent? = nil, actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent? = nil) {
            self.tagSummary = tagSummary
            self.actionsSummary = actionsSummary
        }
    }
    public var components: [ChatListEntryMessageTagSummaryKey: Component]
    
    public init(components: [ChatListEntryMessageTagSummaryKey: Component] = [:]) {
        self.components = components
    }
}

public struct ChatListMessageTagSummaryInfo: Equatable {
    public let tagSummaryCount: Int32?
    public let actionsSummaryCount: Int32?
    
    public init(tagSummaryCount: Int32? = nil, actionsSummaryCount: Int32? = nil) {
        self.tagSummaryCount = tagSummaryCount
        self.actionsSummaryCount = actionsSummaryCount
    }
    
    public static func ==(lhs: ChatListMessageTagSummaryInfo, rhs: ChatListMessageTagSummaryInfo) -> Bool {
        return lhs.tagSummaryCount == rhs.tagSummaryCount && lhs.actionsSummaryCount == rhs.actionsSummaryCount
    }
}

public final class ChatListGroupReferencePeer: Equatable {
    public let peer: RenderedPeer
    public let isUnread: Bool
    
    init(peer: RenderedPeer, isUnread: Bool) {
        self.peer = peer
        self.isUnread = isUnread
    }
    
    public static func ==(lhs: ChatListGroupReferencePeer, rhs: ChatListGroupReferencePeer) -> Bool {
        if lhs.peer != rhs.peer {
            return false
        }
        if lhs.isUnread != rhs.isUnread {
            return false
        }
        return true
    }
}

public struct ChatListGroupReferenceEntry: Equatable {
    public let groupId: PeerGroupId
    public let message: Message?
    public let renderedPeers: [ChatListGroupReferencePeer]
    public let unreadState: PeerGroupUnreadCountersCombinedSummary
    
    public static func ==(lhs: ChatListGroupReferenceEntry, rhs: ChatListGroupReferenceEntry) -> Bool {
        if lhs.groupId != rhs.groupId {
            return false
        }
        if lhs.unreadState != rhs.unreadState {
            return false
        }
        if lhs.message?.stableVersion != rhs.message?.stableVersion {
            return false
        }
        if lhs.renderedPeers != rhs.renderedPeers {
            return false
        }
        return true
    }
}

public struct ChatListForumTopicData: Equatable {
    public var id: Int64
    public var info: StoredMessageHistoryThreadInfo
    
    public init(id: Int64, info: StoredMessageHistoryThreadInfo) {
        self.id = id
        self.info = info
    }
}

public enum ChatListEntry: Comparable {
    case MessageEntry(index: ChatListIndex, messages: [Message], readState: ChatListViewReadState?, isRemovedFromTotalUnreadCount: Bool, embeddedInterfaceState: StoredPeerChatInterfaceState?, renderedPeer: RenderedPeer, presence: PeerPresence?, summaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo], forumTopicData: ChatListForumTopicData?, topForumTopics: [ChatListForumTopicData], hasFailed: Bool, isContact: Bool, autoremoveTimeout: Int32?)
    case HoleEntry(ChatListHole)
    
    public var index: ChatListIndex {
        switch self {
            case let .MessageEntry(index, _, _, _, _, _, _, _, _, _, _, _, _):
                return index
            case let .HoleEntry(hole):
                return ChatListIndex(pinningIndex: nil, messageIndex: hole.index)
        }
    }

    public static func ==(lhs: ChatListEntry, rhs: ChatListEntry) -> Bool {
        switch lhs {
            case let .MessageEntry(lhsIndex, lhsMessages, lhsReadState, lhsIsRemovedFromTotalUnreadCount, lhsEmbeddedState, lhsPeer, lhsPresence, lhsInfo, lhsForumTopicData, lhsTopForumTopics, lhsHasFailed, lhsIsContact, lhsAutoremoveTimeout):
                switch rhs {
                    case let .MessageEntry(rhsIndex, rhsMessages, rhsReadState, rhsIsRemovedFromTotalUnreadCount, rhsEmbeddedState, rhsPeer, rhsPresence, rhsInfo, rhsForumTopicData, rhsTopForumTopics, rhsHasFailed, rhsIsContact, rhsAutoremoveTimeout):
                        if lhsIndex != rhsIndex {
                            return false
                        }
                        if lhsReadState != rhsReadState {
                            return false
                        }
                        if lhsMessages.count != rhsMessages.count {
                            return false
                        }
                        for i in 0 ..< lhsMessages.count {
                            if lhsMessages[i].stableVersion != rhsMessages[i].stableVersion {
                                return false
                            }
                        }
                        if lhsIsRemovedFromTotalUnreadCount != rhsIsRemovedFromTotalUnreadCount {
                            return false
                        }
                        if let lhsEmbeddedState = lhsEmbeddedState, let rhsEmbeddedState = rhsEmbeddedState {
                            if lhsEmbeddedState != rhsEmbeddedState {
                                return false
                            }
                        } else if (lhsEmbeddedState != nil) != (rhsEmbeddedState != nil) {
                            return false
                        }
                        if lhsPeer != rhsPeer {
                            return false
                        }
                        if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
                            if !lhsPresence.isEqual(to: rhsPresence) {
                                return false
                            }
                        } else if (lhsPresence != nil) != (rhsPresence != nil) {
                            return false
                        }
                        if lhsInfo != rhsInfo {
                            return false
                        }
                        if lhsForumTopicData != rhsForumTopicData {
                            return false
                        }
                        if lhsTopForumTopics != rhsTopForumTopics {
                            return false
                        }
                        if lhsHasFailed != rhsHasFailed {
                            return false
                        }
                        if lhsIsContact != rhsIsContact {
                            return false
                        }
                        if lhsAutoremoveTimeout != rhsAutoremoveTimeout {
                            return false
                        }
                        return true
                    default:
                        return false
                }
            case let .HoleEntry(hole):
                if case .HoleEntry(hole) = rhs {
                    return true
                } else {
                    return false
                }
        }
    }

    public static func <(lhs: ChatListEntry, rhs: ChatListEntry) -> Bool {
        return lhs.index < rhs.index
    }
}

enum MutableChatListEntry: Equatable {
    case IntermediateMessageEntry(index: ChatListIndex, messageIndex: MessageIndex?)
    case MessageEntry(index: ChatListIndex, messages: [Message], readState: ChatListViewReadState?, notificationSettings: PeerNotificationSettings?, isRemovedFromTotalUnreadCount: Bool, embeddedInterfaceState: StoredPeerChatInterfaceState?, renderedPeer: RenderedPeer, presence: PeerPresence?, tagSummaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo], forumTopicData: ChatListForumTopicData?, topForumTopics: [ChatListForumTopicData], hasFailedMessages: Bool, isContact: Bool, autoremoveTimeout: Int32?)
    case HoleEntry(ChatListHole)
    
    init(_ intermediateEntry: ChatListIntermediateEntry, cachedDataTable: CachedPeerDataTable, readStateTable: MessageHistoryReadStateTable, messageHistoryTable: MessageHistoryTable) {
        switch intermediateEntry {
            case let .message(index, messageIndex):
                self = .IntermediateMessageEntry(index: index, messageIndex: messageIndex)
            case let .hole(hole):
                self = .HoleEntry(hole)
        }
    }
    
    var index: ChatListIndex {
        switch self {
            case let .IntermediateMessageEntry(index, _):
                return index
            case let .MessageEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _):
                return index
            case let .HoleEntry(hole):
                return ChatListIndex(pinningIndex: nil, messageIndex: hole.index)
        }
    }

    static func ==(lhs: MutableChatListEntry, rhs: MutableChatListEntry) -> Bool {
        if lhs.index != rhs.index {
            return false
        }
        
        switch lhs {
            case .IntermediateMessageEntry:
                switch rhs {
                    case .IntermediateMessageEntry:
                        return true
                    default:
                        return false
                }
            case .MessageEntry:
                switch rhs {
                    case .MessageEntry:
                        return true
                    default:
                        return false
                }
            case .HoleEntry:
                switch rhs {
                    case .HoleEntry:
                        return true
                    default:
                        return false
                }
        }
    }
}

final class MutableChatListViewReplayContext {
    var invalidEarlier: Bool = false
    var invalidLater: Bool = false
    var removedEntries: Bool = false
    
    func empty() -> Bool {
        return !self.removedEntries && !invalidEarlier && !invalidLater
    }
}

private enum ChatListEntryType {
    case message
    case hole
    case groupReference
}

public struct ChatListFilterPredicate {
    public var includePeerIds: Set<PeerId>
    public var excludePeerIds: Set<PeerId>
    public var pinnedPeerIds: [PeerId]
    public var messageTagSummary: ChatListMessageTagSummaryResultCalculation?
    public var includeAdditionalPeerGroupIds: [PeerGroupId]
    public var include: (Peer, Bool, Bool, Bool, Bool?) -> Bool
    
    public init(includePeerIds: Set<PeerId>, excludePeerIds: Set<PeerId>, pinnedPeerIds: [PeerId], messageTagSummary: ChatListMessageTagSummaryResultCalculation?, includeAdditionalPeerGroupIds: [PeerGroupId], include: @escaping (Peer, Bool, Bool, Bool, Bool?) -> Bool) {
        self.includePeerIds = includePeerIds
        self.excludePeerIds = excludePeerIds
        self.pinnedPeerIds = pinnedPeerIds
        self.messageTagSummary = messageTagSummary
        self.includeAdditionalPeerGroupIds = includeAdditionalPeerGroupIds
        self.include = include
    }
    
    public func includes(peer: Peer, groupId: PeerGroupId, isRemovedFromTotalUnreadCount: Bool, isUnread: Bool, isContact: Bool, messageTagSummaryResult: Bool?) -> Bool {
        if self.pinnedPeerIds.contains(peer.id) {
            return false
        }
        let includePeerId = peer.associatedPeerId ?? peer.id
        if self.excludePeerIds.contains(includePeerId) {
            return false
        }
        if self.includePeerIds.contains(includePeerId) {
            return true
        }
        if groupId != .root {
            if !self.includeAdditionalPeerGroupIds.contains(groupId) {
                return false
            }
        }
        return self.include(peer, isRemovedFromTotalUnreadCount, isUnread, isContact, messageTagSummaryResult)
    }
}

struct MutableChatListAdditionalItemEntry: Equatable {
    public var entry: MutableChatListEntry
    public var info: AdditionalChatListItem
    
    static func ==(lhs: MutableChatListAdditionalItemEntry, rhs: MutableChatListAdditionalItemEntry) -> Bool {
        if lhs.entry != rhs.entry {
            return false
        }
        if !lhs.info.isEqual(to: rhs.info) {
            return false
        }
        return true
    }
}

public struct ChatListAdditionalItemEntry: Equatable {
    public let entry: ChatListEntry
    public let info: AdditionalChatListItem
    
    public static func ==(lhs: ChatListAdditionalItemEntry, rhs: ChatListAdditionalItemEntry) -> Bool {
        if lhs.entry != rhs.entry {
            return false
        }
        if !lhs.info.isEqual(to: rhs.info) {
            return false
        }
        return true
    }
}

func renderAssociatedMediaForPeers(postbox: PostboxImpl, peers: SimpleDictionary<PeerId, Peer>) -> [MediaId: Media] {
    var result: [MediaId: Media] = [:]
    
    for (_, peer) in peers {
        if let associatedMediaIds = peer.associatedMediaIds {
            for id in associatedMediaIds {
                if result[id] == nil {
                    if let media = postbox.messageHistoryTable.getMedia(id) {
                        result[id] = media
                    }
                }
            }
        }
    }
    
    return result
}

func renderAssociatedMediaForPeers(postbox: PostboxImpl, peers: [Peer]) -> [MediaId: Media] {
    var result: [MediaId: Media] = [:]
    
    for peer in peers {
        if let associatedMediaIds = peer.associatedMediaIds {
            for id in associatedMediaIds {
                if result[id] == nil {
                    if let media = postbox.messageHistoryTable.getMedia(id) {
                        result[id] = media
                    }
                }
            }
        }
    }
    
    return result
}

func renderAssociatedMediaForPeers(postbox: PostboxImpl, peers: [PeerId: Peer]) -> [MediaId: Media] {
    var result: [MediaId: Media] = [:]
    
    for (_, peer) in peers {
        if let associatedMediaIds = peer.associatedMediaIds {
            for id in associatedMediaIds {
                if result[id] == nil {
                    if let media = postbox.messageHistoryTable.getMedia(id) {
                        result[id] = media
                    }
                }
            }
        }
    }
    
    return result
}

public struct ChatListViewReadState: Equatable {
    public var state: CombinedPeerReadState
    public var isMuted: Bool
    
    public init(state: CombinedPeerReadState, isMuted: Bool) {
        self.state = state
        self.isMuted = isMuted
    }
}

final class MutableChatListView {
    let groupId: PeerGroupId
    let filterPredicate: ChatListFilterPredicate?
    private let aroundIndex: ChatListIndex
    private let summaryComponents: ChatListEntrySummaryComponents
    fileprivate var groupEntries: [ChatListGroupReferenceEntry]
    private var count: Int
    
    private let spaces: [ChatListViewSpace]
    fileprivate var state: ChatListViewState
    fileprivate var sampledState: ChatListViewSample
    
    private var additionalItemIds = Set<PeerId>()
    private var additionalItems: [AdditionalChatListItem] = []
    fileprivate var additionalItemEntries: [MutableChatListAdditionalItemEntry] = []
    
    private var currentHiddenPeerIds = Set<PeerId>()
    
    init(postbox: PostboxImpl, currentTransaction: Transaction, groupId: PeerGroupId, filterPredicate: ChatListFilterPredicate?, aroundIndex: ChatListIndex, count: Int, summaryComponents: ChatListEntrySummaryComponents) {
        self.groupId = groupId
        self.filterPredicate = filterPredicate
        self.aroundIndex = aroundIndex
        self.summaryComponents = summaryComponents
        
        self.currentHiddenPeerIds = postbox.hiddenChatIds
        
        var spaces: [ChatListViewSpace] = [
            .group(groupId: self.groupId, pinned: .notPinned, predicate: filterPredicate)
        ]
        if let filterPredicate = self.filterPredicate {
            spaces.append(.group(groupId: self.groupId, pinned: .includePinnedAsUnpinned, predicate: filterPredicate))
            for additionalGroupId in filterPredicate.includeAdditionalPeerGroupIds {
                spaces.append(.group(groupId: additionalGroupId, pinned: .notPinned, predicate: filterPredicate))
                spaces.append(.group(groupId: additionalGroupId, pinned: .includePinnedAsUnpinned, predicate: filterPredicate))
            }
            if !filterPredicate.pinnedPeerIds.isEmpty {
                spaces.append(.peers(peerIds: filterPredicate.pinnedPeerIds, asPinned: true))
            }
        } else {
            spaces.append(.group(groupId: self.groupId, pinned: .includePinned, predicate: filterPredicate))
        }
        self.spaces = spaces
        self.state = ChatListViewState(postbox: postbox, currentTransaction: currentTransaction, spaces: self.spaces, anchorIndex: aroundIndex, summaryComponents: self.summaryComponents, halfLimit: count)
        self.sampledState = self.state.sample(postbox: postbox, currentTransaction: currentTransaction)
        
        self.count = count
        
        if case .root = groupId, self.filterPredicate == nil {
            let items = postbox.additionalChatListItemsTable.get()
            self.additionalItems = items
            self.additionalItemIds = Set(items.map { $0.peerId })
            for item in items {
                if let entry = postbox.chatListTable.getStandalone(peerId: item.peerId, messageHistoryTable: postbox.messageHistoryTable, includeIfNoHistory: item.includeIfNoHistory) {
                    self.additionalItemEntries.append(MutableChatListAdditionalItemEntry(
                        entry: MutableChatListEntry(entry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable),
                        info: item
                    ))
                }
            }
            self.groupEntries = []
            self.reloadGroups(postbox: postbox)
        } else {
            self.groupEntries = []
        }
    }
    
    private func reloadGroups(postbox: PostboxImpl) {
        self.groupEntries.removeAll()
        if case .root = self.groupId, self.filterPredicate == nil {
            for groupId in postbox.chatListTable.existingGroups() {
                var foundIndices: [(ChatListIndex, MessageIndex)] = []
                var unpinnedCount = 0
                let maxCount = 8
                
                var upperBound: (ChatListIndex, Bool)?
                inner: while true {
                    if let entry = postbox.chatListTable.earlierEntryInfos(groupId: groupId, index: upperBound, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable, count: 1).first {
                        switch entry {
                            case let .message(index, messageIndex):
                                if let messageIndex = messageIndex, !postbox.isChatHidden(peerId: messageIndex.id.peerId) {
                                    foundIndices.append((index, messageIndex))
                                    if index.pinningIndex == nil {
                                        unpinnedCount += 1
                                    }
                                    
                                    if unpinnedCount >= maxCount {
                                        break inner
                                    }
                                    
                                    upperBound = (entry.index, true)
                                } else {
                                    upperBound = (entry.index.predecessor, true)
                                }
                            case .hole:
                                upperBound = (entry.index, false)
                        }
                    } else {
                        break inner
                    }
                }
                
                foundIndices.sort(by: { $0.1 > $1.1 })
                if foundIndices.count > maxCount {
                    foundIndices.removeSubrange(maxCount...)
                }
                
                if !foundIndices.isEmpty {
                    var message: Message?
                    var renderedPeers: [ChatListGroupReferencePeer] = []
                    for (index, messageIndex) in foundIndices {
                        if let peer = postbox.peerTable.get(index.messageIndex.id.peerId) {
                            var peers = SimpleDictionary<PeerId, Peer>()
                            peers[peer.id] = peer
                            if let associatedPeerId = peer.associatedPeerId {
                                if let associatedPeer = postbox.peerTable.get(associatedPeerId) {
                                    peers[associatedPeer.id] = associatedPeer
                                }
                            }
                            
                            let renderedPeer = RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: renderAssociatedMediaForPeers(postbox: postbox, peers: peers))
                            
                            let isUnread: Bool
                            if postbox.seedConfiguration.peerSummaryIsThreadBased(peer) {
                                let hasUnmutedUnread = postbox.peerThreadsSummaryTable.get(peerId: peer.id)?.hasUnmutedUnread ?? false
                                isUnread = hasUnmutedUnread
                            } else {
                                isUnread = postbox.readStateTable.getCombinedState(peer.id)?.isUnread ?? false
                            }
                            
                            renderedPeers.append(ChatListGroupReferencePeer(peer: renderedPeer, isUnread: isUnread))
                            
                            if foundIndices.count == 1 && message == nil {
                                message = postbox.messageHistoryTable.getMessage(messageIndex).flatMap({ postbox.messageHistoryTable.renderMessage($0, peerTable: postbox.peerTable, threadIndexTable: postbox.messageHistoryThreadIndexTable, storyTable: postbox.storyTable) })
                            }
                        }
                    }
                    
                    self.groupEntries.append(ChatListGroupReferenceEntry(groupId: groupId, message: message, renderedPeers: renderedPeers, unreadState: postbox.groupMessageStatsTable.get(groupId: groupId)))
                }
            }
        }
    }
    
    func refreshDueToExternalTransaction(postbox: PostboxImpl, currentTransaction: Transaction) -> Bool {
        var updated = false
        
        self.state = ChatListViewState(postbox: postbox, currentTransaction: currentTransaction, spaces: self.spaces, anchorIndex: self.aroundIndex, summaryComponents: self.summaryComponents, halfLimit: self.count)
        self.sampledState = self.state.sample(postbox: postbox, currentTransaction: currentTransaction)
        updated = true
        
        let currentGroupEntries = self.groupEntries
        
        self.reloadGroups(postbox: postbox)
        
        if self.groupEntries != currentGroupEntries {
            updated = true
        }
        
        return updated
    }
    
    func replay(postbox: PostboxImpl, currentTransaction: Transaction, operations: [PeerGroupId: [ChatListOperation]], updatedPeerNotificationSettings: [PeerId: (PeerNotificationSettings?, PeerNotificationSettings)], updatedPeers: [PeerId: Peer], updatedPeerPresences: [PeerId: PeerPresence], transaction: PostboxTransaction, context: MutableChatListViewReplayContext) -> Bool {
        var hasChanges = false
        
        let hiddenChatIds = postbox.hiddenChatIds
        var hasFilterChanges = false
        if hiddenChatIds != self.currentHiddenPeerIds {
            self.currentHiddenPeerIds = hiddenChatIds
            hasFilterChanges = true
        }
        
        if transaction.updatedGlobalNotificationSettings && self.filterPredicate != nil {
            self.state = ChatListViewState(postbox: postbox, currentTransaction: currentTransaction, spaces: self.spaces, anchorIndex: self.aroundIndex, summaryComponents: self.summaryComponents, halfLimit: self.count)
            self.sampledState = self.state.sample(postbox: postbox, currentTransaction: currentTransaction)
            hasChanges = true
        } else if hasFilterChanges {
            self.state = ChatListViewState(postbox: postbox, currentTransaction: currentTransaction, spaces: self.spaces, anchorIndex: self.aroundIndex, summaryComponents: self.summaryComponents, halfLimit: self.count)
            self.sampledState = self.state.sample(postbox: postbox, currentTransaction: currentTransaction)
            hasChanges = true
        } else {
            if self.state.replay(postbox: postbox, currentTransaction: currentTransaction, transaction: transaction) {
                self.sampledState = self.state.sample(postbox: postbox, currentTransaction: currentTransaction)
                hasChanges = true
            }
        }
        
        if case .root = self.groupId, self.filterPredicate == nil {
            var invalidatedGroups = false
            for (groupId, groupOperations) in operations {
                if case .group = groupId, !groupOperations.isEmpty {
                    invalidatedGroups = true
                }
            }
            if hasFilterChanges {
                invalidatedGroups = true
            }
            
            if invalidatedGroups {
                self.reloadGroups(postbox: postbox)
                hasChanges = true
            } else {
                for i in 0 ..< self.groupEntries.count {
                    if let updatedState = transaction.currentUpdatedTotalUnreadSummaries[self.groupEntries[i].groupId] {
                        self.groupEntries[i] = ChatListGroupReferenceEntry(groupId: self.groupEntries[i].groupId, message: self.groupEntries[i].message, renderedPeers: self.groupEntries[i].renderedPeers, unreadState: updatedState)
                        hasChanges = true
                    }
                }
                
                if !transaction.alteredInitialPeerCombinedReadStates.isEmpty {
                    for i in 0 ..< self.groupEntries.count {
                        for j in 0 ..< groupEntries[i].renderedPeers.count {
                            if transaction.alteredInitialPeerCombinedReadStates[groupEntries[i].renderedPeers[j].peer.peerId] != nil {
                                
                                let isUnread: Bool
                                if let peer = groupEntries[i].renderedPeers[j].peer.peer, postbox.seedConfiguration.peerSummaryIsThreadBased(peer) {
                                    isUnread = postbox.peerThreadsSummaryTable.get(peerId: peer.id)?.hasUnmutedUnread ?? false
                                } else {
                                    isUnread = postbox.readStateTable.getCombinedState(groupEntries[i].renderedPeers[j].peer.peerId)?.isUnread ?? false
                                }
                                
                                if isUnread != groupEntries[i].renderedPeers[j].isUnread {
                                    var renderedPeers = self.groupEntries[i].renderedPeers
                                    renderedPeers[j] = ChatListGroupReferencePeer(peer: groupEntries[i].renderedPeers[j].peer, isUnread: isUnread)
                                    self.groupEntries[i] = ChatListGroupReferenceEntry(groupId: self.groupEntries[i].groupId, message: self.groupEntries[i].message, renderedPeers: renderedPeers, unreadState: self.groupEntries[i].unreadState)
                                }
                            }
                        }
                    }
                }
            }
        }
        
        var updateAdditionalItems = false
        if case .root = self.groupId, self.filterPredicate == nil, let items = transaction.replacedAdditionalChatListItems {
            self.additionalItems = items
            self.additionalItemIds = Set(items.map { $0.peerId })
            updateAdditionalItems = true
        }
        for peerId in self.additionalItemIds {
            if transaction.currentOperationsByPeerId[peerId] != nil {
                updateAdditionalItems = true
            }
            if transaction.currentUpdatedPeers[peerId] != nil {
                updateAdditionalItems = true
            }
            if transaction.currentUpdatedChatListInclusions[peerId] != nil {
                updateAdditionalItems = true
            }
        }
        if updateAdditionalItems {
            self.additionalItemEntries.removeAll()
            for item in self.additionalItems {
                if let entry = postbox.chatListTable.getStandalone(peerId: item.peerId, messageHistoryTable: postbox.messageHistoryTable, includeIfNoHistory: item.includeIfNoHistory) {
                    self.additionalItemEntries.append(MutableChatListAdditionalItemEntry(
                        entry: MutableChatListEntry(entry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable),
                        info: item
                    ))
                }
            }
            hasChanges = true
        }
        return hasChanges
    }
    
    func complete(postbox: PostboxImpl, context: MutableChatListViewReplayContext) {
        
    }
    
    func firstHole() -> (PeerGroupId, ChatListHole)? {
        return self.sampledState.hole
    }
    
    private func renderEntry(_ entry: MutableChatListEntry, postbox: PostboxImpl) -> MutableChatListEntry? {
        switch entry {
        case let .IntermediateMessageEntry(index, messageIndex):
            var renderedMessages: [Message] = []
            
            if let messageIndex = messageIndex {
                if let messageGroup = postbox.messageHistoryTable.getMessageGroup(at: messageIndex, limit: 10) {
                    renderedMessages.append(contentsOf: messageGroup.compactMap(postbox.renderIntermediateMessage))
                }
            }
            var peers = SimpleDictionary<PeerId, Peer>()
            var notificationSettings: PeerNotificationSettings?
            var presence: PeerPresence?
            var isContact: Bool = false
            if let peer = postbox.peerTable.get(index.messageIndex.id.peerId) {
                peers[peer.id] = peer
                if let associatedPeerId = peer.associatedPeerId {
                    if let associatedPeer = postbox.peerTable.get(associatedPeerId) {
                        peers[associatedPeer.id] = associatedPeer
                    }
                    notificationSettings = postbox.peerNotificationSettingsTable.getEffective(associatedPeerId)
                    presence = postbox.peerPresenceTable.get(associatedPeerId)
                    isContact = postbox.contactsTable.isContact(peerId: associatedPeerId)
                } else {
                    notificationSettings = postbox.peerNotificationSettingsTable.getEffective(index.messageIndex.id.peerId)
                    presence = postbox.peerPresenceTable.get(index.messageIndex.id.peerId)
                    isContact = postbox.contactsTable.isContact(peerId: peer.id)
                }
            }
            
            let renderedPeer = RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers, associatedMedia: renderAssociatedMediaForPeers(postbox: postbox, peers: peers))
            
            var forumTopicData: ChatListForumTopicData?
            if let message = renderedMessages.first, let threadId = message.threadId {
                if let info = postbox.messageHistoryThreadIndexTable.get(peerId: message.id.peerId, threadId: threadId) {
                    forumTopicData = ChatListForumTopicData(id: threadId, info: info)
                }
            }
            
            var topForumTopics: [ChatListForumTopicData] = []
            if let peer = renderedPeer.peer, postbox.seedConfiguration.peerSummaryIsThreadBased(peer) {
                for item in postbox.messageHistoryThreadIndexTable.fetch(peerId: peer.id, namespace: 0, start: .upperBound, end: .lowerBound, limit: 5) {
                    topForumTopics.append(ChatListForumTopicData(id: item.threadId, info: item.info))
                }
            }
            
            let readState: ChatListViewReadState?
            if let peer = postbox.peerTable.get(index.messageIndex.id.peerId), postbox.seedConfiguration.peerSummaryIsThreadBased(peer) {
                let summary = postbox.peerThreadsSummaryTable.get(peerId: index.messageIndex.id.peerId)
                var count: Int32 = 0
                var isMuted: Bool = false
                if let summary = summary {
                    count = summary.totalUnreadCount
                    if count > 0 {
                        isMuted = !summary.hasUnmutedUnread
                    }
                }
                readState = ChatListViewReadState(state: CombinedPeerReadState(states: [(0, .idBased(maxIncomingReadId: 1, maxOutgoingReadId: 0, maxKnownId: 0, count: count, markedUnread: false))]), isMuted: isMuted)
            } else {
                readState = postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId).flatMap { state -> ChatListViewReadState in
                    return ChatListViewReadState(state: state, isMuted: false)
                }
            }
            
            var autoremoveTimeout: Int32?
            if let cachedData = postbox.cachedPeerDataTable.get(index.messageIndex.id.peerId) {
                autoremoveTimeout = postbox.seedConfiguration.decodeAutoremoveTimeout(cachedData)
            }
            
            return .MessageEntry(index: index, messages: renderedMessages, readState: readState, notificationSettings: notificationSettings, isRemovedFromTotalUnreadCount: false, embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId), renderedPeer: renderedPeer, presence: presence, tagSummaryInfo: [:], forumTopicData: forumTopicData, topForumTopics: topForumTopics, hasFailedMessages: postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId), isContact: isContact, autoremoveTimeout: autoremoveTimeout)
        default:
            return nil
        }
    }
    
    func render(postbox: PostboxImpl) {
        for i in 0 ..< self.additionalItemEntries.count {
            if let updatedEntry = self.renderEntry(self.additionalItemEntries[i].entry, postbox: postbox) {
                self.additionalItemEntries[i].entry = updatedEntry
            }
        }
    }
}

public final class ChatListView {
    public let groupId: PeerGroupId
    public let additionalItemEntries: [ChatListAdditionalItemEntry]
    public let entries: [ChatListEntry]
    public let groupEntries: [ChatListGroupReferenceEntry]
    public let earlierIndex: ChatListIndex?
    public let laterIndex: ChatListIndex?
    
    init(_ mutableView: MutableChatListView) {
        self.groupId = mutableView.groupId
        
        var entries: [ChatListEntry] = []
        for entry in mutableView.sampledState.entries {
            switch entry {
            case let .MessageEntry(index, messages, combinedReadState, _, isRemovedFromTotalUnreadCount, embeddedState, peer, peerPresence, summaryInfo, forumTopicData, topForumTopics, hasFailed, isContact, autoremoveTimeout):
                entries.append(.MessageEntry(index: index, messages: messages, readState: combinedReadState, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, embeddedInterfaceState: embeddedState, renderedPeer: peer, presence: peerPresence, summaryInfo: summaryInfo, forumTopicData: forumTopicData, topForumTopics: topForumTopics, hasFailed: hasFailed, isContact: isContact, autoremoveTimeout: autoremoveTimeout))
            case let .HoleEntry(hole):
                entries.append(.HoleEntry(hole))
            case .IntermediateMessageEntry:
                assertionFailure()
            }
        }
        
        self.entries = entries
        self.earlierIndex = mutableView.sampledState.lower?.index
        self.laterIndex = mutableView.sampledState.upper?.index
        
        self.groupEntries = mutableView.groupEntries
        
        var additionalItemEntries: [ChatListAdditionalItemEntry] = []
        for entry in mutableView.additionalItemEntries {
            switch entry.entry {
            case let .MessageEntry(index, messages, combinedReadState, _, isExcludedFromUnreadCount, embeddedState, peer, peerPresence, summaryInfo, forumTopicData, topForumTopics, hasFailed, isContact, autoremoveTimeout):
                additionalItemEntries.append(ChatListAdditionalItemEntry(
                    entry: .MessageEntry(index: index, messages: messages, readState: combinedReadState, isRemovedFromTotalUnreadCount: isExcludedFromUnreadCount, embeddedInterfaceState: embeddedState, renderedPeer: peer, presence: peerPresence, summaryInfo: summaryInfo, forumTopicData: forumTopicData, topForumTopics: topForumTopics, hasFailed: hasFailed, isContact: isContact, autoremoveTimeout: autoremoveTimeout),
                    info: entry.info
                ))
            case .HoleEntry:
                assertionFailure()
            case .IntermediateMessageEntry:
                assertionFailure()
            }
        }
        
        self.additionalItemEntries = additionalItemEntries
    }
}