/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 
/* vim: set ts=2 et sw=2 tw=80: */
/*
** Copyright (c) 2013 D. Richard Hipp
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the Simplified BSD License (also
** known as the "2-Clause License" or "FreeBSD License".)
**
** This program is distributed in the hope that it will be useful,
** but without any warranty; without even the implied warranty of
** merchantability or fitness for a particular purpose.
**
** Author contact information:
**   drh@hwaci.com
**   http://www.hwaci.com/drh/
**
*******************************************************************************
** This file houses the db-related v2 APIs.
*/
#include "fossil/fossil2.h"
#include "fsl_internal.h"
#include <assert.h>
#include <stddef.h> /* NULL on linux */
#include <time.h> /* time() */

/* Only for debugging */
#define MARKER(pfexp)                                               \
  do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__);   \
    printf pfexp;                                                   \
  } while(0)

#if 0
/*
**
** Helper which sets fsl_err_set() to some string
** pulled from the db connection. dbCode should be
** the result code which came from an error'd
** sqlite API call.
*/
static int fsl_err_repo_db( fsl_ctx * f, int dbCode ){
  assert(f && f->dbRepo.dbh);
  return fsl_err_set(f, FSL_RC_DB,
                     "Db error #%d: %s",
                     dbCode, sqlite3_errmsg(f->dbRepo.dbh));
}
#endif

/*
** Resets db->error state based on the given code and the current
** error string from the driver.  Returns FSL_RC_DB on success, some
** other non-0 value on error (most likely FSL_RC_OOM while allocating
** the error string).
*/
static int fsl_err_from_db( fsl_db * db, int dbCode ){
  int rc;
  assert(db && db->dbh);
  db->error.msg.used =0 ;
  rc = fsl_buffer_appendf(&db->error.msg, "Db error #%d: %s",
                          dbCode, sqlite3_errmsg(db->dbh));
  return rc ? rc : FSL_RC_DB;
}

int fsl_db_close( fsl_db * db ){
  if(!db) return FSL_RC_MISUSE;
  else{
    if(db->dbh){
      sqlite3_close(db->dbh)
        /* ignoring result in the style of
           "destructors may not throw".
        */;
    }
    if(db->filename.mem){
      fsl_buffer_reserve(&db->filename, 0);
    }
    assert(0==db->openStatementCount);
    *db = fsl_db_empty;
    return 0;
  }
}

int fsl_repo_close( fsl_ctx * f ){
  if(!f) return FSL_RC_MISUSE;
  else if(!f->dbRepo.dbh) return FSL_RC_NOT_A_REPO;
  else return fsl_db_close( &f->dbRepo );
}

/*
** This file contains the fsl_db_xxx() and fsl_stmt_xxx()
** parts of the API.
*/
int fsl_stmt_preparev( fsl_db *db, fsl_stmt * tgt, char const * sql, va_list args ){
  if(!db || !db->dbh || !tgt || !sql) return FSL_RC_MISUSE;
  else if(!*sql) return FSL_RC_RANGE;
  else if(tgt->stmt) return FSL_RC_ALREADY_EXISTS;
  else{
    int rc;
    fsl_buffer buf = fsl_buffer_empty;
    fsl_stmt_t * liteStmt = NULL;
    fsl_ctx * f = db->f;
    rc = fsl_buffer_appendfv( &buf, sql, args );
    if(!rc){
      sql = fsl_buffer_cstr(&buf);
      rc = sqlite3_prepare_v2(db->dbh, sql, (int)buf.used,
                              &liteStmt, 0);
      if(rc){
        rc = fsl_err_set(f, rc, "Db statement preparation failed. "
                         "SQL: [%.*s]. Error #%d: %s\n",
                         (int)buf.used, sql, rc,
                         sqlite3_errmsg(db->dbh));
      }
    }
    if(!rc){
      ++db->openStatementCount;
      tgt->stmt = liteStmt;
      tgt->db = db;
      tgt->sql = buf /*transfer ownership*/;
      tgt->colCount = sqlite3_column_count(tgt->stmt);
      tgt->paramCount = sqlite3_bind_parameter_count(tgt->stmt);
    }else{
      assert(!liteStmt);
      fsl_buffer_reserve(&buf, 0);
    }
    return rc;
  }
}

