import Foundation

public struct UnorderedItemListEntryInfo {
    public let hashValue: Int64
    
    public init(hashValue: Int64) {
        self.hashValue = hashValue
    }
}

public struct UnorderedItemListEntry {
    public let id: ValueBoxKey
    public let info: UnorderedItemListEntryInfo
    public let contents: PostboxCoding
    
    public init(id: ValueBoxKey, info: UnorderedItemListEntryInfo, contents: PostboxCoding) {
        self.id = id
        self.info = info
        self.contents = contents
    }
}

public struct UnorderedItemListEntryTag {
    public let value: ValueBoxKey
    
    public init(value: ValueBoxKey) {
        self.value = value
    }
}

public protocol UnorderedItemListTagMetaInfo: PostboxCoding {
    func isEqual(to: UnorderedItemListTagMetaInfo) -> Bool
}

private enum UnorderedItemListTableKeyspace: UInt8 {
    case metaInfo = 0
    case entries = 1
}

private func extractEntryKey(tagLength: Int, key: ValueBoxKey) -> ValueBoxKey {
    let result = ValueBoxKey(length: key.length - tagLength - 1)
    memcpy(result.memory, key.memory.advanced(by: 1 + tagLength), result.length)
    return result
}

private func extractEntryInfo(_ value: ReadBuffer) -> UnorderedItemListEntryInfo {
    var hashValue: Int64 = 0
    value.read(&hashValue, offset: 0, length: 8)
    return UnorderedItemListEntryInfo(hashValue: hashValue)
}

final class UnorderedItemListTable: Table {
    static func tableSpec(_ id: Int32) -> ValueBoxTable {
        return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
    }
    
    private func metaInfoKey(tag: UnorderedItemListEntryTag) -> ValueBoxKey {
        let tagValue = tag.value
        let key = ValueBoxKey(length: 1 + tagValue.length)
        key.setUInt8(0, value: UnorderedItemListTableKeyspace.metaInfo.rawValue)
        memcpy(key.memory.advanced(by: 1), tagValue.memory, tagValue.length)
        return key
    }
    
    private func entryKey(tag: UnorderedItemListEntryTag, id: ValueBoxKey) -> ValueBoxKey {
        let tagValue = tag.value
        let key = ValueBoxKey(length: 1 + tagValue.length + id.length)
        key.setUInt8(0, value: UnorderedItemListTableKeyspace.entries.rawValue)
        memcpy(key.memory.advanced(by: 1), tagValue.memory, tagValue.length)
        memcpy(key.memory.advanced(by: 1 + tagValue.length), id.memory, id.length)
        return key
    }
    
    private func entryLowerBoundKey(tag: UnorderedItemListEntryTag) -> ValueBoxKey {
        let tagValue = tag.value
        let key = ValueBoxKey(length: 1 + tagValue.length)
        key.setUInt8(0, value: UnorderedItemListTableKeyspace.entries.rawValue)
        memcpy(key.memory.advanced(by: 1), tagValue.memory, tagValue.length)
        return key
    }
    
    private func entryUpperBoundKey(tag: UnorderedItemListEntryTag) -> ValueBoxKey {
        let tagValue = tag.value.successor
        let key = ValueBoxKey(length: 1 + tagValue.length)
        key.setUInt8(0, value: UnorderedItemListTableKeyspace.entries.rawValue)
        memcpy(key.memory.advanced(by: 1), tagValue.memory, tagValue.length)
        return key
    }
    
    private func getMetaInfo(tag: UnorderedItemListEntryTag) -> UnorderedItemListTagMetaInfo? {
        if let value = self.valueBox.get(self.table, key: self.metaInfoKey(tag: tag)), let info = PostboxDecoder(buffer: value).decodeRootObject() as? UnorderedItemListTagMetaInfo {
            return info
        } else {
            return nil
        }
    }
    
    private func setMetaInfo(tag: UnorderedItemListEntryTag, info: UnorderedItemListTagMetaInfo) {
        let encoder = PostboxEncoder()
        encoder.encodeRootObject(info)
        self.valueBox.set(self.table, key: self.metaInfoKey(tag: tag), value: encoder.readBufferNoCopy())
    }
    
    private func getEntryInfos(tag: UnorderedItemListEntryTag) -> [ValueBoxKey: UnorderedItemListEntryInfo] {
        var result: [ValueBoxKey: UnorderedItemListEntryInfo] = [:]
        let tagLength = tag.value.length
        self.valueBox.range(self.table, start: self.entryLowerBoundKey(tag: tag), end: self.entryUpperBoundKey(tag: tag), values: { key, value in
            result[extractEntryKey(tagLength: tagLength, key: key)] = extractEntryInfo(value)
            return true
        }, limit: 0)
        return result
    }
    
