module Main {

    import Std::Io::{PrintLine, Print, PrintInt, ReadFile, WriteFile, FileExists};
    import Std::Os::{Os_ArgsCount, Os_Args};
    import Std::String::{String_Len, String_Eq, String_StartsWith, String_Find, String_Slice, String_Offset};

    extern func bux_strlen(s: String) -> uint;
    extern func bux_strstr(haystack: String, needle: String) -> String;
    extern func bux_str_slice(s: String, start: uint, len: uint) -> String;
    extern func bux_str_split_count(s: String, delim: String) -> uint;
    extern func bux_str_split_part(s: String, delim: String, index: uint) -> String;
    extern func bux_sb_new(initial_cap: uint) -> *void;
    extern func bux_sb_append(sb: *void, s: String);
    extern func bux_sb_append_char(sb: *void, c: char8);

    func SB_AppendChar(sb: *void, c: int) {
        bux_sb_append_char(sb, c as char8);
    }
    extern func bux_sb_build(sb: *void) -> String;
    extern func bux_sb_free(sb: *void);
    extern func bux_alloc(size: uint) -> *void;
    extern func bux_free(ptr: *void);

    struct Database {
        path: String,
        sb: *void,
        loaded: bool,
        count: uint,
    }

    func DB_New(filePath: String) -> Database {
        var db: Database;
        db.path = filePath;
        db.sb = bux_sb_new(4096);
        db.loaded = false;
        db.count = 0;
        return db;
    }

    func DB_Load(self: *Database) -> bool {
        if self.loaded { return true; }

        if !FileExists(self.path) {
            self.loaded = true;
            return true;
        }

        let raw: String = ReadFile(self.path);
        if String_Len(raw) == 0 || bux_strlen(raw) == 0 {
            self.loaded = true;
            return true;
        }

        bux_sb_append(self.sb, raw);

        let lineCount: uint = bux_str_split_count(raw, "\n");
        var i: uint = 0;
        var n: uint = 0;
        while i < lineCount {
            let line: String = bux_str_split_part(raw, "\n", i);
            if bux_strlen(line) > 0 {
                n = n + 1;
            }
            i = i + 1;
        }
        self.count = n;
        self.loaded = true;
        return true;
    }

    func DB_Get(self: *Database, key: String) -> String {
        let raw: String = bux_sb_build(self.sb);
        let lineCount: uint = bux_str_split_count(raw, "\n");
        var i: uint = 0;
        while i < lineCount {
            let line: String = bux_str_split_part(raw, "\n", i);
            if String_StartsWith(line, key) {
                let eqPos: String = String_Find(line, "=");
                if String_Len(eqPos) > 0 {
                    let keyLen: uint = bux_strlen(key);
                    let valStart: uint = keyLen + 1;
                    let lineLen: uint = bux_strlen(line);
                    if valStart < lineLen {
                        return bux_str_slice(line, valStart, lineLen - valStart);
                    }
                }
            }
            i = i + 1;
        }
        return "";
    }

    func DB_Set(self: *Database, key: String, value: String) {
        let raw: String = bux_sb_build(self.sb);
        let lineCount: uint = bux_str_split_count(raw, "\n");

        var newSb: *void = bux_sb_new(4096);
        var found: bool = false;
        var i: uint = 0;
        while i < lineCount {
            let line: String = bux_str_split_part(raw, "\n", i);
            if bux_strlen(line) > 0 {
                if String_StartsWith(line, key) {
                    let eqPos: String = String_Find(line, "=");
                    if String_Len(eqPos) > 0 {
                        bux_sb_append(newSb, key);
                        SB_AppendChar(newSb, 61);
                        bux_sb_append(newSb, value);
                        SB_AppendChar(newSb, 10);
                        found = true;
                    } else {
                        bux_sb_append(newSb, line);
                        SB_AppendChar(newSb, 10);
                    }
                } else {
                    bux_sb_append(newSb, line);
                    SB_AppendChar(newSb, 10);
                }
            }
            i = i + 1;
        }

        if !found {
            bux_sb_append(newSb, key);
            SB_AppendChar(newSb, 61);
            bux_sb_append(newSb, value);
            SB_AppendChar(newSb, 10);
            self.count = self.count + 1;
        }

        bux_sb_free(self.sb);
        self.sb = newSb;
    }

    func DB_Del(self: *Database, key: String) {
        let raw: String = bux_sb_build(self.sb);
        let lineCount: uint = bux_str_split_count(raw, "\n");

        var newSb: *void = bux_sb_new(4096);
        var i: uint = 0;
        var delCount: uint = 0;
        while i < lineCount {
            let line: String = bux_str_split_part(raw, "\n", i);
            if bux_strlen(line) > 0 {
                if String_StartsWith(line, key) {
                    let eqPos: String = String_Find(line, "=");
                    if String_Len(eqPos) > 0 {
                        delCount = delCount + 1;
                    } else {
                        bux_sb_append(newSb, line);
                        SB_AppendChar(newSb, 10);
                    }
                } else {
                    bux_sb_append(newSb, line);
                    SB_AppendChar(newSb, 10);
                }
            }
            i = i + 1;
        }

        if delCount > 0 {
            self.count = self.count - delCount;
        }

        bux_sb_free(self.sb);
        self.sb = newSb;
    }