int fsl_stmt_prepare( fsl_db *db, fsl_stmt * tgt, char const * sql, ... ){
  if(!db || !db->dbh || !tgt || !sql) return FSL_RC_MISUSE;
  else if(!*sql) return FSL_RC_RANGE;
  else if(!tgt->stmt) return FSL_RC_ALREADY_EXISTS;
  else{
    int rc;
    va_list args;
    va_start(args,sql);
    rc = fsl_stmt_preparev( db, tgt, sql, args );
    va_end(args);
    return rc;
  }
}

int fsl_repo_prepare( fsl_ctx *f, fsl_stmt * tgt, char const * sql,
                      ... ){
  if(!f) return FSL_RC_MISUSE;
  else if(!f->dbRepo.dbh) return FSL_RC_NOT_A_REPO;
  else{
    int rc;
    va_list args;
    va_start(args,sql);
    rc = fsl_stmt_preparev( &f->dbRepo, tgt, sql, args );
    va_end(args);
    return rc;
  }
}

int fsl_repo_preparev( fsl_ctx *f, fsl_stmt * tgt, char const * sql,
                       va_list args ){
  if(!f) return FSL_RC_MISUSE;
  else if(!f->dbRepo.dbh) return FSL_RC_NOT_A_REPO;
  else return fsl_stmt_preparev(&f->dbRepo, tgt, sql, args);
}


int fsl_stmt_finalize( fsl_stmt * stmt ){
  if(!stmt) return FSL_RC_MISUSE;
  else{
    assert(stmt->stmt);
    assert(stmt->db);
    --stmt->db->openStatementCount;
    fsl_buffer_reserve(&stmt->sql, 0);
    sqlite3_finalize( stmt->stmt );
    *stmt = fsl_stmt_empty;
    return 0;
  }
}

fsl_step_t fsl_stmt_step( fsl_stmt * stmt ){
  if(!stmt || !stmt->stmt) return FSL_RC_MISUSE;
  else{
    int const rc = sqlite3_step(stmt->stmt);
    assert(stmt->db);
    switch( rc ){
      case SQLITE_ROW:
        return FSL_STEP_ROW;
      case SQLITE_DONE:
        return FSL_STEP_DONE;
      default:
        stmt->db->error.code = FSL_RC_DB;
        fsl_buffer_appendf(&stmt->db->error.msg,
                           "Db error #%d: %s",
                           rc, sqlite3_errmsg(stmt->db->dbh));
        return FSL_STEP_ERROR;
    }
  }
}

int fsl_stmt_each( fsl_stmt * stmt, fsl_stmt_each_f callback,
                   void * callbackState ){
  if(!stmt || !callback) return FSL_RC_MISUSE;
  else{
    fsl_step_t strc;
    int rc = 0;
    char doBreak = 0;
    while( !doBreak && (FSL_STEP_ROW == (strc=fsl_stmt_step(stmt)))){
      rc = callback( stmt, callbackState );
      switch(rc){
        case 0: continue;
        case FSL_RC_BREAK:
          rc = 0;
          /* fall through */
        default:
          doBreak = 1;
          break;
      }
    }
    return rc
      ? rc
      : ((FSL_STEP_ERROR==strc)
         ? FSL_RC_DB
         : 0);
  }
}

int fsl_stmt_reset( fsl_stmt * stmt ){
  if(!stmt || !stmt->stmt) return FSL_RC_MISUSE;
  else{
    int const rc = sqlite3_reset(stmt->stmt);
    assert(stmt->db);
    return rc
      ? fsl_err_from_db(stmt->db, rc)
      : 0;
  }
}

int fsl_stmt_col_count( fsl_stmt const * stmt ){
  return (!stmt || !stmt->stmt)
    ? -1
    : stmt->colCount
    ;
}

int fsl_stmt_param_count( fsl_stmt const * stmt ){
  return (!stmt || !stmt->stmt)
    ? -1
    : stmt->paramCount;
}

#define BIND_PARAM_CHECK \
  if(!stmt || !stmt->stmt || !stmt->db) return FSL_RC_MISUSE; else
