#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include <sql.h>
#include <sqlext.h>

typedef struct {
	SQLCHAR name[200];
	SQLSMALLINT type;
	SQLUINTEGER size;

	int intval;
	char *strval;
	SQLLEN null_ind;
} COLUMN;

typedef struct {
	SQLHANDLE stmt;
	int ncols;
	COLUMN *cols;
} QUERY;

static SQLHANDLE db_env_handle=NULL;

static void init_env() {
	if (db_env_handle)
		return ;
	SQLAllocHandle(SQL_HANDLE_ENV, NULL, &db_env_handle);
	SQLSetEnvAttr( db_env_handle, SQL_ATTR_ODBC_VERSION, (void*) SQL_OV_ODBC3, 0);
}

#define sql_err_decl char *sql_err_msg; int sql_err_code

#define sql_callg(x,y,z,t) do {\
	sql_err_code= x;\
	if (sql_err_code!=SQL_SUCCESS && sql_err_code!=SQL_SUCCESS_WITH_INFO) {\
		sql_err_msg= translate_sql_err(z,t);\
		goto y;\
	}\
} while(0)

#define sql_calle(z,x,y) sql_callg(x,y,z,SQL_HANDLE_ENV)
#define sql_callc(z,x,y) sql_callg(x,y,z,SQL_HANDLE_DBC)
#define sql_call(z,x,y) sql_callg(x,y,z,SQL_HANDLE_STMT)



static char* translate_sql_err(SQLHANDLE h,int typ) {
	SQLSMALLINT dummy_small;
	SQLINTEGER dummy_int;
	int r=1;
	char buf[2000];
	char state[20];
	char buf2[3000];
	int len=0;
	while(r<2) { /* fix this later: allow multiple messages */
		int d= SQLGetDiagRec(typ, h, r, state, &dummy_int, buf, sizeof(buf), &dummy_small);
		if (d!=SQL_SUCCESS && d!=SQL_SUCCESS_WITH_INFO)
			break;
		snprintf(buf2,sizeof(buf2),"SQL Error: %s. State: %s", buf, state);
		r++;
	}
	return strdup(buf2);
}

static void l_pushmethod(lua_State *L, void *handle, char *name, lua_CFunction fn) {
	lua_pushstring(L,name);
	lua_pushlightuserdata(L, handle);
	lua_pushcclosure(L,fn,1);
	lua_settable(L,-3);
}

static void l_verify(lua_State *L, char *fmt, char *errmsg) {
	int l= strlen(fmt);
	int t= lua_gettop(L);
	if (t<l)
		goto error;
	while(l--) {
		switch(fmt[l]) {
			case 's': if (!lua_isstring(L,t)) goto error; break;
			case 'n': if (!lua_isnumber(L,t)) goto error; break;
			case 'u': if (!lua_isuserdata(L,t)) goto error; break;
			case 'f': if (!lua_isfunction(L,t)) goto error; break;
			case 't': if (!lua_istable(L,t)) goto error; break;
		}
		t--;
	}
	return ;
error:
	lua_pushstring(L, errmsg);
	lua_error(L);
}

static void *get_object(lua_State *L) {
	void* h;
	lua_pushvalue(L, lua_upvalueindex(1));
	h= lua_touserdata(L,-1);
	lua_pop(L,1);
	return h;
}

static int l_error(lua_State *L,char *x) {
	lua_pushnil(L);
	lua_pushstring(L,x);
	free(x);
	return 2;
}

static int is_string_type(int t) { return t==SQL_C_CHAR; }

static int fetch_once(lua_State *L,QUERY *q, char **err_msg) {
	SQLRETURN d;
	int i;
	d= SQLFetch(q->stmt);
	if (d==SQL_NO_DATA) /* return nils. i.e. no result, no error */
		return 0;
	if (d!=SQL_SUCCESS && d!=SQL_SUCCESS_WITH_INFO) {
		*err_msg= translate_sql_err(q->stmt, SQL_HANDLE_STMT);
		SQLFreeHandle(SQL_HANDLE_STMT,q->stmt);
		q->stmt= NULL;
		return -1;
	}
	lua_newtable(L);
	for(i=0;i<q->ncols;i++) {
		lua_pushstring(L,q->cols[i].name);
		if (is_string_type(q->cols[i].type)) {
			if (!q->cols[i].null_ind)
				q->cols[i].strval[0]= 0;
			lua_pushstring(L,q->cols[i].strval);
		} else
			lua_pushnumber(L,q->cols[i].intval);
		lua_settable(L,-3);
	}
	return 1;
}

static int lq_dofetch(lua_State *L) {
	QUERY *q= get_object(L);
	char *errmsg;
	if (!q->stmt)
		return l_error(L,strdup("can't fetch from a closed query"));
	switch(fetch_once(L,q,&errmsg)) {
		case 0:
			return 0;
		case -1:
			return l_error(L,errmsg);
		case 1:
			break;
	}
	return 1;
}

static int lq_fetch(lua_State *L) {
	QUERY *q= get_object(L);
	char *errmsg;
	lua_pushlightuserdata(L, q);
	lua_pushcclosure(L, lq_dofetch, 1); 
	lua_pushnumber(L,0);  /* for the iterator variable _s */
	lua_pushnumber(L,0);
	return 3;
}

static void free_columns(int i,COLUMN *c) {
	while(i>=0) {
		if (is_string_type(c[i].type && c[i].strval))
			free(c[i].strval);
		i--;
	}
	free(c);
}

static int lq_close(lua_State *L) {
	QUERY *q= get_object(L);
	if (q->stmt)
		SQLFreeHandle(SQL_HANDLE_STMT,q->stmt);
	free_columns(q->ncols-1,q->cols);
	memset(q,0,sizeof(*q));
	free(q);
	return 0;
}




