import Foundation

public struct ItemCollectionViewEntryIndex: Comparable {
    public let collectionIndex: Int32
    public let collectionId: ItemCollectionId
    public let itemIndex: ItemCollectionItemIndex
    
    public init(collectionIndex: Int32, collectionId: ItemCollectionId, itemIndex: ItemCollectionItemIndex) {
        self.collectionIndex = collectionIndex
        self.collectionId = collectionId
        self.itemIndex = itemIndex
    }
    
    public static func ==(lhs: ItemCollectionViewEntryIndex, rhs: ItemCollectionViewEntryIndex) -> Bool {
        return lhs.collectionIndex == rhs.collectionIndex && lhs.collectionId == rhs.collectionId && lhs.itemIndex == rhs.itemIndex
    }
    
    public static func <(lhs: ItemCollectionViewEntryIndex, rhs: ItemCollectionViewEntryIndex) -> Bool {
        if lhs.collectionIndex == rhs.collectionIndex {
            if lhs.itemIndex == rhs.itemIndex {
                return lhs.collectionId < rhs.collectionId
            } else {
                return lhs.itemIndex < rhs.itemIndex
            }
        } else {
            return lhs.collectionIndex < rhs.collectionIndex
        }
    }
    
    public static func lowerBound(collectionIndex: Int32, collectionId: ItemCollectionId) -> ItemCollectionViewEntryIndex {
        return ItemCollectionViewEntryIndex(collectionIndex: collectionIndex, collectionId: collectionId, itemIndex: ItemCollectionItemIndex(index: 0, id: 0))
    }
}

public struct ItemCollectionViewEntry {
    public let index: ItemCollectionViewEntryIndex
    public let item: ItemCollectionItem
    
    public init(index: ItemCollectionViewEntryIndex, item: ItemCollectionItem) {
        self.index = index
        self.item = item
    }
}

private func fetchLowerEntries(namespaces: [ItemCollectionId.Namespace], collectionId: ItemCollectionId, collectionIndex: Int32, itemIndex: ItemCollectionItemIndex, count: Int, lowerCollectionId: (_ namespaceList: [ItemCollectionId.Namespace], _ collectionId: ItemCollectionId, _ collectionIndex: Int32) -> (ItemCollectionId, Int32)?, lowerItems: (_ collectionId: ItemCollectionId, _ itemIndex: ItemCollectionItemIndex, _ count: Int) -> [ItemCollectionItem]) -> [ItemCollectionViewEntry] {
    
    var entries: [ItemCollectionViewEntry] = []
    
    var currentCollectionIndex = collectionIndex
    var currentCollectionId = collectionId
    var currentItemIndex = itemIndex
    
    while true {
        let remainingCount = count - entries.count
        assert(remainingCount > 0)
        let collectionItems = lowerItems(currentCollectionId, currentItemIndex, remainingCount)
        for item in collectionItems {
            entries.append(ItemCollectionViewEntry(index: ItemCollectionViewEntryIndex(collectionIndex: currentCollectionIndex, collectionId: currentCollectionId, itemIndex: item.index), item: item))
        }
        if entries.count >= count {
            break
        } else {
            assert(collectionItems.count < remainingCount)
            
            if let (previousCollectionId, previousCollectionIndex) = lowerCollectionId(namespaces, currentCollectionId, currentCollectionIndex) {
                currentCollectionIndex = previousCollectionIndex
                currentCollectionId = previousCollectionId
                currentItemIndex = ItemCollectionItemIndex.upperBound
            } else {
                break
            }
        }
    }
    
    return entries
}

