// Low level wrapper around a modern subset of the SQLite3 API.
//
// Using this module involves manual resource management.
// Statements must be finalized and databases must be closed.
module sqlite/sqlite3

import sqlite/capi
pub import sqlite/sqlite-result

// Helper to convert error codes returned directly from SQLite3 to the
// corresponding objects.
fun trusted-result(r: int32, pos: int = -1): sqlite-result
  with final ctl throw-exn(exn) impossible(exn.message)
  r.sqlite-result(pos)

// Database handle. An sqlite3 object is obtained by calling one of `open`,
// `open-existing`, or `read-only` and must be `close`d once all use is finished.
abstract struct sqlite3
  cref: any

// Get the result code and position of the last operation.
pub fun last-result(db: sqlite3): sqlite-result
  capi/extended-errcode(db.cref).trusted-result(capi/error-offset(db.cref))

///////////// System lifetime /////////////

// NOTE(zephyr): sqlite3/initialize has the qualifier on its name because
// type inference doesn't know whether to select this one or the capi function
// when constructing startup-initialized below, and there's no way to specify
// this one without the qualifier.
// This is probably a Koka bug.

// Initialize SQLite3.
// This function is automatically called during program initialization;
// see `startup-initialized`.
// If there is a call to `shutdown`, then there must be another call to
// `initialize` before the next use of any other SQLite3 function.
pub fun sqlite3/initialize(): sqlite-result
  capi/initialize().trusted-result

// Release resources held by SQLite3.
// There must not be any `sqlite3` object which is not closed.
pub fun sqlite3/shutdown(): sqlite-result
  capi/shutdown().trusted-result

// The result of initializing SQLite3 during program startup.
pub val startup-initialized = sqlite3/initialize()

///////////// Database lifetime /////////////

// Modes that can be used to name a database to be opened.
pub type db-name
  Filename(path: string)
  URI(uri: string)
  Memory-Shared(name: string) // in-memory database with shared cache
  Memory

// Synchronization modes in which to open a database.
pub type sync-mode
  Single-Thread // No concurrent use is safe.
  Multi-Thread // The database is thread-safe, but connections are not.
  Serialized // The database and connections are always thread-safe.

fun open-raw(name: db-name, base-flags: int32, sync-mode: sync-mode, vfs: string): (sqlite3, sqlite-result)
  val flags = match sync-mode
    Single-Thread -> base-flags.or(0x02000000.i32) // | EXRESCODE
    Multi-Thread -> base-flags.or(0x02008000.i32) // | EXRESCODE | NOMUTEX
    Serialized -> base-flags.or(0x02010000.i32) // | EXRESCODE | FULLMUTEX
  val (db, r) = match name
    Filename(path) -> capi/open-v2(path, flags, vfs)
    URI(uri) -> capi/open-v2(uri, flags.or(0x00000040.i32), vfs) // | URI
    Memory-Shared(name) -> capi/open-v2("file:" ++ name ++ "?mode=memory&cache=shared", flags.or(0x000200C0.i32), vfs) // | URI | SHAREDCACHE | MEMORY
    Memory -> capi/open-v2(":memory:", flags.or(0x00000080.i32), vfs) // | MEMORY
  (Sqlite3(db), r.trusted-result)

// Open or create a database in read-write mode.
pub fun open(name: db-name, sync-mode: sync-mode = Serialized, vfs: string = ""): (sqlite3, sqlite-result)
  open-raw(name, 6.i32, sync-mode, vfs)

// Open an existing database in read-write mode.
pub fun open-existing(name: db-name, sync-mode: sync-mode = Serialized, vfs: string = ""): (sqlite3, sqlite-result)
  open-raw(name, 2.i32, sync-mode, vfs)

// Open a database in read-only mode.
pub fun read-only(name: db-name, sync-mode: sync-mode = Serialized, vfs: string = ""): (sqlite3, sqlite-result)
  open-raw(name, 1.i32, sync-mode, vfs)

pub fun close(db: sqlite3): sqlite-result
  capi/close(db.cref).trusted-result

///////////// Statements /////////////