    private func getEntry(tag: UnorderedItemListEntryTag, id: ValueBoxKey) -> UnorderedItemListEntry? {
        if let value = self.valueBox.get(self.table, key: self.entryKey(tag: tag, id: id)) {
            var hashValue: Int64 = 0
            value.read(&hashValue, offset: 0, length: 8)
            let tempBuffer = MemoryBuffer(memory: value.memory.advanced(by: 8), capacity: value.length - 8, length: value.length - 8, freeWhenDone: false)
            let contents = withExtendedLifetime(tempBuffer, {
                return PostboxDecoder(buffer: tempBuffer).decodeRootObject()
            })
            if let contents = contents {
                let entry = UnorderedItemListEntry(id: id, info: UnorderedItemListEntryInfo(hashValue: hashValue), contents: contents)
                return entry
            } else {
                assertionFailure()
                return nil
            }
        } else {
            return nil
        }
    }
    
    private func setEntry(tag: UnorderedItemListEntryTag, entry: UnorderedItemListEntry, sharedBuffer: WriteBuffer, sharedEncoder: PostboxEncoder) {
        sharedBuffer.reset()
        sharedEncoder.reset()
        
        var hashValue: Int64 = entry.info.hashValue
        sharedBuffer.write(&hashValue, offset: 0, length: 8)
        
        sharedEncoder.encodeRootObject(entry.contents)
        let tempBuffer = sharedEncoder.readBufferNoCopy()
        withExtendedLifetime(tempBuffer, {
            sharedBuffer.write(tempBuffer.memory, offset: 0, length: tempBuffer.length)
        })
        
        self.valueBox.set(self.table, key: self.entryKey(tag: tag, id: entry.id), value: sharedBuffer)
    }
    
    func scan(tag: UnorderedItemListEntryTag, _ f: (UnorderedItemListEntry) -> Void) {
        let tagLength = tag.value.length
        self.valueBox.range(self.table, start: self.entryLowerBoundKey(tag: tag), end: self.entryUpperBoundKey(tag: tag), values: { key, value in
            let entryKey = extractEntryKey(tagLength: tagLength, key: key)
            
            var hashValue: Int64 = 0
            value.read(&hashValue, offset: 0, length: 8)
            let tempBuffer = MemoryBuffer(memory: value.memory.advanced(by: 8), capacity: value.length - 8, length: value.length - 8, freeWhenDone: false)
            let contents = withExtendedLifetime(tempBuffer, {
                return PostboxDecoder(buffer: tempBuffer).decodeRootObject()
            })
            if let contents = contents {
                f(UnorderedItemListEntry(id: entryKey, info: UnorderedItemListEntryInfo(hashValue: hashValue), contents: contents))
            } else {
                assertionFailure()
            }
            
            return true
        }, limit: 0)
    }
    
    func difference(tag: UnorderedItemListEntryTag, updatedEntryInfos: [ValueBoxKey: UnorderedItemListEntryInfo]) -> (metaInfo: UnorderedItemListTagMetaInfo?, added: [ValueBoxKey], removed: [UnorderedItemListEntry], updated: [UnorderedItemListEntry]) {
        let currentEntryInfos = self.getEntryInfos(tag: tag)
        var currentInfoIds = Set<ValueBoxKey>()
        for key in currentEntryInfos.keys {
            currentInfoIds.insert(key)
        }
        
        var updatedInfoIds = Set<ValueBoxKey>()
        for key in updatedEntryInfos.keys {
            updatedInfoIds.insert(key)
        }
        
        let addedKeys = updatedInfoIds.subtracting(currentInfoIds)
        let added: [ValueBoxKey] = Array(addedKeys)
        
        let removedKeys = currentInfoIds.subtracting(updatedInfoIds)
        var removed: [UnorderedItemListEntry] = []
        for key in removedKeys {
            if let entry = self.getEntry(tag: tag, id: key) {
                removed.append(entry)
            } else {
                assertionFailure()
            }
        }
        
        var updated: [UnorderedItemListEntry] = []
        for (key, info) in updatedEntryInfos {
            if !addedKeys.contains(key) {
                if let currentInfo = currentEntryInfos[key] {
                    if info.hashValue != currentInfo.hashValue {
                        if let entry = self.getEntry(tag: tag, id: key) {
                            updated.append(entry)
                        } else {
                            assertionFailure()
                        }
                    }
                } else {
                    assertionFailure()
                }
            }
        }
        
        return (self.getMetaInfo(tag: tag), added, removed, updated)
    }
    
    func applyDifference(tag: UnorderedItemListEntryTag, previousInfo: UnorderedItemListTagMetaInfo?, updatedInfo: UnorderedItemListTagMetaInfo, setItems: [UnorderedItemListEntry], removeItemIds: [ValueBoxKey]) -> Bool {
        let currentInfo = self.getMetaInfo(tag: tag)
        if let currentInfo = currentInfo, let previousInfo = previousInfo {
            if !currentInfo.isEqual(to: previousInfo) {
                return false
            }
        } else if (currentInfo != nil) != (previousInfo != nil) {
            return false
        }
        
        self.setMetaInfo(tag: tag, info: updatedInfo)
        
        let sharedBuffer = WriteBuffer()
        let sharedEncoder = PostboxEncoder()
        for entry in setItems {
            self.setEntry(tag: tag, entry: entry, sharedBuffer: sharedBuffer, sharedEncoder: sharedEncoder)
        }
        
        for id in removeItemIds {
            self.valueBox.remove(self.table, key: self.entryKey(tag: tag, id: id), secure: false)
        }
        
        return true
    }
}