/**
 * Copyright (c) 2008 Jeremy Hinegardner
 * All rights reserved.  See LICENSE and/or COPYING for details.
 *
 * vim: shiftwidth=4 
 */ 

#include "amalgalite3.h"
#include <stdio.h>
extern VALUE mA;
VALUE cAR;
VALUE cARB;
VALUE eARB_Error;

/* 
 * cleanup the datatbase and statment values if they are currently open and then
 * raise the message.  It is assumed that the *msg pointer passed in is NOT
 * allocated via malloc() or the like.  It should be a local static buffer
 * that we do not have to worry about freeing.
 */
void am_bootstrap_cleanup_and_raise( const char* msg, sqlite3* db, sqlite3_stmt* stmt )
{

    if ( NULL != stmt ) { sqlite3_finalize( stmt ); stmt = NULL; }
    if ( NULL != db   ) { sqlite3_close( db ); }

    rb_raise(eARB_Error, "%s", msg );
}


/**
 * call-seq:
 *   Amalgalite::Requires::Bootstrap.lift( 'dbfile' => "lib.db", 'table_name' => "bootload", 'rowid_column' => "id", 'filename_column' => "filename",  'content_column' => "contents" )
 *
 * *WARNING* *WARNING* *WARNING* *WARNING* *WARNING* *WARNING* *WARNING*
 *
 * This is a boostrap mechanism to eval all the code in a particular column in a
 * specially formatted table in an sqlite database.  It should only be used for
 * a specific purpose, mainly loading the Amalgalite ruby code directly from an
 * sqlite table.  
 *
 * Amalgalite::Requires adds in the ability to _require_ code that is in an
 * sqlite database.  Since Amalgalite::Requires is itself ruby code, if
 * Amalgalite::Requires was in an sqlite database, it could not _require_
 * itself.  Therefore this method is made available.  It is a pure C extension
 * method that directly calls the sqlite3 C functions directly and uses the ruby
 * C api to eval the data in the table.
 *
 * This method attaches to an sqlite3 database (filename) and then does:
 *
 *     SELECT filename_column_name, content_column_name 
 *       FROM table_name
 *   ORDER BY rowid_column_name
 *
 * For each row returned it does an _eval_ on the code in the
 * *content_column_name* and then updates _$LOADED_FEATURES_ directly with the value from
 * *filename_column_name*.
 *
 * The database to be opened by _lift_ *must* be an sqlite3 UTF-8 database.
 *
 */