// Prepared SQL statement.
// Corresponds to `sqlite3_stmt *` in the C API.
abstract struct statement
  cref: any

// Prepare a statement.
// The Right value contains the prepared statement and the portion of `sql`
// remaining after parsing the first statement.
// Once use is finished, the statement must be `finalize`d.
pub fun prepare(db: sqlite3, sql: sslice, persistent: bool = False, disable-vtab: bool = False, disable-log: bool = False): either<sqlite-result, (statement, sslice)>
  val flags = 0.i32
    .or(persistent.int32 * 0x01.i32)
    .or(disable-vtab.int32 * 0x04.i32)
    .or(disable-log.int32 * 0x10.i32)
  val (stmt, left, r) = capi/prepare-v3(db.cref, sql, flags)
  if r == int32/sqlite-ok then
    Right((Statement(stmt), left))
  else
    Left(r.trusted-result(capi/error-offset(db.cref)))

// Release resources associated with a statement.
// Once finalized, a statement can no longer be used.
// Every statement must be finalized.
pub fun statement/finalize(stmt: statement): sqlite-result
  capi/finalize(stmt.cref).trusted-result

// Perform one evaluation step of the statement.
// If the result is `Row`, values are available.
// If the result is `Done`, the statement has finished executing, and it should
// be either `reset` or `finalize`d.
pub fun statement/step(stmt: statement): sqlite-result
  capi/step(stmt.cref).trusted-result

// Reset a statement so that it can be evaluated again.
pub fun statement/reset(stmt: statement): sqlite-result
  capi/reset(stmt.cref).trusted-result

// Get the number of columns available in the statement's result.
pub fun statement/column-count(stmt: statement): int
  capi/column-count(stmt.cref)

// Datatype codes for query results.
pub type sqlite3-type
  Integer
  Float
  Text
  Blob
  Null

// Get the result affinity of a column in the statement's result.
// The leftmost column is numbered 0.
pub fun statment/column-type(stmt: statement, col: int): sqlite3-type
  val t = capi/column-type(stmt.cref, col.int32)
  if t == capi/int32/integer then Integer
  else if t == capi/int32/float then Float
  else if t == capi/int32/blob then Blob
  else if t == capi/int32/text then Text
  // If the DB gives us something unimaginable, pretend it's nothing.
  else Null

// Get the result value of a column as a float64.
// If the result does not have float64 affinity, it is converted.
// The leftmost column is numbered 0.
pub fun statement/float64(stmt: statement, col: int): float64
  capi/column-double(stmt.cref, col.int32)

// Get the result value of a column as an integer.
// If the result does not have integer affinity, it is converted.
// The leftmust column is numbered 0.
pub fun statement/int(stmt: statement, col: int): int
  capi/column-integer(stmt.cref, col.int32)

// Get the result value of a column as a string.
// If the actual result is NULL, the result is the empty string.
// Otherwise, if the result does not have text affinity, it is converted.
// The leftmost column is numbeered 0.
pub fun statement/text(stmt: statement, col: int): string
  capi/column-text(stmt.cref, col.int32)

pub fun statement/bind-float64(stmt: statement, col: int, value: float64): sqlite-result
  capi/bind-double(stmt.cref, col.int32, value).trusted-result

pub fun statement/bind-int64(stmt: statement, col: int, value: int64): sqlite-result
  capi/bind-int64(stmt.cref, col.int32, value).trusted-result

pub fun statement/bind-null(stmt: statement, col: int): sqlite-result
  capi/bind-null(stmt.cref, col.int32).trusted-result

pub fun statement/bind-text(stmt: statement, col: int, value: string): sqlite-result
  capi/bind-text(stmt.cref, col.int32, value).trusted-result

pub fun statement/bind-zeroblob(stmt: statement, col: int, length: int64): sqlite-result
  capi/bind-zeroblob(stmt.cref, col.int32, length).trusted-result

// Set all bound parameters to NULL.
pub fun statement/clear-bindings(stmt: statement): sqlite-result
  capi/clear-bindings(stmt.cref).trusted-result
