#define LUA_LIB

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

#include "lua.h"
#include "lauxlib.h"

#include "libretrodb.h"
#include "lua_common.h"

static int create_db (lua_State * L);
static int db_new (lua_State * L);
static int db_close (lua_State * L);
static int db_cursor_open (lua_State * L);
static int db_query (lua_State * L);

static int cursor_close (lua_State * L);
static int cursor_read (lua_State * L);
static int cursor_iter (lua_State * L);

static const luaL_Reg testlib[] = {
	{"create_db", create_db},
	{"RarchDB", db_new},
	{NULL, NULL}
};

static const struct luaL_Reg cursor_mt [] = {
	{"__gc", cursor_close},
	{"read", cursor_read},
	{"iter", cursor_iter},
	{NULL, NULL}
};

static const struct luaL_Reg libretrodb_mt [] = {
	{"__gc", db_close},
	{"list_all", db_cursor_open},
	{"query", db_query},
	{NULL, NULL}
};


LUALIB_API int luaopen_testlib (lua_State * L) {
	luaL_newmetatable(L, "RarchDB.DB");
	lua_pushstring(L, "__index");
	lua_pushvalue(L, -2);
	lua_settable(L, -3);
	luaL_openlib(L, NULL, libretrodb_mt, 0);

	luaL_newmetatable(L, "RarchDB.Cursor");
	lua_pushstring(L, "__index");
	lua_pushvalue(L, -2);
	lua_settable(L, -3);
	luaL_openlib(L, NULL, cursor_mt, 0);

	luaL_register(L, "testlib", testlib);
	return 1;
}

static libretrodb_cursor * checkcursor(lua_State * L) {
	void * ud = luaL_checkudata(L, 1, "RarchDB.Cursor");
	luaL_argcheck(L, ud != NULL, 1, "`RarchDB.Cursor' expected");
	return ud;
}

static libretrodb * checkdb(lua_State * L) {
	void * ud = luaL_checkudata(L, 1, "RarchDB.DB");
	luaL_argcheck(L, ud != NULL, 1, "`RarchDB.DB' expected");
	return ud;
}

static int value_provider(
        void * ctx,
        struct rmsgpack_dom_value * out
) {
	int rv;
	lua_State * L = ctx;

	lua_getfield(L, LUA_REGISTRYINDEX, "testlib_get_value");

	if (lua_pcall(L, 0, 1, 0) != 0) {
		printf(
		        "error running function `get_value': %s\n",
		        lua_tostring(L, -1)
		);
	}

	if (lua_isnil(L, -1)) {
		rv = 1;
	} else if (lua_istable(L, -1)) {
		rv = libretrodb_lua_to_rmsgpack_value(L, -1, out);
	} else {
		printf("function `get_value' must return a table or nil\n");
	}
	lua_pop(L, 1);
	return rv;
}

