/* --------------------------------------------------------------------------
 * sqlite.c -- glue code between for the SQLite database and Ruby.
 * Copyright (C) 2003 Jamis Buck (jgb3@email.byu.edu)
 * --------------------------------------------------------------------------
 * This file is part of the XChat-Ruby plugin.
 * 
 * The  XChat-Ruby  plugin  is  free software; you can redistribute it and/or
 * modify  it  under the terms of the GNU General Public License as published
 * by  the  Free  Software  Foundation;  either  version 2 of the License, or
 * (at your option) any later version.
 * 
 * The  XChat-Ruby  plugin 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.   See  the  GNU
 * General Public License for more details.
 * 
 * You  should  have  received  a  copy  of  the  GNU  General Public License
 * along  with  the  XChat-Ruby  plugin;  if  not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * --------------------------------------------------------------------------
 * This file defines the glue code between the SQLite database engine and
 * the Ruby interpreter.
 *
 * Author: Jamis Buck (jgb3@email.byu.edu)
 * Date: June 2003
 * -------------------------------------------------------------------------- */

#include <sqlite.h>

#include <stdio.h>
#include "ruby.h"


/* this wraps the sqlite db pointer.  We need to do it this way so that we
 * can keep the object alive even after we've closed the DB handle, and we
 * need to be able to tell the difference between an open and a closed DB
 * handle.  This way, if the 'db' member is NULL, we know the database is
 * not open. */

typedef struct
{
  sqlite *db;
} SQLITE_RUBY_DATA;


/* This represents the information for a callback during query execution. */

typedef struct
{
  VALUE callback;        /* the Method object to invoke for each row */
  VALUE arg;             /* the application-defined cookie value to pass to the method */
  VALUE columns;         /* a ruby array of all of the column names */
  int   built_columns;   /* whether or not the 'columns' member is valid yet */
} SQLITE_RUBY_CALLBACK;

/* This represents the information for a callback on a custom SQL function */

typedef struct
{
  VALUE callback;        /* the Method object to invoke for each function invocation */
  VALUE finalize;        /* the Method object to invoke when an aggregate function is finished */
  VALUE arg;             /* the app-defined cookie value */
} SQLITE_CUSTOM_FUNCTION_CB;


/* global variables for defining the classes, modules, and symbols that are used by this
 * module. */

static VALUE mSQLite;
static VALUE cSQLite;
static VALUE cSQLiteException;
static VALUE cSQLiteQueryContext;
static ID    idCallMethod;
static ID    idInstanceEvalMethod;


/* free's the given database handle when the Ruby engine garbage collects it. */
static void static_free_database_handle( SQLITE_RUBY_DATA *hdb );

/* called when a query is processing another row */
static int static_ruby_sqlite_callback( void *pArg, int argc, char **argv, char **columns );

/* called when a custom SQL function is invoked */
static void static_custom_function_callback( sqlite_func* ctx, int argc, const char **argv );

/* called when a custom aggregate SQL function is invoked */
static void static_custom_aggregate_callback( sqlite_func* ctx, int argc, const char **argv );

/* called when the custom aggregate SQL finalize function is invoked */
static void static_custom_finalize_callback( sqlite_func* ctx );


/* create a new SQLite database connection to the given database */
static VALUE static_database_new( VALUE klass,
                                  VALUE dbname,
                                  VALUE mode );

/* close the database connection */
static VALUE static_database_close( VALUE self );

/* execute the given SQL statement, calling the given callback for each row */
static VALUE static_database_exec( VALUE self,
                                   VALUE sql,
                                   VALUE callback,
                                   VALUE parm );

/* get the key value of the last inserted row */
static VALUE static_last_insert_rowid( VALUE self );

/* get how many rows were affected by the last query */
static VALUE static_changes( VALUE self );

/* interrupt the current query */
static VALUE static_interrupt( VALUE self );

/* whether or not the given SQL statement is completely defined yet or not */
static VALUE static_complete( VALUE self,
                              VALUE sql );

/* define a custom SQL function */
static VALUE static_create_function( VALUE self,
                                     VALUE name,
                                     VALUE argc,
                                     VALUE callback,
                                     VALUE parm );

/* define a custom aggregate SQL function */
static VALUE static_create_aggregate( VALUE self,
                                      VALUE name,
                                      VALUE argc,
                                      VALUE step,
                                      VALUE finalize,
                                      VALUE parm );