int fsl_stmt_bind_null( fsl_stmt * stmt, int ndx ){
  BIND_PARAM_CHECK {
    int const rc = sqlite3_bind_null( stmt->stmt, ndx );
    return rc ? fsl_err_from_db(stmt->db, rc) : 0;
  }
}

int fsl_stmt_bind_int32( fsl_stmt * stmt, int ndx, fsl_int32_t v ){
  BIND_PARAM_CHECK {
    int const rc = sqlite3_bind_int( stmt->stmt, ndx, (int)v );
    return rc ? fsl_err_from_db(stmt->db, rc) : 0;
  }
}

int fsl_stmt_bind_int64( fsl_stmt * stmt, int ndx, fsl_int64_t v ){
  BIND_PARAM_CHECK {
    int const rc = sqlite3_bind_int64( stmt->stmt, ndx, (sqlite3_int64)v );
    return rc ? fsl_err_from_db(stmt->db, rc) : 0;
  }
}

int fsl_stmt_bind_double( fsl_stmt * stmt, int ndx, fsl_double_t v ){
  BIND_PARAM_CHECK {
    int const rc = sqlite3_bind_double( stmt->stmt, ndx, (double)v );
    return rc ? fsl_err_from_db(stmt->db, rc) : 0;
  }
}
#undef BIND_PARAM_CHECK

#define GET_CHECK if(!stmt || !stmt->colCount) return FSL_RC_MISUSE; \
  else if((ndx<0) || (ndx>=stmt->colCount)) return FSL_RC_RANGE
int fsl_stmt_get_int32( fsl_stmt * stmt, int ndx, fsl_int32_t * v ){
  GET_CHECK;
  else {
    if(v) *v = (fsl_int32_t)sqlite3_column_int(stmt->stmt, ndx);
    return 0;
  }
}
int fsl_stmt_get_int64( fsl_stmt * stmt, int ndx, fsl_int64_t * v ){
  GET_CHECK;
  else {
    if(v) *v = (fsl_int64_t)sqlite3_column_int64(stmt->stmt, ndx);
    return 0;
  }
}

int fsl_stmt_get_double( fsl_stmt * stmt, int ndx, fsl_double_t * v ){
  GET_CHECK;
  else {
    if(v) *v = (fsl_double_t)sqlite3_column_double(stmt->stmt, ndx);
    return 0;
  }
}

int fsl_stmt_get_text( fsl_stmt * stmt, int ndx, char const **out,
                       fsl_int_t * outLen ){
  GET_CHECK;
  else {
    unsigned char const * t = (out || outLen)
      ? sqlite3_column_text(stmt->stmt, ndx)
      : NULL;
    if(out) *out = (char const *)t;
    if(outLen) *outLen = t ? sqlite3_column_bytes(stmt->stmt, ndx) : 0;
    return 0;
  }
}

int fsl_stmt_get_blob( fsl_stmt * stmt, int ndx, void const **out,
                       fsl_int_t * outLen ){
  GET_CHECK;
  else {
    void const * t = (out || outLen)
      ? sqlite3_column_blob(stmt->stmt, ndx)
      : NULL;
    if(out) *out = (char const *)t;
    if(outLen) *outLen = t ? sqlite3_column_bytes(stmt->stmt, ndx) : 0;
    return 0;
  }
}

#undef GET_CHECK

static void fsl_db_sql_trace(void *fslCtx, const char *zSql){
  int const n = fsl_strlen(zSql);
  static int counter = 0;
  /* FIXME: in v1 this uses fossil_trace(), but don't have
     that functionality here yet. */
  fsl_outputf((fsl_ctx*)fslCtx,
              "SQL TRACE #%d: %s%s\n", ++counter,
              zSql, (n>0 && zSql[n-1]==';') ? "" : ";");
}

/*
** SQL functions for debugging.
**
** The print() function writes its arguments to fsl_output(), but only
** if the -sqlprint command-line option is turned on.
*/
static void fsl_db_sql_print(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
  fsl_ctx * f = (fsl_ctx*)sqlite3_user_data(context);
  if( f->debug.printSql ){
    int i;
    for(i=0; i<argc; i++){
      char c = i==argc-1 ? '\n' : ' ';
      fsl_outputf(f, "%s%c", sqlite3_value_text(argv[i]), c);
    }
  }
}

