//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation

#if TESTABLE_BUILD

/// Produces instances of `InMemoryKeyValueStore`.
public class InMemoryKeyValueStoreFactory: KeyValueStoreFactory {

    public var stores = [String: InMemoryKeyValueStore]()

    public init() {}

    public func keyValueStore(collection: String) -> SignalServiceKit.KeyValueStore {
        if let store = stores[collection] {
            return store
        }
        let store = InMemoryKeyValueStore(collection: collection)
        stores[collection] = store
        return store
    }
}

/// In memory implementation of a KeyValueStore, requiring no database setup.
/// Should only be used in test environments to simplify testing while replicating "real" storage.
public class InMemoryKeyValueStore: KeyValueStore {

    private var dict = [String: Any]()

    public required init(collection: String) {}

    private func read<T>(_ key: String) -> T? {
        guard let value = dict[key] else {
            return nil
        }
        let val = value as! T
        return val
    }

    private func read<T>(_ key: String, defaultValue: T) -> T {
        guard let value = dict[key] else {
            return defaultValue
        }
        let val = value as! T
        return val
    }

    public func hasValue(_ key: String, transaction: DBReadTransaction) -> Bool {
        return dict[key] != nil
    }

    public func getString(_ key: String, transaction: DBReadTransaction) -> String? {
        return read(key)
    }

    public func setString(_ value: String?, key: String, transaction: SignalServiceKit.DBWriteTransaction) {
        dict[key] = value
    }

    public func getDate(_ key: String, transaction: SignalServiceKit.DBReadTransaction) -> Date? {
        return read(key)
    }

    public func setDate(_ value: Date, key: String, transaction: SignalServiceKit.DBWriteTransaction) {
        dict[key] = value
    }

    public func getBool(_ key: String, transaction: SignalServiceKit.DBReadTransaction) -> Bool? {
        return read(key)
    }

    public func getBool(_ key: String, defaultValue: Bool, transaction: SignalServiceKit.DBReadTransaction) -> Bool {
        return read(key, defaultValue: defaultValue)
    }

    public func setBool(_ value: Bool, key: String, transaction: SignalServiceKit.DBWriteTransaction) {
        dict[key] = value
    }

    public func setBoolIfChanged(_ value: Bool, defaultValue: Bool, key: String, transaction: SignalServiceKit.DBWriteTransaction) {
        dict[key] = value
    }

    public func getUInt(_ key: String, transaction: SignalServiceKit.DBReadTransaction) -> UInt? {
        return read(key)
    }

    public func getUInt(_ key: String, defaultValue: UInt, transaction: SignalServiceKit.DBReadTransaction) -> UInt {
        return read(key, defaultValue: defaultValue)
    }

    public func setUInt(_ value: UInt, key: String, transaction: SignalServiceKit.DBWriteTransaction) {
        dict[key] = value
    }

    public func getData(_ key: String, transaction: SignalServiceKit.DBReadTransaction) -> Data? {
        return read(key)
    }

    public func setData(_ value: Data?, key: String, transaction: SignalServiceKit.DBWriteTransaction) {
        dict[key] = value
    }

    public func getNSNumber(_ key: String, transaction: SignalServiceKit.DBReadTransaction) -> NSNumber? {
        return read(key)
    }

    public func getInt(_ key: String, transaction: SignalServiceKit.DBReadTransaction) -> Int? {
        return read(key)
    }

    public func getInt(_ key: String, defaultValue: Int, transaction: SignalServiceKit.DBReadTransaction) -> Int {
        return read(key, defaultValue: defaultValue)
    }

    public func setInt(_ value: Int, key: String, transaction: SignalServiceKit.DBWriteTransaction) {
        dict[key] = value
    }

    public func getInt32(_ key: String, transaction: SignalServiceKit.DBReadTransaction) -> Int32? {
        return read(key)
    }

    public func getInt32(_ key: String, defaultValue: Int32, transaction: SignalServiceKit.DBReadTransaction) -> Int32 {
        return read(key, defaultValue: defaultValue)
    }

    public func setInt32(_ value: Int32, key: String, transaction: SignalServiceKit.DBWriteTransaction) {
        dict[key] = value
    }

    public func getUInt32(_ key: String, transaction: SignalServiceKit.DBReadTransaction) -> UInt32? {
        return read(key)
    }

    public func getUInt32(_ key: String, defaultValue: UInt32, transaction: SignalServiceKit.DBReadTransaction) -> UInt32 {
        return read(key, defaultValue: defaultValue)
    }

    public func setUInt32(_ value: UInt32, key: String, transaction: SignalServiceKit.DBWriteTransaction) {
        dict[key] = value
    }

    public func getUInt64(_ key: String, transaction: SignalServiceKit.DBReadTransaction) -> UInt64? {
        return read(key)
    }

