
{.deadCodeElim: on.}

import json, times

import db_connector/db_common
import db_connector/sqlite3
import query_hooks
export db_common

type
  DbConn* = PSqlite3  ## encapsulates a database connection

  varcharType* = string
  blobType* = seq[byte]
  intType* = int
  floatType* = float
  boolType* = bool
  timestampType* = DateTime
  jsonType* = JsonNode

var jsonTimeFormat* = "yyyy-MM-dd HH:mm:ss"

proc dbError*(db: DbConn) {.noreturn.} =
  ## raises a DbError exception.
  var e: ref DbError
  new(e)
  e.msg = $sqlite3.errmsg(db)
  raise e

proc prepareStmt*(db: DbConn; q: string): PStmt =
  when defined(debugOrminTrace):
    echo "[[Ormin Executing]]: ", q
  if prepare_v2(db, q, q.len.cint, result, nil) != SQLITE_OK:
    dbError(db)

template startBindings*(s: PStmt; n: int) =
  if clear_bindings(s) != SQLITE_OK: dbError(db)

template bindParam*(db: DbConn; s: PStmt; idx: int; x, t: untyped) =
  #when not (x is t):
  #  {.error: "type mismatch for query argument at position " & $idx.}
  when t is blobType:
    let xs = x
    let blobPtr = if xs.len == 0:
        cast[pointer](nil)
      else:
        cast[pointer](unsafeAddr(xs[0]))
    if bind_blob(s, idx.cint, blobPtr, xs.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
      dbError(db)
  elif t is int or t is int64 or t is bool:
    if bind_int64(s, idx.cint, x.int64) != SQLITE_OK: dbError(db)
  elif t is string:
    if bind_text(s, idx.cint, cstring(x), x.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
      dbError(db)
  elif t is float64:
    if bind_double(s, idx.cint, x) != SQLITE_OK:
      dbError(db)
  elif t is DateTime:
    let xx = if x.nanosecond > 0:
      x.utc().format("yyyy-MM-dd HH:mm:ss\'.\'fff")
    else:
      x.utc().format("yyyy-MM-dd HH:mm:ss")

    if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
      dbError(db)
  elif t is JsonNode:
    let xx = $x
    if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
      dbError(db)
  else:
    {.error: "type mismatch for query argument at position " & $idx.}

template bindNullParam*(db: DbConn; s: PStmt; idx: int) =
  if bind_null(s, idx.cint) != SQLITE_OK:
    dbError(db)

template bindParamJson*(db: DbConn; s: PStmt; idx: int; xx: JsonNode;
                        t: typedesc) =
  let x = xx
  if x.kind == JNull:
    if bind_null(s, idx.cint) != SQLITE_OK: dbError(db)
  else:
    bindFromJson(db, s, idx, x, t)

template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
                       t: typedesc) =
  {.error: "invalid type for JSON object".}

template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
                       t: typedesc[string]) =
  doAssert x.kind == JString
  let xs = x.str
  if bind_text(s, idx.cint, cstring(xs), xs.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
    dbError(db)

template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
                       t: typedesc[int|int64]) =
  doAssert x.kind == JInt
  let xi = x.num
  if bind_int64(s, idx.cint, xi.int64) != SQLITE_OK: dbError(db)

template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
                       t: typedesc[float64]) =
  doAssert x.kind == JFloat
  let xf = x.fnum
  if bind_double(s, idx.cint, xf) != SQLITE_OK:
    dbError(db)

template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
                       t: typedesc[bool]) =
  doAssert x.kind == JBool
  let xb = x.bval
  if bind_int64(s, idx.cint, xb.int64) != SQLITE_OK: dbError(db)

template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
                       t: typedesc[DateTime]) =
  doAssert x.kind == JString
  let
    dtStr = x.str
    i = dtStr.find('.')
    dt = if i > 0 and dtStr[i+1 .. ^1] == "000":
      dtStr[0 ..< i]
    else:
      dtStr
  if bind_text(s, idx.cint, cstring(dt), dt.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
    dbError(db)

template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int;
                     t: typedesc; name: string) =
  dest = int column_int64(s, idx.cint)

template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int64;
                     t: typedesc; name: string) =
  dest = column_int64(s, idx.cint)

proc fillString(dest: var string; src: cstring; srcLen: int) =
  when defined(nimNoNilSeqs):
    setLen(dest, srcLen)
  else:
    if dest.isNil: dest = newString(srcLen)
    else: setLen(dest, srcLen)
  if srcLen > 0: 
    copyMem(unsafeAddr(dest[0]), src, srcLen)

proc fillBytes(dest: var seq[byte]; src: pointer; srcLen: int) =
  when defined(nimNoNilSeqs):
    setLen(dest, srcLen)
  else:
    if dest.isNil: dest = newSeq[byte](srcLen)
    else: setLen(dest, srcLen)
  if srcLen > 0:
    copyMem(unsafeAddr(dest[0]), src, srcLen)