private func fetchHigherEntries(namespaces: [ItemCollectionId.Namespace], collectionId: ItemCollectionId, collectionIndex: Int32, itemIndex: ItemCollectionItemIndex, count: Int, higherCollectionId: (_ namespaceList: [ItemCollectionId.Namespace], _ collectionId: ItemCollectionId, _ collectionIndex: Int32) -> (ItemCollectionId, Int32)?, higherItems: (_ collectionId: ItemCollectionId, _ itemIndex: ItemCollectionItemIndex, _ count: Int) -> [ItemCollectionItem]) -> [ItemCollectionViewEntry] {
    
    var entries: [ItemCollectionViewEntry] = []
    
    var currentCollectionIndex = collectionIndex
    var currentCollectionId = collectionId
    var currentItemIndex = itemIndex
    
    while true {
        let remainingCount = count - entries.count
        assert(remainingCount > 0)
        let collectionItems = higherItems(currentCollectionId, currentItemIndex, remainingCount)
        for item in collectionItems {
            entries.append(ItemCollectionViewEntry(index: ItemCollectionViewEntryIndex(collectionIndex: currentCollectionIndex, collectionId: currentCollectionId, itemIndex: item.index), item: item))
        }
        if entries.count >= count {
            break
        } else {
            assert(collectionItems.count < remainingCount)
            
            if let (nextCollectionId, nextCollectionIndex) = higherCollectionId(namespaces, currentCollectionId, currentCollectionIndex) {
                currentCollectionIndex = nextCollectionIndex
                currentCollectionId = nextCollectionId
                currentItemIndex = ItemCollectionItemIndex.lowerBound
            } else {
                break
            }
        }
    }
    
    return entries
}

private func aroundEntries(namespaces: [ItemCollectionId.Namespace],
                           aroundIndex: ItemCollectionViewEntryIndex?,
                           count: Int,
                           collectionIndexById: (ItemCollectionId) -> Int32?,
                           lowerCollectionId: (_ namespaceList: [ItemCollectionId.Namespace], _ collectionId: ItemCollectionId, _ collectionIndex: Int32) -> (ItemCollectionId, Int32)?,
                           fetchLowerItems: (_ collectionId: ItemCollectionId, _ itemIndex: ItemCollectionItemIndex, _ count: Int) -> [ItemCollectionItem],
                           higherCollectionId: (_ namespaceList: [ItemCollectionId.Namespace], _ collectionId: ItemCollectionId, _ collectionIndex: Int32) -> (ItemCollectionId, Int32)?,
                           fetchHigherItems: (_ collectionId: ItemCollectionId, _ itemIndex: ItemCollectionItemIndex, _ count: Int) -> [ItemCollectionItem]) -> ([ItemCollectionViewEntry], ItemCollectionViewEntry?, ItemCollectionViewEntry?) {
    var lowerEntries: [ItemCollectionViewEntry] = []
    var upperEntries: [ItemCollectionViewEntry] = []
    var lower: ItemCollectionViewEntry?
    var upper: ItemCollectionViewEntry?
    
    let selectedAroundIndex: ItemCollectionViewEntryIndex
    if let aroundIndex = aroundIndex, let aroundCollectionIndex = collectionIndexById(aroundIndex.collectionId) {
        selectedAroundIndex = ItemCollectionViewEntryIndex(collectionIndex: aroundCollectionIndex, collectionId: aroundIndex.collectionId, itemIndex: aroundIndex.itemIndex)
    } else {
        selectedAroundIndex = ItemCollectionViewEntryIndex(collectionIndex: 0, collectionId: ItemCollectionId(namespace: namespaces[0], id: 0), itemIndex: ItemCollectionItemIndex.lowerBound)
    }
    
    let collectionId: ItemCollectionId = selectedAroundIndex.collectionId
    let collectionIndex: Int32 = selectedAroundIndex.collectionIndex
    let itemIndex: ItemCollectionItemIndex = selectedAroundIndex.itemIndex
    
    lowerEntries.append(contentsOf: fetchLowerEntries(namespaces: namespaces, collectionId: collectionId, collectionIndex: collectionIndex, itemIndex: itemIndex, count: count / 2 + 1, lowerCollectionId: lowerCollectionId, lowerItems: fetchLowerItems))
    
    let lowerIndices = lowerEntries.map { $0.index }
    assert(lowerIndices.sorted() == lowerIndices.reversed())
    
    if lowerEntries.count >= count / 2 + 1 {
        lower = lowerEntries.last
        lowerEntries.removeLast()
    }
    
    upperEntries.append(contentsOf: fetchHigherEntries(namespaces: namespaces, collectionId: collectionId, collectionIndex: collectionIndex, itemIndex: ItemCollectionItemIndex(index: itemIndex.index, id: max(0, itemIndex.id - 1)), count: count - lowerEntries.count + 1, higherCollectionId: higherCollectionId, higherItems: fetchHigherItems))
    
    let upperIndices = upperEntries.map { $0.index }
    assert(upperIndices.sorted() == upperIndices)
    
    if upperEntries.count >= count - lowerEntries.count + 1 {
        upper = upperEntries.last
        upperEntries.removeLast()
    }
    
    if lowerEntries.count != 0 && lowerEntries.count + upperEntries.count < count {
        var additionalLowerEntries: [ItemCollectionViewEntry] = fetchLowerEntries(namespaces: namespaces, collectionId: lowerEntries.last!.index.collectionId, collectionIndex: lowerEntries.last!.index.collectionIndex, itemIndex: lowerEntries.last!.index.itemIndex, count: count - lowerEntries.count - upperEntries.count + 1, lowerCollectionId: lowerCollectionId, lowerItems: fetchLowerItems)
        
        if additionalLowerEntries.count >= count - lowerEntries.count + upperEntries.count + 1 {
            lower = additionalLowerEntries.last
            additionalLowerEntries.removeLast()
        }
        lowerEntries.append(contentsOf: additionalLowerEntries)
    }
    
    var entries: [ItemCollectionViewEntry] = []
    entries.append(contentsOf: lowerEntries.reversed())
    entries.append(contentsOf: upperEntries)
    return (entries: entries, lower: lower, upper: upper)
}

