module sqlite3;

import c.sqlite3, std.string;

pragma(lib, "sqlite3");

extern(C) {
  int sqlite3_exec(sqlite3*, char*, int function(void*, int, char**, char**), void*, char**);
  void sysFree(void* c) { free c; }
  int sqlite3_bind_text(sqlite3_stmt*, int, char*, int, void function(void*));
  int sqlite3_bind_blob(sqlite3_stmt*, int, void*, int, void function(void*));
}

int callback(void* threadinfo, int argc, char** argv, char** colName) {
  auto _threadlocal = getThreadlocal;
  while int i <- 0..argc {
    writeln "$(CToString colName[i]) = $(CToString argv[i])";
  }
  writeln "";
  return 0;
}

void* mallocDupData(byte[] data) {
  auto res = malloc data.length;
  memcpy(res, data.ptr, data.length);
  return res;
}

class SQLiteError : Error {
  void init(string s) { super.init "SQLiteError: $s"; }
}

class SQLiteBusy : SQLiteError {
  void init() { super.init "SQLiteBusy: Database busy. "; }
}

interface IQueryIterator {
  void finalize();
  bool isFinalized();
}

shared (string, int, int)[] profiledata;

shared bool profile;
void record(string qs, int ms) {
  if (!profile) return;
  // TODO: sync
  for ref tup <- profiledata {
    if (tup[0] == qs) { tup[1] ++; tup[2] += ms; return; }
  }
  profiledata ~= (qs, 1, ms);
}

template QueryIterator(T) {
  class QueryIterator : IQueryIterator {
    Database db;
    sqlite3_stmt* stmt;
    int bindCount;
    string sql;
    bool finalized;
    bool isFinalized() { return finalized; }
    template addBind(T) {
      int addBind(T t) {
        static if types-equal(T, int) || types-equal(T, bool) {
          return sqlite3_bind_int(stmt, bindCount ++, t);
        }
        static if types-equal(T, float) || types-equal(T, double) {
          return sqlite3_bind_double(stmt, bindCount ++, double:t);
        }
        static if types-equal(T, string) {
          return sqlite3_bind_text(stmt, bindCount ++, char*: mallocDupData byte[]:t, t.length, &sysFree);
        }
        static if types-equal(T, byte[]) || types-equal(T, ubyte[]) {
          return sqlite3_bind_blob(stmt, bindCount ++, mallocDupData byte[]:t, t.length, &sysFree);
        }
        raise new Error "Unknown bind type $(string-of T)";
      }
    }
    template getColumn(T) {
      T getColumn(int i) {
        static if types-equal(T, int) {
          return sqlite3_column_int(stmt, i);
        }
        static if types-equal(T, float) {
          return float:sqlite3_column_double(stmt, i);
        }
        static if types-equal(T, string) {
          auto resptr = sqlite3_column_text(stmt, i);
          auto reslen = sqlite3_column_bytes(stmt, i);
          return resptr[0..reslen];
        }
        static if types-equal(T, byte[]) || types-equal(T, ubyte[]) {
          auto resptr = byte*:sqlite3_column_blob(stmt, i);
          auto reslen = sqlite3_column_bytes(stmt, i);
          return T:resptr[0..reslen];
        }
        raise new Error "Unknown column type $(string-of T)";
      }
    }
    T value;
    void finalize() {
      if finalized {
        raise new SQLiteError "tried to double finalize statement";
      }
      finalized = true;
      sqlite3_finalize stmt;
    }
    bool advance() {
      import std.time;
      auto start = msec();
      onSuccess record(sql, msec() - start);
      define-exit "retry" { }
      auto res = sqlite3_step stmt;
      if res == SQLITE_DONE {
        finalize();
        return false;
      }
      if res == SQLITE_BUSY raise new SQLiteBusy;
      if res != SQLITE_ROW
        raise new SQLiteError "Failed to iterate query '$sql': $res: $(CToString sqlite3_errmsg db.db)";
      static if type-is tuple T {
        static if value.length {
          static while int i <- 0 .. value.length {
            value[i] = getColumn!type-of value[i] i;
          }
        }
      } else {
        value = getColumn!T 0;
      }
      return true;
    }
    void init(Database db, sqlite3_stmt* stmt) {
      this.db = db;
      this.stmt = stmt;
      bindCount = 1;
    }
  }
}

class Database {
  sqlite3* db;
  IQueryIterator[auto~][] stack;
  void openStatementList() {
    IQueryIterator[auto~] qi;
    stack ~= qi;
  }
  void finStatementList() {
    (IQueryIterator[auto~] qi, stack) = stack[($-1, 0..$-1)];
    for auto i <- qi if !i.isFinalized() i.finalize();
  }
  void sqlite3fail(string msg) {
    raise new Error "in sqlite3: $msg";
  }
  void mkfast() {
    exec("pragma cache_size=100000"); // 100MB may be used for cache
  }
  void init(string dbname) {
    if sqlite3_open(toStringz dbname, &db)
      sqlite3fail CToString sqlite3_errmsg db;
  }
  template exec(U) {
    template exec(T) {
      auto exec(U u) {
        define-exit "retry" { }
        static if type-is tuple U { string sql = u[0]; }
        else { string sql = u; }
        sqlite3_stmt* stmt;
        if SQLITE_OK != auto result = sqlite3_prepare_v2(
          db,
          sql.ptr, sql.length,
          &stmt,
          null
        ) {
          if (result == SQLITE_BUSY) raise new SQLiteBusy;
          sqlite3fail "while executing $(sql): $(CToString sqlite3_errmsg db)";
        }
        
        T bogosity;
        
        auto res = new QueryIterator!T (this, stmt);
        res.sql = sql;
        
        if (stack.length) stack[$-1] ~= res;
        
        static if type-is tuple U {
          static while int i <- 0 .. (u.length - 1) {
            res.addBind u[1+i];
          }
        }
        static if type-is tuple T {
          static if bogosity.length == 0 {
            res.advance();
            return;
          } else {
            return res;
          }
        } else {
          return res;
        }
      }
    }
  }
  void close() {
    sqlite3_close db;
  }
}