    func DB_Has(self: *Database, key: String) -> bool {
        let val: String = DB_Get(self, key);
        return bux_strlen(val) > 0;
    }

    func DB_Count(self: *Database) -> uint {
        return self.count;
    }

    func DB_Keys(self: *Database) -> *String {
        if self.count == 0 { return null as *String; }

        let raw: String = bux_sb_build(self.sb);
        let lineCount: uint = bux_str_split_count(raw, "\n");
        let keys: *String = bux_alloc(self.count * sizeof(String)) as *String;

        var found: uint = 0;
        var i: uint = 0;
        while i < lineCount && found < self.count {
            let line: String = bux_str_split_part(raw, "\n", i);
            if bux_strlen(line) > 0 {
                let eqPos: String = String_Find(line, "=");
                if String_Len(eqPos) > 0 {
                    let keyLen: uint = String_Offset(eqPos, line);
                    keys[found] = bux_str_slice(line, 0, keyLen);
                    found = found + 1;
                }
            }
            i = i + 1;
        }

        return keys;
    }

    func DB_Save(self: *Database) -> bool {
        let content: String = bux_sb_build(self.sb);
        let ok: bool = WriteFile(self.path, content);
        return ok;
    }

    func PrintUsage() {
        PrintLine("SimpleDB — file-backed key-value database");
        PrintLine("Usage:");
        PrintLine("  simpledb <dbfile> set <key> <value>");
        PrintLine("  simpledb <dbfile> get <key>");
        PrintLine("  simpledb <dbfile> del <key>");
        PrintLine("  simpledb <dbfile> has <key>");
        PrintLine("  simpledb <dbfile> keys");
        PrintLine("  simpledb <dbfile> count");
    }

    func Main() -> int {
        let argc: int = Os_ArgsCount();

        if argc < 3 {
            PrintUsage();
            return 1;
        }

        let dbFile: String = Os_Args(1);
        let cmd: String = Os_Args(2);

        var db: Database = DB_New(dbFile);

        if String_Eq(cmd, "get") {
            DB_Load(&db);
            if argc < 4 {
                PrintLine("get: missing key");
                return 1;
            }
            let key: String = Os_Args(3);
            let val: String = DB_Get(&db, key);
            if bux_strlen(val) > 0 {
                PrintLine(val);
            } else {
                Print("(not found) ");
                PrintLine(key);
            }
            return 0;
        }

        if String_Eq(cmd, "set") {
            DB_Load(&db);
            if argc < 5 {
                PrintLine("set: missing key or value");
                return 1;
            }
            let key: String = Os_Args(3);
            let val: String = Os_Args(4);
            DB_Set(&db, key, val);
            let saved: bool = DB_Save(&db);
            if saved {
                Print("OK  ");
                Print(key);
                Print(" = ");
                PrintLine(val);
            } else {
                PrintLine("set: save failed");
            }
            return 0;
        }

        if String_Eq(cmd, "del") {
            DB_Load(&db);
            if argc < 4 {
                PrintLine("del: missing key");
                return 1;
            }
            let key: String = Os_Args(3);
            DB_Del(&db, key);
            DB_Save(&db);
            Print("DEL ");
            PrintLine(key);
            return 0;
        }

        if String_Eq(cmd, "has") {
            DB_Load(&db);
            if argc < 4 {
                PrintLine("has: missing key");
                return 1;
            }
            let key: String = Os_Args(3);
            let found: bool = DB_Has(&db, key);
            if found {
                Print("true  ");
                PrintLine(key);
            } else {
                Print("false  ");
                PrintLine(key);
            }
            return 0;
        }

        if String_Eq(cmd, "keys") {
            DB_Load(&db);
            let n: uint = DB_Count(&db);
            Print("keys: ");
            PrintInt(n as int);
            PrintLine("");

            if n == 0 {
                return 0;
            }

            let list: *String = DB_Keys(&db);
            var i: uint = 0;
            while i < n {
                Print("  ");
                PrintLine(list[i]);
                i = i + 1;
            }
            bux_free(list as *void);
            return 0;
        }

        if String_Eq(cmd, "count") {
            DB_Load(&db);
            let n: uint = DB_Count(&db);
            Print("count: ");
            PrintInt(n as int);
            PrintLine("");
            return 0;
        }

        Print("unknown command: ");
        PrintLine(cmd);
        return 1;
    }

}