/* return the number of rows in the aggregate */
static VALUE static_aggregate_count( VALUE self );

/* get the properties attribute of the aggregate context */
static VALUE static_get_properties( VALUE self );



static void static_free_database_handle( SQLITE_RUBY_DATA *hdb )
{
  if( hdb )
  {
    if( hdb->db )
    {
      sqlite_close( hdb->db );
      hdb->db = NULL;
    }

    free( hdb );
  }
}


static int static_ruby_sqlite_callback( void *pArg, int argc, char **argv, char **columns )
{
  SQLITE_RUBY_CALLBACK *hook = (SQLITE_RUBY_CALLBACK*)pArg;
  VALUE hash;
  int i;
  VALUE rc;

  hash = rb_hash_new();

  if( !hook->built_columns )
  {
    hook->columns = rb_ary_new2( argc );
  }

  for( i = 0; i < argc; i++ )
  {
    VALUE val;

    if( !hook->built_columns ) rb_ary_push( hook->columns, rb_str_new2( columns[i] ) );

    val = ( argv[i] ? rb_str_new2( argv[i] ) : Qnil );

    rb_hash_aset( hash, rb_ary_entry( hook->columns, i ), val );
    rb_hash_aset( hash, INT2FIX(i+1), val );
  }

  hook->built_columns = 1;

  rb_iv_set( hash, "@argument", hook->arg );
  rb_funcall( hash, idInstanceEvalMethod, 1, rb_str_new2( "def argument\n@argument\nend\n" ) );

  rc = rb_funcall( hook->callback, idCallMethod, 1, hash );

  return ( TYPE( rc ) == T_FIXNUM ? FIX2INT( rc ) : 0 );
}


static void static_custom_function_callback( sqlite_func* ctx, int argc, const char **argv )
{
  SQLITE_CUSTOM_FUNCTION_CB *data;
  VALUE rc;
  VALUE args;
  int i;

  data = (SQLITE_CUSTOM_FUNCTION_CB*)sqlite_user_data( ctx );

  args = rb_ary_new2( argc );
  rb_ary_push( args, Data_Wrap_Struct( cSQLiteQueryContext, 0, 0, ctx ) );

  for( i = 0; i < argc; i++ )
  {
    if( argv[i] )
      rb_ary_push( args, rb_str_new2( argv[i] ) );
    else
      rb_ary_push( args, Qnil );
  }

  rc = rb_apply( data->callback, idCallMethod, args );

  switch( TYPE(rc) )
  {
    case T_STRING:
      sqlite_set_result_string( ctx, STR2CSTR(rc), RSTRING(rc)->len );
      break;
    case T_FIXNUM:
      sqlite_set_result_int( ctx, FIX2INT(rc) );
      break;
    case T_FLOAT:
      sqlite_set_result_double( ctx, NUM2DBL(rc) );
      break;
  }
}


static void static_custom_aggregate_callback( sqlite_func* ctx, int argc, const char **argv )
{
  SQLITE_CUSTOM_FUNCTION_CB *data;
  VALUE args;
  VALUE *hash;
  int i;

  hash = (VALUE*)sqlite_aggregate_context( ctx, sizeof( VALUE ) );
  if( *hash == 0 ) *hash = rb_hash_new();

  data = (SQLITE_CUSTOM_FUNCTION_CB*)sqlite_user_data( ctx );

  args = rb_ary_new2( argc );
  rb_ary_push( args, Data_Wrap_Struct( cSQLiteQueryContext, 0, 0, ctx ) );

  for( i = 0; i < argc; i++ )
  {
    if( argv[i] )
      rb_ary_push( args, rb_str_new2( argv[i] ) );
    else
      rb_ary_push( args, Qnil );
  }

  rb_apply( data->callback, idCallMethod, args );
}


static void static_custom_finalize_callback( sqlite_func* ctx )
{
  SQLITE_CUSTOM_FUNCTION_CB *data;
  VALUE rc;
  int i;

  data = (SQLITE_CUSTOM_FUNCTION_CB*)sqlite_user_data( ctx );
  rc = rb_funcall( data->finalize, idCallMethod, 1, Data_Wrap_Struct( cSQLiteQueryContext, 0, 0, ctx ) );

  switch( TYPE(rc) )
  {
    case T_STRING:
      sqlite_set_result_string( ctx, STR2CSTR(rc), RSTRING(rc)->len );
      break;
    case T_FIXNUM:
      sqlite_set_result_int( ctx, FIX2INT(rc) );
      break;
    case T_FLOAT:
      sqlite_set_result_double( ctx, NUM2DBL(rc) );
      break;
  }
}