template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var string;
                     t: typedesc; name: string) =
  if column_type(s, idx.cint) == SQLITE_NULL:
    when defined(nimNoNilSeqs):
      setLen(dest, 0)
    else:
      dest = nil
  else:
    let srcLen = column_bytes(s, idx.cint)
    let src = column_text(s, idx.cint)
    fillString(dest, src, srcLen)

template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var blobType;
                     t: typedesc; name: string) =
  let srcLen = column_bytes(s, idx.cint)
  let src = column_blob(s, idx.cint)
  fillBytes(dest, src, srcLen)

template bindResult*(db: DbConn; s: PStmt; idx: int; dest: float64;
                     t: typedesc; name: string) =
  dest = column_double(s, idx.cint)

template bindResult*(db: DbConn; s: PStmt; idx: int; dest: bool;
                     t: typedesc; name: string) =
  dest = column_int64(s, idx.cint) != 0

template bindResult*(db: DbConn; s: PStmt; idx: int; dest: DateTime;
                     t: typedesc; name: string) =
  let
    src = $column_text(s, idx.cint)
    i = src.find('.')
  if i < 0:
    dest = parse(src, "yyyy-MM-dd HH:mm:ss", utc())
  else:
    dest = parse(src, "yyyy-MM-dd HH:mm:ss\'.\'fff", utc())

template bindResult*(db: DbConn; s: PStmt; idx: int; dest: JsonNode;
                     t: typedesc; name: string) =
  let src = column_text(s, idx.cint)
  dest = parseJson($src)

template bindResult*[T](db: DbConn; s: PStmt; idx: int; dest: var DbValue[T];
                        t: typedesc; name: string) =
  if column_type(s, idx.cint) == SQLITE_NULL:
    dest.isNull = true
  else:
    dest.isNull = false
    when T is string:
      let srcLen = column_bytes(s, idx.cint)
      let src = column_text(s, idx.cint)
      fillString(dest.value, src, srcLen)
    else:
      bindResult(db, s, idx, dest.value, t, name)

template createJObject*(): untyped = newJObject()
template createJArray*(): untyped = newJArray()

template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
                         t: typedesc; name: string) =
  let x = obj
  doAssert x.kind == JObject
  if column_type(s, idx.cint) == SQLITE_NULL:
    x[name] = newJNull()
  else:
    bindToJson(db, s, idx, x, t, name)

template columnIsNull*(db: DbConn; s: PStmt; idx: int): bool =
  column_type(s, idx.cint) == SQLITE_NULL

template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
                     t: typedesc; name: string) =
  {.error: "invalid type for JSON object".}

template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
                     t: typedesc[string]; name: string) =
  let dest = newJString("")
  let srcLen = column_bytes(s, idx.cint)
  let src = column_text(s, idx.cint)
  fillString(dest.str, src, srcLen)
  obj[name] = dest

template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
                     t: typedesc[int|int64]; name: string) =
  obj[name] = newJInt(column_int64(s, idx.cint))

template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
                     t: typedesc[blobType]; name: string) =
  let srcLen = column_bytes(s, idx.cint)
  let src = column_blob(s, idx.cint)
  var data: blobType
  fillBytes(data, src, srcLen)
  let arr = newJArray()
  for b in data:
    arr.add newJInt(int(b))
  obj[name] = arr

template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
                     t: typedesc[float64]; name: string) =
  obj[name] = newJFloat(column_double(s, idx.cint))

template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
                     t: typedesc[bool]; name: string) =
  obj[name] = newJBool(column_int64(s, idx.cint) != 0)

template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
                     t: typedesc[DateTime]; name: string) =
  var dt: DateTime
  bindResult(db, s, idx, dt, t, name)
  obj[name] = newJString(format(dt, jsonTimeFormat))

template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
                     t: typedesc[JsonNode]; name: string) =
  let src = column_text(s, idx.cint)
  obj[name] = parseJson($src)

template startQuery*(db: DbConn; s: PStmt) = discard "nothing to do"

template stopQuery*(db: DbConn; s: PStmt) =
  if sqlite3.reset(s) != SQLITE_OK: dbError(db)

template stepQuery*(db: DbConn; s: PStmt; returnsData: bool): bool =
  when returnsData:
    step(s) == SQLITE_ROW
  else:
    step(s) == SQLITE_DONE

template getLastId*(db: DbConn; s: PStmt): int =
  int(last_insert_rowid(db))

template getAffectedRows*(db: DbConn; s: PStmt): int =
  int(changes(db))

proc close*(db: DbConn) =
  ## closes the database connection.
  if sqlite3.close(db) != SQLITE_OK: dbError(db)

proc open*(connection, user, password, database: string): DbConn =
  ## opens a database connection. Raises `EDb` if the connection could not
  ## be established. Only the ``connection`` parameter is used for ``sqlite``.
  if sqlite3.open(connection, result) != SQLITE_OK:
    dbError(result)