final class MutableItemCollectionsView {
    let orderedItemListsViews: [MutableOrderedItemListView]
    let namespaces: [ItemCollectionId.Namespace]
    let requestedAroundIndex: ItemCollectionViewEntryIndex?
    let requestedCount: Int
    
    var collectionInfos: [(ItemCollectionId, ItemCollectionInfo, ItemCollectionItem?)]
    var entries: [ItemCollectionViewEntry]
    var lower: ItemCollectionViewEntry?
    var higher: ItemCollectionViewEntry?
    
    init(postbox: PostboxImpl, orderedItemListsViews: [MutableOrderedItemListView], namespaces: [ItemCollectionId.Namespace], aroundIndex: ItemCollectionViewEntryIndex?, count: Int) {
        self.orderedItemListsViews = orderedItemListsViews
        self.namespaces = namespaces
        self.requestedAroundIndex = aroundIndex
        self.requestedCount = count
        
        self.collectionInfos = []
        self.entries = []
        self.lower = nil
        self.higher = nil
        
        self.reload(postbox: postbox, aroundIndex: aroundIndex, count: count)
    }
    
    private func lowerItems(postbox: PostboxImpl, collectionId: ItemCollectionId, itemIndex: ItemCollectionItemIndex, count: Int) -> [ItemCollectionItem] {
        return postbox.itemCollectionItemTable.lowerItems(collectionId: collectionId, itemIndex: itemIndex, count: count)
    }
    
    private func higherItems(postbox: PostboxImpl, collectionId: ItemCollectionId, itemIndex: ItemCollectionItemIndex, count: Int) -> [ItemCollectionItem] {
        return postbox.itemCollectionItemTable.higherItems(collectionId: collectionId, itemIndex: itemIndex, count: count)
    }
    
    private func lowerCollectionId(postbox: PostboxImpl, namespaceList: [ItemCollectionId.Namespace], collectionId: ItemCollectionId, collectionIndex: Int32) -> (ItemCollectionId, Int32)? {
        return postbox.itemCollectionInfoTable.lowerCollectionId(namespaceList: namespaceList, collectionId: collectionId, index: collectionIndex)
    }
    
