#include "database.js.h"

#include "log.h"
#include "mem.h"
#include "ssb.h"
#include "task.h"

#include "sqlite3.h"

#include <assert.h>
#include <stdbool.h>
#include <string.h>

static JSClassID _database_class_id;
static int _database_count;

typedef struct _database_t
{
	JSContext* context;
	JSValue object;
	void* task;
	const char* id;
} database_t;

static JSValue _database_create(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data);
static void _database_finalizer(JSRuntime *runtime, JSValue value);

static JSValue _database_get(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static JSValue _database_set(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static JSValue _database_exchange(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static JSValue _database_remove(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static JSValue _database_get_all(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static JSValue _database_get_like(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static JSValue _databases_list(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data);

void tf_database_register(JSContext* context)
{
	JS_NewClassID(&_database_class_id);
	JSClassDef def =
	{
		.class_name = "Database",
		.finalizer = &_database_finalizer,
	};
	if (JS_NewClass(JS_GetRuntime(context), _database_class_id, &def) != 0)
	{
		tf_printf("Failed to register database.\n");
	}

	JSValue global = JS_GetGlobalObject(context);
	JSValue constructor = JS_NewCFunctionData(context, _database_create, 0, 0, 0, NULL);
	JS_SetConstructorBit(context, constructor, true);
	JS_SetPropertyStr(context, global, "Database", constructor);
	JSValue databases = JS_NewObject(context);
	JS_SetPropertyStr(context, global, "databases", databases);
	JS_SetPropertyStr(context, databases, "list", JS_NewCFunctionData(context, _databases_list, 0, 0, 0, NULL));
	JS_FreeValue(context, global);
}

static JSValue _database_create(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	++_database_count;
	JSValue object = JS_NewObjectClass(context, _database_class_id);

	database_t* database = tf_malloc(sizeof(database_t));
	*database = (database_t)
	{
		.task = JS_GetContextOpaque(context),
		.context = context,
		.object = object,
	};
	const char* id = JS_ToCString(context, argv[0]);
	database->id = tf_strdup(id);
	JS_FreeCString(context, id);
	JS_SetOpaque(object, database);

	JS_SetPropertyStr(context, object, "get", JS_NewCFunction(context, _database_get, "get", 1));
	JS_SetPropertyStr(context, object, "set", JS_NewCFunction(context, _database_set, "set", 2));
	JS_SetPropertyStr(context, object, "exchange", JS_NewCFunction(context, _database_exchange, "exchange", 2));
	JS_SetPropertyStr(context, object, "remove", JS_NewCFunction(context, _database_remove, "remove", 1));
	JS_SetPropertyStr(context, object, "getAll", JS_NewCFunction(context, _database_get_all, "getAll", 0));
	JS_SetPropertyStr(context, object, "getLike", JS_NewCFunction(context, _database_get_like, "getLike", 1));

	return object;
}

static void _database_finalizer(JSRuntime *runtime, JSValue value)
{
	database_t* database = JS_GetOpaque(value, _database_class_id);
	if (database)
	{
		tf_free((void*)database->id);
		tf_free(database);
	}
	--_database_count;
}

static JSValue _database_get(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
	JSValue entry = JS_UNDEFINED;
	database_t* database = JS_GetOpaque(this_val, _database_class_id);
	if (database)
	{
		tf_ssb_t* ssb = tf_task_get_ssb(database->task);
		sqlite3_stmt* statement;
		sqlite3* db = tf_ssb_acquire_db_reader(ssb);
		if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK)
		{
			size_t length;
			const char* keyString = JS_ToCStringLen(context, &length, argv[0]);
			if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK &&
				sqlite3_bind_text(statement, 2, keyString, length, NULL) == SQLITE_OK &&
				sqlite3_step(statement) == SQLITE_ROW)
			{
				entry = JS_NewStringLen(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0));
			}
			JS_FreeCString(context, keyString);
			sqlite3_finalize(statement);
		}
		tf_ssb_release_db_reader(ssb, db);
	}
	return entry;
}

JSValue _database_set(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
	database_t* database = JS_GetOpaque(this_val, _database_class_id);
	if (database)
	{
		sqlite3_stmt* statement;
		tf_ssb_t* ssb = tf_task_get_ssb(database->task);
		sqlite3* db = tf_ssb_acquire_db_writer(ssb);
		if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ($1, $2, $3)", -1, &statement, NULL) == SQLITE_OK)
		{
			size_t keyLength;
			const char* keyString = JS_ToCStringLen(context, &keyLength, argv[0]);
			size_t valueLength;
			const char* valueString = JS_ToCStringLen(context, &valueLength, argv[1]);
			if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK &&
				sqlite3_bind_text(statement, 2, keyString, keyLength, NULL) == SQLITE_OK &&
				sqlite3_bind_text(statement, 3, valueString, valueLength, NULL) == SQLITE_OK &&
				sqlite3_step(statement) == SQLITE_OK)
			{
			}
			JS_FreeCString(context, keyString);
			JS_FreeCString(context, valueString);
			sqlite3_finalize(statement);
		}
		tf_ssb_release_db_writer(ssb, db);
	}
	return JS_UNDEFINED;
}

static JSValue _database_exchange(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
	JSValue exchanged = JS_UNDEFINED;
	database_t* database = JS_GetOpaque(this_val, _database_class_id);
	if (database)
	{
		sqlite3_stmt* statement;
		tf_ssb_t* ssb = tf_task_get_ssb(database->task);
		sqlite3* db = tf_ssb_acquire_db_writer(ssb);
		if (JS_IsNull(argv[1]) || JS_IsUndefined(argv[1]))
		{
			if (sqlite3_prepare(db, "INSERT INTO properties (id, key, value) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING", -1, &statement, NULL) == SQLITE_OK)
			{
				size_t key_length;
				size_t set_length;
				const char* key = JS_ToCStringLen(context, &key_length, argv[0]);
				const char* set = JS_ToCStringLen(context, &set_length, argv[2]);
				if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK &&
					sqlite3_bind_text(statement, 2, key, key_length, NULL) == SQLITE_OK &&
					sqlite3_bind_text(statement, 3, set, set_length, NULL) == SQLITE_OK &&
					sqlite3_step(statement) == SQLITE_DONE)
				{
					exchanged = sqlite3_changes(db) != 0 ? JS_TRUE : JS_FALSE;
				}
				JS_FreeCString(context, key);
				JS_FreeCString(context, set);
				sqlite3_finalize(statement);
			}
		}
		else if (sqlite3_prepare(db, "UPDATE properties SET value = $1 WHERE id = $2 AND key = $3 AND value = $4", -1, &statement, NULL) == SQLITE_OK)
		{
			size_t key_length;
			size_t expected_length;
			size_t set_length;
			const char* key = JS_ToCStringLen(context, &key_length, argv[0]);
			const char* expected = JS_ToCStringLen(context, &expected_length, argv[1]);
			const char* set = JS_ToCStringLen(context, &set_length, argv[2]);
			if (sqlite3_bind_text(statement, 1, set, set_length, NULL) == SQLITE_OK &&
				sqlite3_bind_text(statement, 2, database->id, -1, NULL) == SQLITE_OK &&
				sqlite3_bind_text(statement, 3, key, key_length, NULL) == SQLITE_OK &&
				sqlite3_bind_text(statement, 4, expected, expected_length, NULL) == SQLITE_OK &&
				sqlite3_step(statement) == SQLITE_DONE)
			{
				exchanged = sqlite3_changes(db) != 0 ? JS_TRUE : JS_FALSE;
			}
			JS_FreeCString(context, key);
			JS_FreeCString(context, expected);
			JS_FreeCString(context, set);
			sqlite3_finalize(statement);
		}
		tf_ssb_release_db_writer(ssb, db);
	}
	return exchanged;
}

JSValue _database_remove(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
	database_t* database = JS_GetOpaque(this_val, _database_class_id);
	if (database)
	{
		sqlite3_stmt* statement;
		tf_ssb_t* ssb = tf_task_get_ssb(database->task);
		sqlite3* db = tf_ssb_acquire_db_writer(ssb);
		if (sqlite3_prepare(db, "DELETE FROM properties WHERE id = $1 AND key = $2", -1, &statement, NULL) == SQLITE_OK)
		{
			size_t keyLength;
			const char* keyString = JS_ToCStringLen(context, &keyLength, argv[0]);
			if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK &&
				sqlite3_bind_text(statement, 2, keyString, keyLength, NULL) == SQLITE_OK &&
				sqlite3_step(statement) == SQLITE_OK)
			{
			}
			JS_FreeCString(context, keyString);
			sqlite3_finalize(statement);
		}
		tf_ssb_release_db_writer(ssb, db);
	}
	return JS_UNDEFINED;
}

