
#include <tiny_tds_ext.h>
#include <errno.h>

VALUE cTinyTdsClient;
extern VALUE mTinyTds, cTinyTdsError;
static ID sym_username, sym_password, sym_dataserver, sym_database, sym_appname, sym_tds_version, sym_login_timeout, sym_timeout, sym_encoding, sym_azure;
static ID intern_source_eql, intern_severity_eql, intern_db_error_number_eql, intern_os_error_number_eql;
static ID intern_new, intern_dup, intern_transpose_iconv_encoding, intern_local_offset, intern_gsub;
VALUE opt_escape_regex, opt_escape_dblquote;


// Lib Macros

#define GET_CLIENT_WRAPPER(self) \
  tinytds_client_wrapper *cwrap; \
  Data_Get_Struct(self, tinytds_client_wrapper, cwrap)

#define REQUIRE_OPEN_CLIENT(cwrap) \
  if (cwrap->closed || cwrap->userdata->closed) { \
    rb_raise(cTinyTdsError, "closed connection"); \
    return Qnil; \
  }


// Lib Backend (Helpers)

static VALUE rb_tinytds_raise_error(DBPROCESS *dbproc, int cancel, char *error, char *source, int severity, int dberr, int oserr) {
  GET_CLIENT_USERDATA(dbproc);
  if (cancel && !dbdead(dbproc) && userdata && !userdata->closed) { 
    userdata->dbsqlok_sent = 1;
    dbsqlok(dbproc);
    userdata->dbcancel_sent = 1;
    dbcancel(dbproc);
  }
  VALUE e = rb_exc_new2(cTinyTdsError, error);
  rb_funcall(e, intern_source_eql, 1, rb_str_new2(source));
  if (severity)
    rb_funcall(e, intern_severity_eql, 1, INT2FIX(severity));
  if (dberr)
    rb_funcall(e, intern_db_error_number_eql, 1, INT2FIX(dberr));
  if (oserr)
    rb_funcall(e, intern_os_error_number_eql, 1, INT2FIX(oserr));   
  rb_exc_raise(e);
  return Qnil;
}


// Lib Backend (Memory Management & Handlers)

int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, char *dberrstr, char *oserrstr) { 
 static char *source = "error";
 GET_CLIENT_USERDATA(dbproc);
 int return_value = INT_CONTINUE;
 int cancel = 0;
 switch(dberr) {
   case SYBESMSG:
     return return_value;
   case SYBEICONVI:
     return INT_CANCEL;
   case SYBEFCON:
   case SYBESOCK:
   case SYBECONN:
     return_value = INT_EXIT;
     break;
   case SYBESEOF: {
     if (userdata && userdata->timing_out)
       return_value = INT_TIMEOUT;
     return INT_CANCEL;
     break;
   }
   case SYBETIME: {
     if (userdata) {
       if (userdata->timing_out) {
         return INT_CONTINUE;
       } else {
         userdata->timing_out = 1;
       }
     }
     cancel = 1;
     break;
   }
   case SYBEWRIT: {
     if (userdata && (userdata->dbsqlok_sent || userdata->dbcancel_sent))
       return INT_CANCEL;
     cancel = 1;
     break;
   }
   case SYBEREAD:
     cancel = 1;
     break;
 }
 rb_tinytds_raise_error(dbproc, cancel, dberrstr, source, severity, dberr, oserr);
 return return_value;
}

int tinytds_msg_handler(DBPROCESS *dbproc, DBINT msgno, int msgstate, int severity, char *msgtext, char *srvname, char *procname, int line) {
  static char *source = "message";
  if (severity)
    rb_tinytds_raise_error(dbproc, 1, msgtext, source, severity, msgno, msgstate);
  return 0;
}

static void rb_tinytds_client_reset_userdata(tinytds_client_userdata *userdata) {
  userdata->timing_out = 0;
  userdata->dbsql_sent = 0;
  userdata->dbsqlok_sent = 0;
  userdata->dbcancel_sent = 0;
}

static void rb_tinytds_client_mark(void *ptr) {
  tinytds_client_wrapper *cwrap = (tinytds_client_wrapper *)ptr;
  if (cwrap) {
    rb_gc_mark(cwrap->charset);
  }
}

static void rb_tinytds_client_free(void *ptr) {
  tinytds_client_wrapper *cwrap = (tinytds_client_wrapper *)ptr;
  if (cwrap->login)
    dbloginfree(cwrap->login);
  if (cwrap->client && !cwrap->closed) {
    dbclose(cwrap->client);
    cwrap->closed = 1;
    cwrap->userdata->closed = 1;
  }
  xfree(cwrap->userdata);
  xfree(ptr);
}

static VALUE allocate(VALUE klass) {
  VALUE obj;
  tinytds_client_wrapper *cwrap;
  obj = Data_Make_Struct(klass, tinytds_client_wrapper, rb_tinytds_client_mark, rb_tinytds_client_free, cwrap);
  cwrap->closed = 1;
  cwrap->charset = Qnil;
  cwrap->userdata = malloc(sizeof(tinytds_client_userdata));
  cwrap->userdata->closed = 1;
  rb_tinytds_client_reset_userdata(cwrap->userdata);  
  return obj;
}