/*
** Function to return the number of seconds since 1970.  This is
** the same as strftime('%s','now') but is more compact.
*/
static void fsl_db_now_function(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
  sqlite3_result_int64(context, time(0));
}

/*
** Function to return the check-in time for a file.
*/
static void fsl_db_checkin_mtime_function(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
#if 0
  /* TODO, once we can parse manifests. */
  fsl_time_t mtime;
  int rc = mtime_of_manifest_file(sqlite3_value_int(argv[0]),
                                  sqlite3_value_int(argv[1]), &mtime);
  if( rc==0 ){
    sqlite3_result_int64(context, (sqlite_int64)mtime);
  }
#endif
}

/*
** Implement the user() SQL function.  user() takes no arguments and
** returns the user ID of the current user.
*/
static void fsl_db_sql_user(
  sqlite3_context *context,
  int argc,
  sqlite3_value **argv
){
#if 0
  if( g.zLogin!=0 ){
    sqlite3_result_text(context, g.zLogin, -1, SQLITE_STATIC);
  }
#else
  sqlite3_result_text(context, "FIXME: missing g.zLogin info", -1, SQLITE_STATIC);
#endif
}

/*
** Returns non-0 (true) if the given SQL would result
** in at least one result row, else 0 (false).
*/
char fsl_db_val_exists( fsl_db * db, char const * sql, ...){
  fsl_stmt st = fsl_stmt_empty;
  int rc;
  va_list args;
  if(!db || !sql) return 0;
  va_start(args,sql);
  rc = fsl_stmt_preparev( db, &st, sql, args );
  va_end(args);
  if(rc) return 0;
  else{
    rc = (FSL_STEP_ROW == fsl_stmt_step(&st)) ? 1 : 0;
    fsl_stmt_finalize( &st );
    return rc;
  }
}

/*
** Return TRUE if the schema is out-of-date
*/
int fsl_db_schema_is_outofdate(fsl_db *db){
  return fsl_db_val_exists(db, "SELECT 1 FROM config "
                           "WHERE name='aux-schema' "
                           "AND value<>'%s'",
                           FSL_AUX_SCHEMA);
}


/*
** Returns - if db appears to be a current repository,
** 1 if it appears to have an out of date schema, and
** -1 if it appears to not be a repository.
*/
int fsl_repo_verify_schema(fsl_db * db){
  if(fsl_db_schema_is_outofdate(db)) return 1;
  else return fsl_db_val_exists(db,
                                "SELECT 1 FROM config "
                                "WHERE name='project-code'")
    ? 0 : -1;
}

int fsl_db_open( fsl_db * db, char const * repoDbFile,
                 int openFlags ){
  if(!db || !repoDbFile || !*repoDbFile) return FSL_RC_MISUSE;
  else if(db->dbh) return FSL_RC_MISUSE;
  else{
    fsl_dbh_t * dbh = NULL;
    int rc = sqlite3_open_v2( repoDbFile,
                              &dbh,
                              SQLITE_OPEN_READWRITE,
                              NULL );
    if(rc){
      if(dbh){
        /* By some complete coincidence, FSL_RC_DB==SQLITE_CANTOPEN. */
        fsl_buffer_appendf(&db->error.msg,
                           "Opening db file [%s] failed with "
                           "sqlite code #%d: %s",
                           repoDbFile, rc, sqlite3_errmsg(dbh));
      }else{
        fsl_buffer_appendf(&db->error.msg,
                           "Opening db file [%s] failed with "
                           "sqlite code #%d",
                           repoDbFile, rc);
      }
      rc = FSL_RC_DB;
    }

    if(!rc){
      rc = fsl_buffer_append(&db->filename, repoDbFile, -1);
      if(rc){
        assert((FSL_RC_OOM==rc) && "That's the only logical explanation.");
      }
    }

    if(rc){
      db->error.code = rc;
      if(dbh){
        sqlite3_close(dbh);
      }
    }else{
      db->dbh = dbh;
    }
    return rc;
  }
}


