// =====================================================================
// sqlite.sx — thin sx bindings over the VENDORED SQLite amalgamation
// (vendor/sqlite/, see its README for version + upgrade notes).
//
// `#library "sqlite3"` resolves to the vendored build products under
// build/vendor/ (never the OS copy): `sx build` links the static
// archive via `-L build/vendor`; `sx run` dlopens the dylib via
// `-L build/vendor/jit` (tests/run.sh). tests/sqlite_smoke.sx pins
// `sqlite3_libversion()` to the vendored version so a silent fallback
// to the system library fails the suite.
//
// FFI type choices: handles cross as `usize` (`sqlite3*` /
// `sqlite3_stmt*` are opaque and never dereferenced; `usize` keeps
// `sqlite3_open`'s out-param a single-level `*usize` instead of
// `**void`). Whole C strings IN are `[:0]u8` (the std FFI convention —
// build terminated copies with `cstring`); `prepare_v2`/`bind_text`
// take `[*]u8` + explicit byte length instead, so un-terminated sx
// string views pass without copying. Nullable C strings OUT are `*u8`
// + a cast null-check (the `getenv` convention — `[:0]u8` has no null),
// and are COPIED into `context.allocator` before returning — sqlite's
// buffers die on the next step/finalize/close.
//
// The wrapper surface is exactly what the storage layer needs: open /
// exec / prepare / bind / step / column / finalize, last_insert_rowid,
// and errmsg for failure detail. Everything else stays behind the FFI
// line until needed.
// =====================================================================

#import "modules/std.sx";

sqlib :: #library "sqlite3";

sqlite3_open        :: (path: [:0]u8, out_db: *usize) -> i32 #foreign sqlib "dist_sqlite3_open";
sqlite3_close       :: (db: usize) -> i32 #foreign sqlib "dist_sqlite3_close";
sqlite3_exec        :: (db: usize, sql: [:0]u8, cb: usize, arg: usize, errmsg: usize) -> i32 #foreign sqlib "dist_sqlite3_exec";
sqlite3_prepare_v2  :: (db: usize, sql: [*]u8, nbyte: i32, out_stmt: *usize, out_tail: usize) -> i32 #foreign sqlib "dist_sqlite3_prepare_v2";
sqlite3_step        :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_step";
sqlite3_finalize    :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_finalize";
sqlite3_reset       :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_reset";
sqlite3_bind_text   :: (stmt: usize, idx: i32, text: [*]u8, n: i32, destructor: isize) -> i32 #foreign sqlib "dist_sqlite3_bind_text";
sqlite3_bind_int64  :: (stmt: usize, idx: i32, v: i64) -> i32 #foreign sqlib "dist_sqlite3_bind_int64";
sqlite3_bind_null   :: (stmt: usize, idx: i32) -> i32 #foreign sqlib "dist_sqlite3_bind_null";
sqlite3_column_int64 :: (stmt: usize, icol: i32) -> i64 #foreign sqlib "dist_sqlite3_column_int64";
sqlite3_column_text  :: (stmt: usize, icol: i32) -> *u8 #foreign sqlib "dist_sqlite3_column_text";
sqlite3_column_bytes :: (stmt: usize, icol: i32) -> i32 #foreign sqlib "dist_sqlite3_column_bytes";
sqlite3_column_type  :: (stmt: usize, icol: i32) -> i32 #foreign sqlib "dist_sqlite3_column_type";
sqlite3_column_count :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_column_count";
sqlite3_errmsg       :: (db: usize) -> *u8 #foreign sqlib "dist_sqlite3_errmsg";
sqlite3_libversion   :: () -> *u8 #foreign sqlib "dist_sqlite3_libversion";
sqlite3_last_insert_rowid :: (db: usize) -> i64 #foreign sqlib "dist_sqlite3_last_insert_rowid";
sqlite3_changes      :: (db: usize) -> i32 #foreign sqlib "dist_sqlite3_changes";

// Result codes (the subset the wrappers interpret).
SQLITE_OK   :: 0;
SQLITE_ROW  :: 100;
SQLITE_DONE :: 101;

// sqlite3_bind_text destructor sentinel: copy the bytes NOW — sx-side
// buffers (arena strings, stack temporaries) don't outlive the call.
SQLITE_TRANSIENT : isize : -1;

// Column types (sqlite3_column_type).
SQLITE_INTEGER :: 1;
SQLITE_TEXT    :: 3;
SQLITE_NULL    :: 5;

SqliteErr :: error {
    Open,
    Exec,
    Prepare,
    Bind,
    Step,
}

// `s` as a null-terminated heap copy, passable where `[:0]u8` is expected.
sq_cstr :: (s: string) -> string {
    z := cstring(s.len);
    if s.len > 0 { memcpy(z.ptr, s.ptr, s.len); }
    return z;
}