// TinyTds::Client (public) 

static VALUE rb_tinytds_tds_version(VALUE self) {
  GET_CLIENT_WRAPPER(self);
  return INT2FIX(dbtds(cwrap->client));
}

static VALUE rb_tinytds_close(VALUE self) {
  GET_CLIENT_WRAPPER(self);
  if (cwrap->client && !cwrap->closed) {
    dbclose(cwrap->client);
    cwrap->closed = 1;
    cwrap->userdata->closed = 1;
  }
  return Qtrue;
}

static VALUE rb_tinytds_dead(VALUE self) {
  GET_CLIENT_WRAPPER(self);
  return dbdead(cwrap->client) ? Qtrue : Qfalse;
}

static VALUE rb_tinytds_closed(VALUE self) {
  GET_CLIENT_WRAPPER(self);
  return (cwrap->closed || cwrap->userdata->closed) ? Qtrue : Qfalse;
}

static VALUE rb_tinytds_canceled(VALUE self) {
  GET_CLIENT_WRAPPER(self);
  return cwrap->userdata->dbcancel_sent ? Qtrue : Qfalse;
}

static VALUE rb_tinytds_sqlsent(VALUE self) {
  GET_CLIENT_WRAPPER(self);
  return cwrap->userdata->dbsql_sent ? Qtrue : Qfalse;
}

static VALUE rb_tinytds_execute(VALUE self, VALUE sql) {
  GET_CLIENT_WRAPPER(self);
  rb_tinytds_client_reset_userdata(cwrap->userdata);
  REQUIRE_OPEN_CLIENT(cwrap);
  dbcmd(cwrap->client, StringValuePtr(sql));
  if (dbsqlsend(cwrap->client) == FAIL) {
    rb_warn("TinyTds: dbsqlsend() returned FAIL.\n");
    return Qfalse;
  }
  cwrap->userdata->dbsql_sent = 1;
  VALUE result = rb_tinytds_new_result_obj(cwrap->client);
  rb_iv_set(result, "@query_options", rb_funcall(rb_iv_get(self, "@query_options"), intern_dup, 0));
  GET_RESULT_WRAPPER(result);
  rwrap->local_offset = rb_funcall(cTinyTdsClient, intern_local_offset, 0);
  #ifdef HAVE_RUBY_ENCODING_H
    rwrap->encoding = cwrap->encoding;
  #endif
  return result;  
}

static VALUE rb_tinytds_charset(VALUE self) {
  GET_CLIENT_WRAPPER(self);
  return cwrap->charset;
}

static VALUE rb_tinytds_encoding(VALUE self) {
  GET_CLIENT_WRAPPER(self);
  #ifdef HAVE_RUBY_ENCODING_H
    return rb_enc_from_encoding(cwrap->encoding);
  #else
    return Qnil;
  #endif
}

static VALUE rb_tinytds_escape(VALUE self, VALUE string) {
  Check_Type(string, T_STRING);
  GET_CLIENT_WRAPPER(self);
  VALUE new_string = rb_funcall(string, intern_gsub, 2, opt_escape_regex, opt_escape_dblquote);
  #ifdef HAVE_RUBY_ENCODING_H
    rb_enc_associate(new_string, cwrap->encoding);
  #endif
  return new_string;
}

/* Duplicated in result.c */
static VALUE rb_tinytds_return_code(VALUE self) {
  GET_CLIENT_WRAPPER(self);
  if (cwrap->client && dbhasretstat(cwrap->client)) {
    return LONG2NUM((long)dbretstatus(cwrap->client));
  } else {
    return Qnil;
  }
}


// TinyTds::Client (protected) 