static int create_db (lua_State * L) {
	int dst;
	const char * db_file;
	int rv;
	db_file = luaL_checkstring(L, -2);
	if (!lua_isfunction(L, -1)) {
		lua_pushstring(L, "second argument must be a function");
		lua_error(L);
	}
	lua_setfield(L, LUA_REGISTRYINDEX, "testlib_get_value");

	dst = open(db_file, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
	if (dst == -1) {
		lua_pushstring(L, "Could not open destination file");
		lua_error(L);
	}

	rv = libretrodb_create(dst, &value_provider, L);
	close(dst);
	return 0;
}

static int db_new (lua_State * L) {
	libretrodb_t * db = NULL;
	const char * db_file = NULL;
	int rv;
	db_file = luaL_checkstring(L, -1);
	db = lua_newuserdata(L, sizeof(libretrodb_t));
	if ((rv = libretrodb_open(db_file, db)) == 0) {
		luaL_getmetatable(L, "RarchDB.DB");
		lua_setmetatable(L, -2);
		lua_pushnil(L);
	} else {
		lua_pop(L, 1);
		lua_pushnil(L);
		lua_pushstring(L, strerror(-rv));
	}
	return 2;
}

static int db_close (lua_State * L) {
	libretrodb_t *db = checkdb(L);
	libretrodb_close(db);
	return 0;
}

static int db_query (lua_State * L) {
	int rv;
	libretrodb_cursor_t *cursor = NULL;
	libretrodb_t *db = checkdb(L);
	const char * query = luaL_checkstring(L, -1);
	const char * error = NULL;
	libretrodb_query_t *q = libretrodb_query_compile(
	                db,
	                query,
	                strlen(query),
	                &error
	        );
	if (error) {
		lua_pushnil(L);
		lua_pushstring(L, error);
	} else {
		cursor = lua_newuserdata(L, sizeof(libretrodb_t));
		if ((rv = libretrodb_cursor_open(db, cursor, q)) == 0) {
			luaL_getmetatable(L, "RarchDB.Cursor");
			lua_setmetatable(L, -2);
			lua_pushnil(L);
		} else {
			lua_pop(L, 1);
			lua_pushnil(L);
			lua_pushstring(L, strerror(-rv));
		}
		libretrodb_query_free(q);
	}
	return 2;
}
static int db_cursor_open (lua_State * L) {
	int rv;
	libretrodb_cursor_t *cursor = NULL;
	libretrodb_t *db = checkdb(L);
	cursor = lua_newuserdata(L, sizeof(libretrodb_t));
	if ((rv = libretrodb_cursor_open(db, cursor, NULL)) == 0) {
		luaL_getmetatable(L, "RarchDB.Cursor");
		lua_setmetatable(L, -2);
		lua_pushnil(L);
	} else {
		lua_pop(L, 1);
		lua_pushnil(L);
		lua_pushstring(L, strerror(-rv));
	}
	return 2;
}
static int cursor_close (lua_State * L) {
	libretrodb_cursor_t *cursor = checkcursor(L);
	libretrodb_cursor_close(cursor);
	return 0;
}

static void push_rmsgpack_value(
        lua_State * L,
        struct rmsgpack_dom_value * value
) {
	uint32_t i;
	switch (value->type) {
	case RDT_INT:
		lua_pushnumber(
		        L,
		        value->int_
		);
		break;
	case RDT_UINT:
		lua_pushnumber(
		        L,
		        value->uint_
		);
		break;
	case RDT_BINARY:
		lua_pushlstring(
		        L,
		        value->binary.buff,
		        value->binary.len
		);
		break;
	case RDT_BOOL:
		lua_pushboolean(
		        L,
		        value->bool_
		);
		break;
	case RDT_NULL:
		lua_pushnil(L);
		break;
	case RDT_STRING:
		lua_pushlstring(
		        L,
		        value->string.buff,
		        value->binary.len
		);
		break;
	case RDT_MAP:
		lua_createtable(L, 0, value->map.len);
		for (i = 0; i < value->map.len; i++) {
			push_rmsgpack_value(
			        L,
			        &value->map.items[i].key
			);
			push_rmsgpack_value(
			        L,
			        &value->map.items[i].value
			);
			lua_settable(L, -3);
		}
		break;
	case RDT_ARRAY:
		lua_createtable(L, value->array.len, 0);
		for (i = 0; i < value->array.len; i++) {
			lua_pushnumber(
			        L,
			        i + 1
			);
			push_rmsgpack_value(
			        L,
			        &value->array.items[i]
			);
			lua_settable(L, -3);
		}
		break;
	}

}

static int cursor_read (lua_State * L) {
	libretrodb_cursor_t *cursor = checkcursor(L);
	struct rmsgpack_dom_value value;
	if (libretrodb_cursor_read_item(cursor, &value) == 0) {
		push_rmsgpack_value(L, &value);
	} else {
		lua_pushnil(L);
	}
	return 1;
}

static int cursor_iter (lua_State * L) {
	libretrodb_cursor_t * cursor = checkcursor(L);
	luaL_getmetafield(L, -1, "read");
	lua_pushvalue(L, -2);
	return 2;
}