int fsl_repo_open_db( fsl_ctx * f, char const * repoDbFile,
                      int openFlags ){
  fsl_dbh_t * dbh = NULL;
  if(!f || !repoDbFile || !*repoDbFile) return FSL_RC_MISUSE;
  else if(f->dbRepo.dbh) return FSL_RC_MISUSE;
  if(!(FSL_DB_OPEN_NOT_FOUND_OK & openFlags)
     && fsl_file_access(repoDbFile, 0)){
    return FSL_RC_NOT_FOUND;
  }
  else {
    int rc = sqlite3_open_v2( repoDbFile,
                              &dbh,
                              SQLITE_OPEN_READWRITE,
                              NULL );
    if(rc){
      if(dbh){
        /* By some complete coincidence, FSL_RC_DB==SQLITE_CANTOPEN. */
        rc = fsl_err_set(f, FSL_RC_DB,
                         "Opening db file [%s] failed with "
                         "sqlite code #%d: %s",
                         repoDbFile, rc, sqlite3_errmsg(dbh));
      }else{
        rc = fsl_err_set(f, FSL_RC_DB,
                         "Opening db file [%s] failed with "
                         "sqlite code #%d",
                         repoDbFile, rc);

      }
      rc = FSL_RC_DB;
      goto end;
    }
    if( f->debug.traceSql ){
      sqlite3_trace(dbh, fsl_db_sql_trace, f);
    }
    if(!(FSL_DB_OPEN_NO_SCHEMA_VALIDATE & openFlags)){
      int check;
      fsl_db dbTmp = fsl_db_empty;
      dbTmp.f = f;
      dbTmp.dbh = dbh;
      check = fsl_repo_verify_schema(&dbTmp);
      if(0 != check){
        rc = (check<0)
          ? fsl_err_set(f, FSL_RC_NOT_A_REPO,
                        "DB file [%s] does not appear to be "
                        "a repository.", repoDbFile)
          : fsl_err_set( f, FSL_RC_REPO_NEEDS_REBUILD,
                         "DB file [%s] appears to be a fossil "
                         "repsitory, but is out-of-date and needs "
                         "a rebuild.",
                         repoDbFile)
          ;
        goto end;
      }
    }
      

    /* This all comes from db.c:db_open()... */
    /* FIXME: check result codes here. */
    sqlite3_busy_timeout(dbh, 5000 /* historical value */);
    sqlite3_wal_autocheckpoint(dbh, 1);  /* Set to checkpoint frequently */
    sqlite3_exec(dbh, "PRAGMA foreign_keys=OFF;", 0, 0, 0);
    sqlite3_create_function(dbh, "now", 0, SQLITE_ANY, f,
                            fsl_db_now_function, 0, 0);
    sqlite3_create_function(dbh, "checkin_mtime", 2, SQLITE_ANY, f,
                            fsl_db_checkin_mtime_function, 0, 0);
    sqlite3_create_function(dbh, "user", 0, SQLITE_ANY, f,
                            fsl_db_sql_user, 0, 0);
    if( f->debug.printSql ){
      sqlite3_create_function(dbh, "print", -1, SQLITE_UTF8, f,
                              fsl_db_sql_print,0,0);
    }
#if 0
    /* functions registered in v1 by db.c:db_open(). */
    /* porting cgi() requires access to the HTTP/CGI layer. i.e. this belongs
       downstream. */
    sqlite3_create_function(dbh, "cgi", 1, SQLITE_ANY, 0, db_sql_cgi, 0, 0);
    sqlite3_create_function(dbh, "cgi", 2, SQLITE_ANY, 0, db_sql_cgi, 0, 0);
    
    /*
      i[sf]_selected() both require access to the array of file
      names being considered for commit: g.aCommitFile[]
    */
    sqlite3_create_function(dbh, "is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0
                            );
    sqlite3_create_function(
                            dbh, "if_selected", 3, SQLITE_UTF8, 0, file_is_selected,0,0
                            );

    re_add_sql_func(db) /* Requires the regex bits. */;
#endif

    end:
    if(!rc){
      f->dbRepo.dbh = dbh;
      f->dbRepo.f = f;
      rc = fsl_buffer_append(&f->dbRepo.filename, repoDbFile, -1);
      if(rc){
        assert((FSL_RC_OOM==rc) && "That's the only logical explanation.");
        return rc;
      }
    }else{
      if(dbh){
        sqlite3_close(dbh);
      }
    }
    return rc;
  }
}


#undef MARKER