static VALUE rb_tinytds_connect(VALUE self, VALUE opts) {
  /* Parsing options hash to local vars. */
  VALUE user, pass, dataserver, database, app, version, ltimeout, timeout, charset, azure;
  user = rb_hash_aref(opts, sym_username);
  pass = rb_hash_aref(opts, sym_password);
  dataserver = rb_hash_aref(opts, sym_dataserver);
  database = rb_hash_aref(opts, sym_database);
  app = rb_hash_aref(opts, sym_appname);
  version = rb_hash_aref(opts, sym_tds_version);
  ltimeout = rb_hash_aref(opts, sym_login_timeout);
  timeout = rb_hash_aref(opts, sym_timeout);
  charset = rb_hash_aref(opts, sym_encoding);
  azure = rb_hash_aref(opts, sym_azure);
  /* Dealing with options. */
  if (dbinit() == FAIL) {
    rb_raise(cTinyTdsError, "failed dbinit() function");
    return self;
  }
  dberrhandle(tinytds_err_handler);
  dbmsghandle(tinytds_msg_handler);
  GET_CLIENT_WRAPPER(self);
  cwrap->login = dblogin();
  if (!NIL_P(user))
    dbsetluser(cwrap->login, StringValuePtr(user));
  if (!NIL_P(pass))
    dbsetlpwd(cwrap->login, StringValuePtr(pass));
  if (!NIL_P(app))
    dbsetlapp(cwrap->login, StringValuePtr(app));
  if (!NIL_P(version))
    dbsetlversion(cwrap->login, NUM2INT(version));
  if (!NIL_P(ltimeout))
    dbsetlogintime(NUM2INT(ltimeout));
  if (!NIL_P(timeout))
    dbsettime(NUM2INT(timeout));
  if (!NIL_P(charset))
    DBSETLCHARSET(cwrap->login, StringValuePtr(charset));
  if (!NIL_P(database) && (azure == Qtrue)) {
    #ifdef DBSETLDBNAME
      DBSETLDBNAME(cwrap->login, StringValuePtr(database));
    #else
      rb_warn("TinyTds: Azure connections not supported in this version of FreeTDS.\n");
    #endif
  }
  cwrap->client = dbopen(cwrap->login, StringValuePtr(dataserver));
  if (cwrap->client) {
    cwrap->closed = 0;
    cwrap->charset = charset;
    dbsetuserdata(cwrap->client, (BYTE*)cwrap->userdata);
    cwrap->userdata->closed = 0;
    if (!NIL_P(database) && (azure != Qtrue)) {
      dbuse(cwrap->client, StringValuePtr(database));
    }
    #ifdef HAVE_RUBY_ENCODING_H
      VALUE transposed_encoding = rb_funcall(cTinyTdsClient, intern_transpose_iconv_encoding, 1, charset);
      cwrap->encoding = rb_enc_find(StringValuePtr(transposed_encoding));
    #endif
  }
  return self;
}


// Lib Init

void init_tinytds_client() {
  cTinyTdsClient = rb_define_class_under(mTinyTds, "Client", rb_cObject);
  rb_define_alloc_func(cTinyTdsClient, allocate);
  /* Define TinyTds::Client Public Methods */
  rb_define_method(cTinyTdsClient, "tds_version", rb_tinytds_tds_version, 0);
  rb_define_method(cTinyTdsClient, "close", rb_tinytds_close, 0);
  rb_define_method(cTinyTdsClient, "closed?", rb_tinytds_closed, 0);
  rb_define_method(cTinyTdsClient, "canceled?", rb_tinytds_canceled, 0);
  rb_define_method(cTinyTdsClient, "dead?", rb_tinytds_dead, 0);
  rb_define_method(cTinyTdsClient, "sqlsent?", rb_tinytds_sqlsent, 0);
  rb_define_method(cTinyTdsClient, "execute", rb_tinytds_execute, 1);
  rb_define_method(cTinyTdsClient, "charset", rb_tinytds_charset, 0);
  rb_define_method(cTinyTdsClient, "encoding", rb_tinytds_encoding, 0);
  rb_define_method(cTinyTdsClient, "escape", rb_tinytds_escape, 1);
  rb_define_method(cTinyTdsClient, "return_code", rb_tinytds_return_code, 0);
  /* Define TinyTds::Client Protected Methods */
  rb_define_protected_method(cTinyTdsClient, "connect", rb_tinytds_connect, 1);
  /* Symbols For Connect */
  sym_username = ID2SYM(rb_intern("username"));
  sym_password = ID2SYM(rb_intern("password"));
  sym_dataserver = ID2SYM(rb_intern("dataserver"));
  sym_database = ID2SYM(rb_intern("database"));
  sym_appname = ID2SYM(rb_intern("appname"));
  sym_tds_version = ID2SYM(rb_intern("tds_version"));
  sym_login_timeout = ID2SYM(rb_intern("login_timeout"));
  sym_timeout = ID2SYM(rb_intern("timeout"));
  sym_encoding = ID2SYM(rb_intern("encoding"));
  sym_azure = ID2SYM(rb_intern("azure"));
  /* Intern TinyTds::Error Accessors */
  intern_source_eql = rb_intern("source=");
  intern_severity_eql = rb_intern("severity=");
  intern_db_error_number_eql = rb_intern("db_error_number=");
  intern_os_error_number_eql = rb_intern("os_error_number=");
  /* Intern Misc */
  intern_new = rb_intern("new");
  intern_dup = rb_intern("dup");
  intern_transpose_iconv_encoding = rb_intern("transpose_iconv_encoding");
  intern_local_offset = rb_intern("local_offset");
  intern_gsub = rb_intern("gsub");
  /* Escape Regexp Global */
  opt_escape_regex = rb_funcall(rb_cRegexp, intern_new, 1, rb_str_new2("\\\'"));
  opt_escape_dblquote = rb_str_new2("''");
  rb_global_variable(&opt_escape_regex);
  rb_global_variable(&opt_escape_dblquote);
}