JSValue _database_get_all(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
	JSValue array = JS_UNDEFINED;
	database_t* database = JS_GetOpaque(this_val, _database_class_id);
	if (database)
	{
		sqlite3_stmt* statement;
		tf_ssb_t* ssb = tf_task_get_ssb(database->task);
		sqlite3* db = tf_ssb_acquire_db_reader(ssb);
		if (sqlite3_prepare(db, "SELECT key, value FROM properties WHERE id = $1", -1, &statement, NULL) == SQLITE_OK)
		{
			if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK)
			{
				array = JS_NewArray(context);
				uint32_t index = 0;
				while (sqlite3_step(statement) == SQLITE_ROW)
				{
					JS_SetPropertyUint32(context, array, index++, JS_NewStringLen(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0)));
				}
			}
			sqlite3_finalize(statement);
		}
		tf_ssb_release_db_reader(ssb, db);
	}
	return array;
}

JSValue _database_get_like(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
	JSValue result = JS_UNDEFINED;
	database_t* database = JS_GetOpaque(this_val, _database_class_id);
	if (database)
	{
		sqlite3_stmt* statement;
		tf_ssb_t* ssb = tf_task_get_ssb(database->task);
		sqlite3* db = tf_ssb_acquire_db_reader(ssb);
		if (sqlite3_prepare(db, "SELECT key, value FROM properties WHERE id = ? AND KEY LIKE ?", -1, &statement, NULL) == SQLITE_OK)
		{
			const char* pattern = JS_ToCString(context, argv[0]);
			if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK &&
				sqlite3_bind_text(statement, 2, pattern, -1, NULL) == SQLITE_OK)
			{
				result = JS_NewObject(context);
				while (sqlite3_step(statement) == SQLITE_ROW)
				{
					JS_SetPropertyStr(
						context,
						result,
						(const char*)sqlite3_column_text(statement, 0),
						JS_NewStringLen(context, (const char*)sqlite3_column_text(statement, 1), sqlite3_column_bytes(statement, 1)));
				}
			}
			JS_FreeCString(context, pattern);
			sqlite3_finalize(statement);
		}
		tf_ssb_release_db_reader(ssb, db);
	}
	return result;
}

static JSValue _databases_list(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
	tf_task_t* task = tf_task_get(context);
	tf_ssb_t* ssb = tf_task_get_ssb(task);
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	JSValue array = JS_UNDEFINED;
	sqlite3_stmt* statement;
	if (sqlite3_prepare(db, "SELECT DISTINCT id FROM properties WHERE id LIKE ?", -1, &statement, NULL) == SQLITE_OK)
	{
		const char* pattern = JS_ToCString(context, argv[0]);
		if (sqlite3_bind_text(statement, 1, pattern, -1, NULL) == SQLITE_OK)
		{
			array = JS_NewArray(context);
			uint32_t index = 0;
			while (sqlite3_step(statement) == SQLITE_ROW)
			{
				JS_SetPropertyUint32(context, array, index++, JS_NewStringLen(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0)));
			}
		}
		JS_FreeCString(context, pattern);
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_reader(ssb, db);
	return array;
}