static VALUE static_database_new( VALUE klass,
                                  VALUE dbname,
                                  VALUE mode )
{
  SQLITE_RUBY_DATA *hdb;
  sqlite *db;
  char   *s_dbname;
  int     i_mode;
  char   *errmsg;
  VALUE   v_db;

  Check_Type( dbname, T_STRING );
  Check_Type( mode,   T_FIXNUM );

  s_dbname = STR2CSTR(dbname);
  i_mode   = FIX2INT(mode);

  db = sqlite_open( s_dbname, i_mode, &errmsg );
  if( db == NULL )
  {
    VALUE err = rb_str_new2( errmsg );
    free( errmsg );

    rb_raise( cSQLiteException, "%s", STR2CSTR( err ) );
  }

  hdb = ALLOC( SQLITE_RUBY_DATA );
  hdb->db = db;

  v_db = Data_Wrap_Struct( klass, NULL, static_free_database_handle, hdb );

  return v_db;
}


static VALUE static_database_close( VALUE self )
{
  SQLITE_RUBY_DATA *hdb;

  Data_Get_Struct( self, SQLITE_RUBY_DATA, hdb );
  if( hdb->db != NULL )
  {
    sqlite_close( hdb->db );
    hdb->db = NULL;
  }

  return Qnil;
}


static VALUE static_database_exec( VALUE self,
                                   VALUE sql,
                                   VALUE callback,
                                   VALUE parm )
{
  SQLITE_RUBY_DATA *hdb;
  SQLITE_RUBY_CALLBACK hook;
  char *s_sql;
  int i;
  char *err = NULL;
  VALUE v_err;

  Check_Type( sql, T_STRING );
  s_sql = STR2CSTR(sql);

  Data_Get_Struct( self, SQLITE_RUBY_DATA, hdb );
  if( hdb->db == NULL )
  {
    rb_raise( cSQLiteException, "attempt to access a closed database" );
  }

  hook.callback = callback;
  hook.arg = parm;
  hook.built_columns = 0;
  hook.columns = Qnil;

  i = sqlite_exec( hdb->db,
                   s_sql,
                   static_ruby_sqlite_callback,
                   &hook,
                   &err );

  if( err != 0 )
  {
    v_err = rb_str_new2( err );
    free( err );
  }

  switch( i )
  {
    case SQLITE_OK:
      break;
    default:
      /* TODO: a better solution is to create a set of exceptions that can differentiate
       * between the different return values. */
      rb_raise( cSQLiteException, "%s", STR2CSTR(v_err) );
  }

  return INT2FIX(0);
}


static VALUE static_last_insert_rowid( VALUE self )
{
  SQLITE_RUBY_DATA *hdb;

  Data_Get_Struct( self, SQLITE_RUBY_DATA, hdb );

  if( hdb->db == NULL )
    rb_raise( cSQLiteException, "attempt to access a closed database" );

  return INT2FIX( sqlite_last_insert_rowid( hdb->db ) );
}


static VALUE static_changes( VALUE self )
{
  SQLITE_RUBY_DATA *hdb;

  Data_Get_Struct( self, SQLITE_RUBY_DATA, hdb );

  if( hdb->db == NULL )
    rb_raise( cSQLiteException, "attempt to access a closed database" );

  return INT2FIX( sqlite_changes( hdb->db ) );
}


static VALUE static_interrupt( VALUE self )
{
  SQLITE_RUBY_DATA *hdb;

  Data_Get_Struct( self, SQLITE_RUBY_DATA, hdb );

  if( hdb->db == NULL )
    rb_raise( cSQLiteException, "attempt to access a closed database" );

  sqlite_interrupt( hdb->db );

  return Qnil;
}


static VALUE static_complete( VALUE self,
                              VALUE sql )
{
  Check_Type( sql, T_STRING );

  return ( sqlite_complete( STR2CSTR( sql ) ) ? Qtrue : Qfalse );
}


