/*
 * Copyright 2022, The Cozo Project Authors.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
 * If a copy of the MPL was not distributed with this file,
 * You can obtain one at https://mozilla.org/MPL/2.0/.
 */

import Foundation
import SwiftyJSON

public enum CozoError: Error {
    case system(String)
    case query(JSON)
}

public class RowHeaders {
    public let headers: [String]
    init(headers: [String]) {
        self.headers = headers
    }
    lazy var invertedKeys: [String:Int] = {
        var ret: [String:Int] = [:]
        for i in 0..<self.headers.count {
            ret[self.headers[i]] = i
        }
        return ret
    }()
}

public struct NamedRow {
    public let headers: RowHeaders
    public let fields: [JSON]
    
    public func get(idx: Int) -> JSON {
        return self.fields[idx]
    }
    public func get(key: String) -> JSON? {
        if let idx = self.headers.invertedKeys[key] {
            return self.fields[idx]
        } else {
            return nil
        }
    }
}

extension [NamedRow] {
    public func toString() -> String {
        var ret = "["
        var isFirst = true
        for field in self {
            if !isFirst {
                ret += ", "
            } else {
                isFirst = false
            }
            ret += field.toString()
        }
        ret += "]"
        return ret
    }
}

extension NamedRow {
    public func toString() -> String {
        var ret = "{"
        for i in 0..<self.headers.headers.count {
            if i != 0 {
                ret += ", "
            }
            ret += self.headers.headers[i]
            ret += ": "
            ret += self.fields[i].rawString(.utf8, options: .init(rawValue: 0))!
        }
        ret += "}"
        return ret
    }
}

public class CozoDB {
    public let db: DbInstance
    
    public init() {
        let db = new_cozo_db("mem", "", "")!
        self.db = db
    }
    public init(kind: String, path: String) throws {
        if let db = new_cozo_db(kind, path, "") {
            self.db = db
        } else {
            throw CozoError.system("Cannot create database")
        }
    }
    public func run(_ query: String, params: JSON) throws -> [NamedRow] {
        let payload = params.rawString(.utf8, options: .init(rawValue: 0))!
        return try self.run(query, stringParams: payload)
    }
    public func run(_ query: String) throws -> [NamedRow] {
        return try self.run(query, stringParams: "")
    }
    func run(_ query: String, stringParams: String) throws -> [NamedRow] {
        let resStr = self.db.run_script_str(query, stringParams, false).toString()
        let dataFromString = resStr.data(using: .utf8, allowLossyConversion: false)!
        let json = JSON(dataFromString);
        if json["ok"].boolValue {
            let jHeaders = json["headers"].arrayValue.map{(j) -> String in
                return j.stringValue
            }
            let headers = RowHeaders(headers: jHeaders)
            return json["rows"].arrayValue.map{(j) -> NamedRow in
                let fields = j.arrayValue
                return NamedRow(headers: headers, fields: fields)
            }
        } else {
            throw CozoError.query(json)
        }
    }
    public func exportRelations(relations: [String]) throws -> JSON {
        let payload = JSON(["relations": relations]).rawString(.utf8, options: .init(rawValue: 0))!
        let resStr = self.db.export_relations_str(payload).toString()
        let dataFromString = resStr.data(using: .utf8, allowLossyConversion: false)!
        let json = JSON(dataFromString);
        if json["ok"].boolValue {
            return json["data"]
        } else {
            throw CozoError.query(json)
        }
    }
    public func importRelations(data: JSON) throws {
        let payload = data.rawString(.utf8, options: .init(rawValue: 0))!
        let resStr = self.db.import_relations_str(payload).toString()
        let dataFromString = resStr.data(using: .utf8, allowLossyConversion: false)!
        let json = JSON(dataFromString);
        if !json["ok"].boolValue {
            throw CozoError.query(json)
        }
    }
    public func backup(path: String) throws {
        let resStr = self.db.backup_db_str(path).toString()
        let dataFromString = resStr.data(using: .utf8, allowLossyConversion: false)!
        let json = JSON(dataFromString);

        if !json["ok"].boolValue {
            throw CozoError.query(json)
        }
    }
    public func restore(path: String) throws {
        let resStr = self.db.restore_backup_str(path).toString()
        let dataFromString = resStr.data(using: .utf8, allowLossyConversion: false)!
        let json = JSON(dataFromString);

        if !json["ok"].boolValue {
            throw CozoError.query(json)
        }
    }
    public func importRelationsFromBackup(path: String, relations: [String]) throws {
        let payload = JSON(["relations": relations, "path": path]).rawString(.utf8, options: .init(rawValue: 0))!
        let resStr = self.db.import_relations_str(payload).toString()
        let dataFromString = resStr.data(using: .utf8, allowLossyConversion: false)!
        let json = JSON(dataFromString);
        if !json["ok"].boolValue {
            throw CozoError.query(json)
        }
    }
}

Graph