    public func getUInt64(_ key: String, defaultValue: UInt64, transaction: SignalServiceKit.DBReadTransaction) -> UInt64 {
        return read(key, defaultValue: defaultValue)
    }

    public func setUInt64(_ value: UInt64, key: String, transaction: SignalServiceKit.DBWriteTransaction) {
        dict[key] = value
    }

    public func getInt64(_ key: String, transaction: SignalServiceKit.DBReadTransaction) -> Int64? {
        return read(key)
    }

    public func getInt64(_ key: String, defaultValue: Int64, transaction: SignalServiceKit.DBReadTransaction) -> Int64 {
        return read(key, defaultValue: defaultValue)
    }

    public func setInt64(_ value: Int64, key: String, transaction: SignalServiceKit.DBWriteTransaction) {
        dict[key] = value
    }

    public func getDouble(_ key: String, transaction: SignalServiceKit.DBReadTransaction) -> Double? {
        return read(key)
    }

    public func getDouble(_ key: String, defaultValue: Double, transaction: SignalServiceKit.DBReadTransaction) -> Double {
        return read(key, defaultValue: defaultValue)
    }

    public func setDouble(_ value: Double, key: String, transaction: SignalServiceKit.DBWriteTransaction) {
        dict[key] = value
    }

    public func getObject(forKey key: String, transaction: SignalServiceKit.DBReadTransaction) -> Any? {
        return read(key)
    }

    public func setObject(_ anyValue: Any?, key: String, transaction: SignalServiceKit.DBWriteTransaction) {
        dict[key] = anyValue
    }

    public func removeValue(forKey key: String, transaction: SignalServiceKit.DBWriteTransaction) {
        dict[key] = nil
    }

    public func removeValues(forKeys keys: [String], transaction: SignalServiceKit.DBWriteTransaction) {
        keys.forEach {
            removeValue(forKey: $0, transaction: transaction)
        }
    }

    public func removeAll(transaction: SignalServiceKit.DBWriteTransaction) {
        dict = [:]
    }

    public func enumerateKeysAndObjects(transaction: SignalServiceKit.DBReadTransaction, block: (String, Any, UnsafeMutablePointer<ObjCBool>) -> Void) {
        for key in dict.keys {
            var bool: ObjCBool = .init(booleanLiteral: false)
            block(key, dict[key]!, &bool)
            if bool.boolValue {
                return
            }
        }
    }

    public func enumerateKeys(transaction: SignalServiceKit.DBReadTransaction, block: (String, UnsafeMutablePointer<ObjCBool>) -> Void) {
        enumerateKeysAndObjects(transaction: transaction, block: { key, _, stop in
            block(key, stop)
        })
    }

    public func allValues(transaction: SignalServiceKit.DBReadTransaction) -> [Any] {
        return Array(dict.values)
    }

    public func allDataValues(transaction: SignalServiceKit.DBReadTransaction) -> [Data] {
        return allValues(transaction: transaction).compactMap { $0 as? Data }
    }

    public func allBoolValuesMap(transaction: SignalServiceKit.DBReadTransaction) -> [String: Bool] {
        return dict.compactMapValues { $0 as? Bool }
    }

    public func allUIntValuesMap(transaction: SignalServiceKit.DBReadTransaction) -> [String: UInt] {
        return dict.compactMapValues { $0 as? UInt }
    }

    public func anyDataValue(transaction: SignalServiceKit.DBReadTransaction) -> Data? {
        return allDataValues(transaction: transaction).randomElement()
    }

    public func allKeys(transaction: SignalServiceKit.DBReadTransaction) -> [String] {
        return Array(dict.keys)
    }

    public func numberOfKeys(transaction: SignalServiceKit.DBReadTransaction) -> UInt {
        return UInt(dict.count)
    }

    public func setCodable<T>(_ value: T, key: String, transaction: SignalServiceKit.DBWriteTransaction) throws where T: Encodable {
        let encoded = try JSONEncoder().encode(value)
        setData(encoded, key: key, transaction: transaction)
    }

    public func setCodable<T>(optional value: T, key: String, transaction: SignalServiceKit.DBWriteTransaction) throws where T: Encodable {
        let encoded = try JSONEncoder().encode(value)
        setData(encoded, key: key, transaction: transaction)
    }

    public func getCodableValue<T>(forKey key: String, transaction: SignalServiceKit.DBReadTransaction) throws -> T? where T: Decodable {
        guard let raw = dict[key] else {
            return nil
        }
        let data = raw as! Data
        let decoded = try JSONDecoder().decode(T.self, from: data)
        return decoded
    }

    public func allCodableValues<T>(transaction: SignalServiceKit.DBReadTransaction) throws -> [T] where T: Decodable {
        let decoder = JSONDecoder()
        return dict.values.compactMap { raw in
            guard
                let data = raw as? Data,
                let decoded = try? decoder.decode(T.self, from: data)
            else {
                return nil
            }
            return decoded
        }
    }
}

#endif