// Copy the C string at `p` (null = empty) into context.allocator.
sq_from_cstr :: (p: *u8) -> string {
    addr : i64 = xx p;
    if addr == 0 { return ""; }
    cp : [*]u8 = xx p;
    n := 0;
    while cp[n] != 0 { n += 1; }
    raw : [*]u8 = xx context.allocator.alloc_bytes(n + 1);
    if n > 0 { memcpy(raw, cp, xx n); }
    raw[n] = 0;
    return string.{ ptr = raw, len = n };
}

// The linked/loaded SQLite's version string (e.g. "3.53.2").
sqlite_version :: () -> string {
    return sq_from_cstr(sqlite3_libversion());
}

// One prepared statement. `finalize` releases it; bind indexes are
// 1-based and column indexes 0-based, as in the C API.
SqliteStmt :: struct {
    handle: usize;
    db: usize;

    bind_text :: (self: *SqliteStmt, idx: i64, s: string) -> !SqliteErr {
        rc := sqlite3_bind_text(self.handle, xx idx, s.ptr, xx s.len, SQLITE_TRANSIENT);
        if rc != SQLITE_OK { raise error.Bind; }
        return;
    }
    bind_int64 :: (self: *SqliteStmt, idx: i64, v: i64) -> !SqliteErr {
        rc := sqlite3_bind_int64(self.handle, xx idx, v);
        if rc != SQLITE_OK { raise error.Bind; }
        return;
    }
    bind_null :: (self: *SqliteStmt, idx: i64) -> !SqliteErr {
        rc := sqlite3_bind_null(self.handle, xx idx);
        if rc != SQLITE_OK { raise error.Bind; }
        return;
    }

    // SQLITE_ROW / SQLITE_DONE on success; anything else raises with the
    // detail left in the connection's errmsg.
    step :: (self: *SqliteStmt) -> (i64, !SqliteErr) {
        rc := sqlite3_step(self.handle);
        if rc != SQLITE_ROW and rc != SQLITE_DONE { raise error.Step; }
        return xx rc;
    }

    column_int64 :: (self: *SqliteStmt, icol: i64) -> i64 {
        return sqlite3_column_int64(self.handle, xx icol);
    }
    // Copied into context.allocator; a NULL column reads as "".
    column_text :: (self: *SqliteStmt, icol: i64) -> string {
        p := sqlite3_column_text(self.handle, xx icol);
        addr : i64 = xx p;
        if addr == 0 { return ""; }
        n := sqlite3_column_bytes(self.handle, xx icol);
        cp : [*]u8 = xx p;
        raw : [*]u8 = xx context.allocator.alloc_bytes(xx (n + 1));
        if n > 0 { memcpy(raw, cp, xx n); }
        raw[n] = 0;
        return string.{ ptr = raw, len = xx n };
    }
    column_type :: (self: *SqliteStmt, icol: i64) -> i64 {
        return xx sqlite3_column_type(self.handle, xx icol);
    }
    column_count :: (self: *SqliteStmt) -> i64 {
        return xx sqlite3_column_count(self.handle);
    }

    reset :: (self: *SqliteStmt) {
        sqlite3_reset(self.handle);
    }
    finalize :: (self: *SqliteStmt) {
        sqlite3_finalize(self.handle);
        self.handle = 0;
    }
}

// One connection. `open` creates the file (and parent-less path) per
// SQLite's defaults; `close` is safe on an already-closed handle.
Sqlite :: struct {
    handle: usize;

    open :: (path: string) -> (Sqlite, !SqliteErr) {
        h : usize = 0;
        rc := sqlite3_open(sq_cstr(path), @h);
        if rc != SQLITE_OK {
            if h != 0 { sqlite3_close(h); }
            raise error.Open;
        }
        return Sqlite.{ handle = h };
    }

    close :: (self: *Sqlite) {
        if self.handle != 0 { sqlite3_close(self.handle); }
        self.handle = 0;
    }

    // Run one or more ;-separated statements with no result rows
    // (DDL, pragmas, BEGIN/COMMIT). Detail via errmsg on failure.
    exec :: (self: *Sqlite, sql: string) -> !SqliteErr {
        rc := sqlite3_exec(self.handle, sq_cstr(sql), 0, 0, 0);
        if rc != SQLITE_OK { raise error.Exec; }
        return;
    }

    prepare :: (self: *Sqlite, sql: string) -> (SqliteStmt, !SqliteErr) {
        sh : usize = 0;
        rc := sqlite3_prepare_v2(self.handle, sql.ptr, xx sql.len, @sh, 0);
        if rc != SQLITE_OK { raise error.Prepare; }
        return SqliteStmt.{ handle = sh, db = self.handle };
    }

    errmsg :: (self: *Sqlite) -> string {
        return sq_from_cstr(sqlite3_errmsg(self.handle));
    }

    last_insert_rowid :: (self: *Sqlite) -> i64 {
        return sqlite3_last_insert_rowid(self.handle);
    }

    changes :: (self: *Sqlite) -> i64 {
        return xx sqlite3_changes(self.handle);
    }
}