VALUE am_bootstrap_lift( VALUE self, VALUE args )
{
    sqlite3*        db = NULL;
    sqlite3_stmt* stmt = NULL;
    int             rc;
    int  last_row_good; 
    char raise_msg[BUFSIZ];

    VALUE     am_db_c  = rb_const_get( cARB, rb_intern("DEFAULT_DB") );
    VALUE    am_tbl_c  = rb_const_get( cARB, rb_intern("DEFAULT_BOOTSTRAP_TABLE") );
    VALUE     am_pk_c  = rb_const_get( cARB, rb_intern("DEFAULT_ROWID_COLUMN") );
    VALUE  am_fname_c  = rb_const_get( cARB, rb_intern("DEFAULT_FILENAME_COLUMN") );
    VALUE am_content_c = rb_const_get( cARB, rb_intern("DEFAULT_CONTENTS_COLUMN") );

    char*      dbfile = NULL;
    char*    tbl_name = NULL;
    char*      pk_col = NULL;
    char*   fname_col = NULL;
    char* content_col = NULL;

    char             sql[BUFSIZ];
    const char* sql_tail = NULL;
    int        sql_bytes = 0;
    
    const unsigned char* result_text = NULL;
    int                result_length = 0;

    VALUE     require_name = Qnil;  /* ruby string of the file name for use in eval */
    VALUE   eval_this_code = Qnil;  /* ruby string of the code to eval from the db  */
    VALUE toplevel_binding = rb_const_get( rb_cObject, rb_intern("TOPLEVEL_BINDING") ) ;
    VALUE              tmp = Qnil;

    ID             eval_id = rb_intern("eval");


    if (   Qnil == args  ) {
        args = rb_hash_new();
    } else {
        args = rb_ary_shift( args );
    }

    Check_Type( args, T_HASH );
    
    /* get the arguments */
    dbfile      = ( Qnil == (tmp = rb_hash_aref( args, rb_str_new2( "dbfile"          ) ) ) ) ? StringValuePtr( am_db_c )      : StringValuePtr( tmp );
    tbl_name    = ( Qnil == (tmp = rb_hash_aref( args, rb_str_new2( "table_name"      ) ) ) ) ? StringValuePtr( am_tbl_c )     : StringValuePtr( tmp );
    pk_col      = ( Qnil == (tmp = rb_hash_aref( args, rb_str_new2( "rowid_column"    ) ) ) ) ? StringValuePtr( am_pk_c )      : StringValuePtr( tmp );
    fname_col   = ( Qnil == (tmp = rb_hash_aref( args, rb_str_new2( "filename_column" ) ) ) ) ? StringValuePtr( am_fname_c )   : StringValuePtr( tmp );
    content_col = ( Qnil == (tmp = rb_hash_aref( args, rb_str_new2( "contents_column" ) ) ) ) ? StringValuePtr( am_content_c ) : StringValuePtr( tmp );


    /* open the database */
    rc = sqlite3_open_v2( dbfile , &db, SQLITE_OPEN_READONLY, NULL);
    if ( SQLITE_OK != rc ) {
        memset( raise_msg, 0, BUFSIZ );
        snprintf(raise_msg, BUFSIZ, "Failure to open database %s for bootload: [SQLITE_ERROR %d] : %s", dbfile, rc, sqlite3_errmsg( db ) );
        am_bootstrap_cleanup_and_raise( raise_msg, db, stmt );
    }

    /* prepare the db query */
    memset( sql, 0, BUFSIZ );
    sql_bytes = snprintf( sql, BUFSIZ, "SELECT %s, %s FROM %s ORDER BY %s", fname_col, content_col, tbl_name, pk_col );
    rc = sqlite3_prepare_v2( db, sql, sql_bytes, &stmt, &sql_tail ) ;
    if ( SQLITE_OK != rc) {
        memset( raise_msg, 0, BUFSIZ );
        snprintf( raise_msg, BUFSIZ,
                  "Failure to prepare bootload select statement table = '%s', rowid col = '%s', filename col ='%s', contents col = '%s' : [SQLITE_ERROR %d] %s\n",
                  tbl_name, pk_col, fname_col, content_col, rc, sqlite3_errmsg( db ));
        am_bootstrap_cleanup_and_raise( raise_msg, db, stmt );
    }

    /* loop over the resulting rows, eval'ing and loading $LOADED_FEATURES */
    last_row_good = -1;
    while ( SQLITE_ROW == ( rc = sqlite3_step( stmt ) ) ) {
        /* file name */
        result_text   = sqlite3_column_text( stmt, 0 );
        result_length = sqlite3_column_bytes( stmt, 0 );
        require_name  = rb_str_new( (const char*)result_text, result_length );

        /* ruby code */
        result_text    = sqlite3_column_text( stmt, 1 );
        result_length  = sqlite3_column_bytes( stmt, 1 );
        eval_this_code = rb_str_new( (const char*)result_text, result_length );

        /* Kernel.eval( code, TOPLEVEL_BINDING, filename, 1 ) */ 
        rb_funcall(rb_mKernel, eval_id, 4, eval_this_code, toplevel_binding, require_name, INT2FIX(1) );

        /* TODO: for ruby 1.9 -- put in ? sqlite3://path/to/database?tablename=tbl_name#require_name */
        /* update $LOADED_FEATURES */
        rb_ary_push( rb_gv_get( "$LOADED_FEATURES" ), require_name );
    }

    /* if there was some sqlite error in the processing of the rows */
    if ( SQLITE_DONE != rc ) {
        memset( raise_msg, 0, BUFSIZ );
        snprintf( raise_msg, BUFSIZ, "Failure in bootloading, last successfully loaded rowid was %d : [SQLITE_ERROR %d] %s\n", 
                  last_row_good, rc, sqlite3_errmsg( db ) );
        am_bootstrap_cleanup_and_raise( raise_msg, db, stmt );
    }

    /* finalize the statement */    
    rc = sqlite3_finalize( stmt );
    if ( SQLITE_OK != rc ) {
        memset( raise_msg, 0, BUFSIZ );
        snprintf( raise_msg, BUFSIZ, "Failure to finalize bootload statement : [SQLITE_ERROR %d] %s\n", rc, sqlite3_errmsg( db ) );
        am_bootstrap_cleanup_and_raise( raise_msg, db, stmt );
    }

    stmt = NULL;

    /* close the database */
    rc = sqlite3_close( db );
    if ( SQLITE_OK != rc ) {
        memset( raise_msg, 0, BUFSIZ );
        snprintf( raise_msg, BUFSIZ, "Failure to close database : [SQLITE_ERROR %d] : %s\n", rc, sqlite3_errmsg( db )),
        am_bootstrap_cleanup_and_raise( raise_msg, db,stmt );
    }

    return Qnil;
}

/**
 * Bootstrapping module to help _require_ when Amalgalite::Requires is not
 * availble in files.
 */
void Init_amalgalite3_requires_bootstrap()
{

    mA   = rb_define_module("Amalgalite");
    cAR  = rb_define_class_under(mA, "Requires", rb_cObject);
    cARB = rb_define_class_under(cAR, "Bootstrap", rb_cObject);

    eARB_Error = rb_define_class_under(cARB, "Error", rb_eStandardError);

    rb_define_module_function(cARB, "lift", am_bootstrap_lift, -2); 

    /* constants for default db, table, column, rowid, contents */ 
    rb_define_const(cARB,                "DEFAULT_DB", rb_str_new2( "lib.db" ));
    rb_define_const(cARB,             "DEFAULT_TABLE", rb_str_new2( "rubylibs" ));
    rb_define_const(cARB,   "DEFAULT_BOOTSTRAP_TABLE", rb_str_new2( "bootstrap" ));
    rb_define_const(cARB,      "DEFAULT_ROWID_COLUMN", rb_str_new2( "id" ));
    rb_define_const(cARB,   "DEFAULT_FILENAME_COLUMN", rb_str_new2( "filename" ));
    rb_define_const(cARB,   "DEFAULT_CONTENTS_COLUMN", rb_str_new2( "contents" ));
    rb_define_const(cARB, "DEFAULT_COMPRESSED_COLUMN", rb_str_new2( "compressed" ));

    return;
}