    private func higherCollectionId(postbox: PostboxImpl, namespaceList: [ItemCollectionId.Namespace], collectionId: ItemCollectionId, collectionIndex: Int32) -> (ItemCollectionId, Int32)? {
        return postbox.itemCollectionInfoTable.higherCollectionId(namespaceList: namespaceList, collectionId: collectionId, index: collectionIndex)
    }
    
    private func reload(postbox: PostboxImpl, aroundIndex: ItemCollectionViewEntryIndex?, count: Int) {
        self.collectionInfos = []
        for namespace in namespaces {
            for (_, id, info) in postbox.itemCollectionInfoTable.getInfos(namespace: namespace) {
                let item = self.higherItems(postbox: postbox, collectionId: id, itemIndex: ItemCollectionItemIndex.lowerBound, count: 1).first
                self.collectionInfos.append((id, info, item))
            }
        }
        
        let (entries, lower, higher) = aroundEntries(namespaces: namespaces,
                                                     aroundIndex: aroundIndex,
                                                     count: count, collectionIndexById: { id in
                                                        return postbox.itemCollectionInfoTable.getIndex(id: id)
        },
                                                     lowerCollectionId: { namespaceList, collectionId, collectionIndex in
                                                        return self.lowerCollectionId(postbox: postbox, namespaceList: namespaceList, collectionId: collectionId, collectionIndex: collectionIndex)
        },
                                                     fetchLowerItems: { collectionId, itemIndex, count in
                                                        return self.lowerItems(postbox: postbox, collectionId: collectionId, itemIndex: itemIndex, count: count)
        },
                                                     higherCollectionId: { namespaceList, collectionId, collectionIndex in
                                                        return self.higherCollectionId(postbox: postbox, namespaceList: namespaceList, collectionId: collectionId, collectionIndex: collectionIndex)
        },
                                                     fetchHigherItems: {
                                                        collectionId, itemIndex, count in
                                                        return self.higherItems(postbox: postbox, collectionId: collectionId, itemIndex: itemIndex, count: count)
        })
        
        self.entries = entries
        self.lower = lower
        self.higher = higher
    }
    
    func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool {
        var updated = false
        
        if !transaction.currentOrderedItemListOperations.isEmpty {
            for view in self.orderedItemListsViews {
                if view.replay(postbox: postbox, transaction: transaction) {
                    updated = true
                }
            }
        }
        
        
        var reloadNamespaces = Set<ItemCollectionId.Namespace>()
        for operation in transaction.currentItemCollectionInfosOperations {
            switch operation {
                case let .replaceInfos(namespace):
                    reloadNamespaces.insert(namespace)
            }
        }
        
        for (id, operations) in transaction.currentItemCollectionItemsOperations {
            for operation in operations {
                switch operation {
                    case .replaceItems:
                        reloadNamespaces.insert(id.namespace)
                }
            }
        }
        
        var shouldReloadEntries = false
        if !reloadNamespaces.isEmpty {
            for namespace in self.namespaces {
                if reloadNamespaces.contains(namespace) {
                    shouldReloadEntries = true
                    break
                }
            }
        }
        
        if shouldReloadEntries {
            self.reload(postbox: postbox, aroundIndex: self.requestedAroundIndex, count: self.requestedCount)
            updated = true
        }
        
        return updated
    }
}

public final class ItemCollectionsView {
    public let orderedItemListsViews: [OrderedItemListView]
    public let collectionInfos: [(ItemCollectionId, ItemCollectionInfo, ItemCollectionItem?)]
    public let entries: [ItemCollectionViewEntry]
    public let lower: ItemCollectionViewEntry?
    public let higher: ItemCollectionViewEntry?
    
    init(_ mutableView: MutableItemCollectionsView) {
        self.orderedItemListsViews = mutableView.orderedItemListsViews.map { OrderedItemListView($0) }
        self.collectionInfos = mutableView.collectionInfos
        self.entries = mutableView.entries
        self.lower = mutableView.lower
        self.higher = mutableView.higher
    }
}