static int type_sql_to_c(int t) {
	if (t==SQL_VARCHAR || t==SQL_CHAR)
		return SQL_C_CHAR;
	else
		return SQL_C_LONG;
}

static int get_cols_and_bind(SQLHANDLE stmt_handle, COLUMN **Rc, int *Rncols,char **errmsg) {
	sql_err_decl;
	int i;
	SQLSMALLINT dummy;
	COLUMN *c;
	SQLSMALLINT ncols;
	sql_call(stmt_handle,SQLNumResultCols(stmt_handle, &ncols), err_colcount);
	c= calloc(ncols,sizeof(*c));
	if (!c) {
		sql_err_code= -1;
		goto err_colcount;
	}
	for(i=0;i<ncols;i++) {
		sql_call(stmt_handle,SQLDescribeCol(stmt_handle, i+1,  c[i].name, sizeof(c[i].name)-1, &dummy,
											&c[i].type, &c[i].size, &dummy, &dummy), err_describe);
		c[i].type= type_sql_to_c(c[i].type);
		if (is_string_type(c[i].type)) {
			c[i].size=2048;
			c[i].strval= malloc(c[i].size+1);
		}
		sql_call(stmt_handle,SQLBindCol(stmt_handle,i+1, c[i].type, 
							is_string_type(c[i].type) ? (void*) c[i].strval : (void*) &c[i].intval,
							is_string_type(c[i].type) ? c[i].size : sizeof(int), &c[i].null_ind), err_bind);
	}
	*Rncols= ncols;
	*Rc= c;
	return 0;

err_bind:
	ncols= i;
	while(i>=0) {
		SQLBindCol(stmt_handle, i+1, c[i].type, NULL, 0, NULL);
		i--;
	}
	i= ncols;
err_describe:
	free_columns(i,c);
err_colcount:
	*errmsg= sql_err_msg;
	return 1;
}

static int ldb_query(lua_State *L) {
	sql_err_decl;
	SQLHANDLE conn= get_object(L);
	l_verify(L,"s","usage: db:query(querystring)");
	{
		SQLHANDLE stmt;
		int ncols;
		COLUMN *c;
		sql_callc(
			conn,
			SQLAllocHandle(SQL_HANDLE_STMT,conn, &stmt),
			err_allocstmt
		);
		sql_call(
			stmt,
			SQLExecDirect(stmt,(char*)lua_tostring(L,-1), SQL_NTS),
			err_execstmt
		);
		if (get_cols_and_bind(stmt, &c, &ncols,&sql_err_msg))
			goto err_execstmt;
		{
			QUERY *q= malloc(sizeof(*q));
			if (!q) {
				free_columns(ncols,c);
				sql_err_msg= strdup("memory allocation failed");
				goto err_execstmt;
			}
			q->stmt= stmt;
			q->ncols= ncols;
			q->cols= c;
			lua_newtable(L);
			l_pushmethod(L, q, "fetch", lq_fetch);
			l_pushmethod(L, q, "close", lq_close);
		}
		return 1;
err_execstmt:
		SQLFreeHandle(SQL_HANDLE_STMT, stmt);
err_allocstmt:
		return l_error(L,sql_err_msg);
	}
}

static int ldb_exec(lua_State *L) {
	sql_err_decl;
	SQLHANDLE conn= get_object(L);
	SQLHANDLE stmt;
	l_verify(L,"s","usage: db.exec(string)");
	sql_callc(
		conn,
		SQLAllocHandle(SQL_HANDLE_STMT,conn, &stmt),
		err_allocstmt
	);
	sql_call(
		stmt,
		SQLExecDirect(stmt,(char*)lua_tostring(L,-1), SQL_NTS),
		err_execstmt
	);
	SQLFreeHandle(SQL_HANDLE_STMT,stmt);
	lua_pushnumber(L,0);
	return 1;
err_execstmt:
	SQLFreeHandle(SQL_HANDLE_STMT, stmt);
err_allocstmt:
	return l_error(L,sql_err_msg);
}


static int ldb_close(lua_State *L) {
	SQLHANDLE db_conn_handle=get_object(L);
	SQLFreeHandle(SQL_HANDLE_DBC,db_conn_handle);
	return 0;
}

static char *null_if_empty(const char *x) {
	if (*x)
		return (char*) x;
	return NULL;
}

static int l_opendb(lua_State *L) {
	sql_err_decl;
	init_env();
	SQLHANDLE db_conn_handle;
	l_verify(L,"sss","usage: opendb(database,user,passwd)");
	sql_calle(
		db_env_handle,
		SQLAllocHandle(SQL_HANDLE_DBC, db_env_handle, &db_conn_handle),
		err_allocconn
	);
	sql_callc(
		db_conn_handle,
		SQLConnect(db_conn_handle, (char*) lua_tostring(L,-3), SQL_NTS,
								    null_if_empty(lua_tostring(L,-2)), SQL_NTS,
								    null_if_empty(lua_tostring(L,-1)), SQL_NTS),
		err_connect
	);
	lua_newtable(L);
	l_pushmethod(L, db_conn_handle, "query", ldb_query);
	l_pushmethod(L, db_conn_handle, "exec", ldb_exec);
	l_pushmethod(L, db_conn_handle, "close", ldb_close);
	return 1;

err_connect:
	SQLFreeHandle(SQL_HANDLE_DBC,db_conn_handle);
err_allocconn:
	return l_error(L,sql_err_msg);
}

void odbclua_init(lua_State *L){
	lua_register(L, "dbopen", l_opendb);
}

void luashlib_entry(lua_State *L){
	odbclua_init(L);
}