static VALUE static_create_function( VALUE self,
                                     VALUE name,
                                     VALUE argc,
                                     VALUE callback,
                                     VALUE parm )
{
  SQLITE_RUBY_DATA *hdb;
  SQLITE_CUSTOM_FUNCTION_CB *data;
  char *s_name;
  int   i_argc;
  int   rc;

  Data_Get_Struct( self, SQLITE_RUBY_DATA, hdb );
  s_name = STR2CSTR( name );
  i_argc = FIX2INT( argc );

  if( hdb->db == NULL )
    rb_raise( cSQLiteException, "attempt to access a closed database" );

  data = ALLOC( SQLITE_CUSTOM_FUNCTION_CB );
  data->callback = callback;
  data->arg = parm;

  rc = sqlite_create_function( hdb->db,
                               s_name,
                               i_argc,
                               static_custom_function_callback,
                               data );

  if( rc != 0 )
    rb_raise( cSQLiteException, "error registering custom function" );

  return Qnil;
}


static VALUE static_create_aggregate( VALUE self,
                                      VALUE name,
                                      VALUE argc,
                                      VALUE step,
                                      VALUE finalize,
                                      VALUE parm )
{
  SQLITE_RUBY_DATA *hdb;
  SQLITE_CUSTOM_FUNCTION_CB *data;
  char *s_name;
  int   i_argc;
  int   rc;

  Data_Get_Struct( self, SQLITE_RUBY_DATA, hdb );
  s_name = STR2CSTR( name );
  i_argc = FIX2INT( argc );

  if( hdb->db == NULL )
    rb_raise( cSQLiteException, "attempt to access a closed database" );

  data = ALLOC( SQLITE_CUSTOM_FUNCTION_CB );
  data->callback = step;
  data->finalize = finalize;
  data->arg = parm;

  rc = sqlite_create_aggregate( hdb->db,
                                s_name,
                                i_argc,
                                static_custom_aggregate_callback,
                                static_custom_finalize_callback,
                                data );

  if( rc != 0 )
    rb_raise( cSQLiteException, "error registering custom function" );

  return Qnil;
}


static VALUE static_aggregate_count( VALUE self )
{
  sqlite_func *ctx;

  Data_Get_Struct( self, sqlite_func, ctx );

  return INT2FIX( sqlite_aggregate_count( ctx ) );
}


static VALUE static_get_properties( VALUE self )
{
  sqlite_func *ctx;

  Data_Get_Struct( self, sqlite_func, ctx );

  return *(VALUE*)sqlite_aggregate_context( ctx, sizeof( VALUE ) );
}


void Init__sqlite()
{
  mSQLite = rb_define_module( "SQLite" );

  idCallMethod  = rb_intern( "call" );
  idInstanceEvalMethod = rb_intern("instance_eval");
  
  cSQLite = rb_define_class_under( mSQLite, "Database", rb_cObject );
  cSQLiteException = rb_define_class_under( mSQLite, "DatabaseException", rb_eException );
  cSQLiteQueryContext = rb_define_class_under( mSQLite, "QueryContext", rb_cHash );

  rb_define_singleton_method( cSQLite, "new", static_database_new, 2 );
  rb_define_alias( cSQLite, "new", "open" );

  rb_define_method( cSQLite, "close", static_database_close, 0 );
  rb_define_method( cSQLite, "exec", static_database_exec, 3 );
  rb_define_method( cSQLite, "last_insert_rowid", static_last_insert_rowid, 0 );
  rb_define_method( cSQLite, "changes", static_changes, 0 );
  rb_define_method( cSQLite, "interrupt", static_interrupt, 0 );
  rb_define_method( cSQLite, "complete?", static_complete, 1 );
  rb_define_method( cSQLite, "create_function", static_create_function, 4 );
  rb_define_method( cSQLite, "create_aggregate", static_create_aggregate, 5 );

  rb_define_const( cSQLite, "VERSION", rb_str_new2( sqlite_version ) );
  rb_define_const( cSQLite, "ENCODING", rb_str_new2( sqlite_encoding ) );

  rb_define_method( cSQLiteQueryContext, "aggregate_count", static_aggregate_count, 0 );
  rb_define_method( cSQLiteQueryContext, "properties", static_get_properties, 0 );
}
