#include "database.js.h"

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

#include "sqlite3.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, 1, 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;
}

typedef struct _database_get_t
{
	const char* id;
	const char* key;
	size_t key_length;
	char* out_value;
	size_t out_length;
	JSValue promise[2];
} database_get_t;

static void _database_get_work(tf_ssb_t* ssb, void* user_data)
{
	database_get_t* work = user_data;
	sqlite3_stmt* statement;
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	if (sqlite3_prepare_v2(db, "SELECT value FROM properties WHERE id = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK &&
			sqlite3_step(statement) == SQLITE_ROW)
		{
			size_t length = sqlite3_column_bytes(statement, 0);
			char* data = tf_malloc(length + 1);
			memcpy(data, sqlite3_column_text(statement, 0), length);
			data[length] = '\0';
			work->out_value = data;
			work->out_length = length;
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_reader(ssb, db);
}

static void _database_get_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	database_get_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue result = JS_UNDEFINED;
	if (work->out_value)
	{
		result = JS_NewStringLen(context, work->out_value, work->out_length);
	}
	JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, result);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	tf_free(work->out_value);
	tf_free(work);
}

static JSValue _database_get(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)
	{
		tf_ssb_t* ssb = tf_task_get_ssb(database->task);

		size_t length;
		const char* key = JS_ToCStringLen(context, &length, argv[0]);
		database_get_t* work = tf_malloc(sizeof(database_get_t) + strlen(database->id) + 1 + length + 1);
		*work = (database_get_t) {
			.id = (const char*)(work + 1),
			.key = (const char*)(work + 1) + strlen(database->id) + 1,
			.key_length = length,
		};
		memcpy((char*)work->id, database->id, strlen(database->id) + 1);
		memcpy((char*)work->key, key, length + 1);
		JS_FreeCString(context, key);

		tf_ssb_run_work(ssb, _database_get_work, _database_get_after_work, work);
		result = JS_NewPromiseCapability(context, work->promise);
	}
	return result;
}

typedef struct _database_set_t
{
	const char* id;
	const char* key;
	size_t key_length;
	const char* value;
	size_t value_length;
	bool result;
	JSValue promise[2];
} database_set_t;

static void _database_set_work(tf_ssb_t* ssb, void* user_data)
{
	database_set_t* work = user_data;
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES (?1, ?2, ?3)", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK &&
			sqlite3_bind_text(statement, 3, work->value, work->value_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE)
		{
			work->result = true;
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_writer(ssb, db);
}

static void _database_set_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	database_set_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue result = work->result ? JS_TRUE : JS_UNDEFINED;
	JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, result);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	tf_free(work);
}

static JSValue _database_set(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)
	{
		tf_ssb_t* ssb = tf_task_get_ssb(database->task);

		size_t key_length = 0;
		const char* key = JS_ToCStringLen(context, &key_length, argv[0]);
		size_t value_length = 0;
		const char* value = JS_ToCStringLen(context, &value_length, argv[1]);

		database_set_t* work = tf_malloc(sizeof(database_set_t) + strlen(database->id) + 1 + key_length + 1 + value_length + 1);
		*work = (database_set_t) {
			.id = (const char*)(work + 1),
			.key = (const char*)(work + 1) + strlen(database->id) + 1,
			.value = (const char*)(work + 1) + strlen(database->id) + 1 + key_length + 1,
			.key_length = key_length,
			.value_length = value_length,
		};

		memcpy((char*)work->id, database->id, strlen(database->id) + 1);
		memcpy((char*)work->key, key, key_length + 1);
		memcpy((char*)work->value, value, value_length + 1);

		result = JS_NewPromiseCapability(context, work->promise);
		tf_ssb_run_work(ssb, _database_set_work, _database_set_after_work, work);
		JS_FreeCString(context, key);
		JS_FreeCString(context, value);
	}
	return result;
}

typedef struct _database_exchange_t
{
	const char* id;
	const char* key;
	size_t key_length;
	const char* expected;
	size_t expected_length;
	const char* value;
	size_t value_length;
	bool result;
	JSValue promise[2];
} database_exchange_t;

static void _database_exchange_work(tf_ssb_t* ssb, void* user_data)
{
	database_exchange_t* work = user_data;
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	sqlite3_stmt* statement;
	if (!work->expected)
	{
		if (sqlite3_prepare_v2(db, "INSERT INTO properties (id, key, value) VALUES (?1, ?2, ?3) ON CONFLICT DO NOTHING", -1, &statement, NULL) == SQLITE_OK)
		{
			if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK &&
				sqlite3_bind_text(statement, 3, work->value, work->value_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE)
			{
				work->result = sqlite3_changes(db) != 0;
			}
			sqlite3_finalize(statement);
		}
	}
	else if (sqlite3_prepare_v2(db, "UPDATE properties SET value = ?1 WHERE id = ?2 AND key = ?3 AND value = ?4", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, work->value, work->value_length, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->id, -1, NULL) == SQLITE_OK &&
			sqlite3_bind_text(statement, 3, work->key, work->key_length, NULL) == SQLITE_OK &&
			sqlite3_bind_text(statement, 4, work->expected, work->expected_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE)
		{
			work->result = sqlite3_changes(db) != 0;
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_writer(ssb, db);
}

static void _database_exchange_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	database_exchange_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue result = work->result ? JS_TRUE : JS_UNDEFINED;
	JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, result);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	JS_FreeCString(context, work->key);
	JS_FreeCString(context, work->expected);
	JS_FreeCString(context, work->value);
	tf_free((char*)work->id);
	tf_free(work);
}

static JSValue _database_exchange(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)
	{
		tf_ssb_t* ssb = tf_task_get_ssb(database->task);
		database_exchange_t* work = tf_malloc(sizeof(database_exchange_t));
		*work = (database_exchange_t) {
			.id = tf_strdup(database->id),
		};
		work->key = JS_ToCStringLen(context, &work->key_length, argv[0]);
		work->expected = (JS_IsNull(argv[1]) || JS_IsUndefined(argv[1])) ? NULL : JS_ToCStringLen(context, &work->expected_length, argv[1]);
		work->value = JS_ToCStringLen(context, &work->value_length, argv[2]);
		result = JS_NewPromiseCapability(context, work->promise);
		tf_ssb_run_work(ssb, _database_exchange_work, _database_exchange_after_work, work);
	}
	return result;
}

typedef struct _database_remove_t
{
	const char* id;
	size_t key_length;
	JSValue promise[2];
	char key[];
} database_remove_t;

static void _database_remove_work(tf_ssb_t* ssb, void* user_data)
{
	database_remove_t* work = user_data;
	sqlite3_stmt* statement;
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	if (sqlite3_prepare_v2(db, "DELETE FROM properties WHERE id = ?1 AND key = ?2", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK &&
			sqlite3_step(statement) == SQLITE_OK)
		{
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_writer(ssb, db);
}

static void _database_remove_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	database_remove_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue result = JS_UNDEFINED;
	JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, result);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	tf_free((char*)work->id);
	tf_free(work);
}

static JSValue _database_remove(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)
	{
		size_t key_length = 0;
		const char* key = JS_ToCStringLen(context, &key_length, argv[0]);

		database_remove_t* work = tf_malloc(sizeof(database_remove_t) + key_length + 1);
		*work = (database_remove_t) {
			.id = tf_strdup(database->id),
			.key_length = key_length,
		};
		memcpy(work->key, key, key_length + 1);
		JS_FreeCString(context, key);
		result = JS_NewPromiseCapability(context, work->promise);
		tf_ssb_run_work(tf_task_get_ssb(database->task), _database_remove_work, _database_remove_after_work, work);
	}
	return result;
}

typedef struct _database_get_all_t
{
	const char* id;
	char** out_values;
	size_t* out_lengths;
	int out_values_length;
	JSValue promise[2];
} database_get_all_t;

static void _database_get_all_work(tf_ssb_t* ssb, void* user_data)
{
	database_get_all_t* work = user_data;
	sqlite3_stmt* statement;
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	if (sqlite3_prepare_v2(db, "SELECT key FROM properties WHERE id = ?", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK)
		{
			while (sqlite3_step(statement) == SQLITE_ROW)
			{
				work->out_values = tf_resize_vec(work->out_values, sizeof(char*) * (work->out_values_length + 1));
				work->out_lengths = tf_resize_vec(work->out_lengths, sizeof(size_t) * (work->out_values_length + 1));
				size_t length = sqlite3_column_bytes(statement, 0);
				char* data = tf_malloc(length + 1);
				memcpy(data, sqlite3_column_text(statement, 0), length);
				data[length] = '\0';
				work->out_values[work->out_values_length] = data;
				work->out_lengths[work->out_values_length] = length;
				work->out_values_length++;
			}
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_reader(ssb, db);
}

static void _database_get_all_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	database_get_all_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue result = JS_NewArray(context);
	;
	for (int i = 0; i < work->out_values_length; i++)
	{
		JS_SetPropertyUint32(context, result, i, JS_NewStringLen(context, work->out_values[i], work->out_lengths[i]));
		tf_free((void*)work->out_values[i]);
	}
	JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, result);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	tf_free(work->out_values);
	tf_free(work->out_lengths);
	tf_free(work);
}

static JSValue _database_get_all(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)
	{
		tf_ssb_t* ssb = tf_task_get_ssb(database->task);

		database_get_all_t* work = tf_malloc(sizeof(database_get_all_t) + strlen(database->id) + 1);
		*work = (database_get_all_t) {
			.id = (const char*)(work + 1),
		};
		memcpy((char*)work->id, database->id, strlen(database->id) + 1);

		tf_ssb_run_work(ssb, _database_get_all_work, _database_get_all_after_work, work);
		result = JS_NewPromiseCapability(context, work->promise);
	}
	return result;
}

typedef struct _key_value_t
{
	char* key;
	size_t key_length;
	char* value;
	size_t value_length;
} key_value_t;

typedef struct _database_get_like_t
{
	const char* id;
	const char* pattern;
	key_value_t* results;
	int results_length;
	JSValue promise[2];
} database_get_like_t;

static void _database_get_like_work(tf_ssb_t* ssb, void* user_data)
{
	database_get_like_t* work = user_data;
	sqlite3_stmt* statement;
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	if (sqlite3_prepare_v2(db, "SELECT key, value FROM properties WHERE id = ? AND KEY LIKE ?", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->pattern, -1, NULL) == SQLITE_OK)
		{
			while (sqlite3_step(statement) == SQLITE_ROW)
			{
				work->results = tf_resize_vec(work->results, sizeof(key_value_t) * (work->results_length + 1));
				key_value_t* out = &work->results[work->results_length];
				*out = (key_value_t) {
					.key_length = sqlite3_column_bytes(statement, 0),
					.value_length = sqlite3_column_bytes(statement, 1),
				};
				out->key = tf_malloc(out->key_length + 1);
				memcpy(out->key, sqlite3_column_text(statement, 0), out->key_length + 1);
				out->value = tf_malloc(out->value_length + 1);
				memcpy(out->value, sqlite3_column_text(statement, 1), out->value_length + 1);
				work->results_length++;
			}
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_reader(ssb, db);
}

static void _database_get_like_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	database_get_like_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue result = JS_NewObject(context);
	for (int i = 0; i < work->results_length; i++)
	{
		const key_value_t* row = &work->results[i];
		JS_SetPropertyStr(context, result, row->key, JS_NewStringLen(context, row->value, row->value_length));
		tf_free(row->key);
		tf_free(row->value);
	}
	JS_FreeCString(context, work->pattern);
	JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, result);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	tf_free((void*)work->id);
	tf_free(work->results);
	tf_free(work);
}

static 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)
	{
		tf_ssb_t* ssb = tf_task_get_ssb(database->task);
		database_get_like_t* work = tf_malloc(sizeof(database_get_like_t));
		*work = (database_get_like_t) {
			.id = tf_strdup(database->id),
			.pattern = JS_ToCString(context, argv[0]),
		};
		result = JS_NewPromiseCapability(context, work->promise);
		tf_ssb_run_work(ssb, _database_get_like_work, _database_get_like_after_work, work);
	}
	return result;
}

typedef struct _databases_list_t
{
	const char* pattern;
	char** names;
	int names_length;
	JSValue promise[2];
} databases_list_t;

static void _databases_list_work(tf_ssb_t* ssb, void* user_data)
{
	databases_list_t* work = user_data;
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db, "SELECT DISTINCT id FROM properties WHERE id LIKE ?", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, work->pattern, -1, NULL) == SQLITE_OK)
		{
			while (sqlite3_step(statement) == SQLITE_ROW)
			{
				work->names = tf_resize_vec(work->names, sizeof(char*) * (work->names_length + 1));
				work->names[work->names_length] = tf_strdup((const char*)sqlite3_column_text(statement, 0));
				work->names_length++;
			}
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_reader(ssb, db);
}

static void _databases_list_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	databases_list_t* work = user_data;
	JSContext* context = tf_ssb_get_context(ssb);
	JSValue result = JS_NewArray(context);
	for (int i = 0; i < work->names_length; i++)
	{
		JS_SetPropertyUint32(context, result, i, JS_NewString(context, work->names[i]));
		tf_free(work->names[i]);
	}
	JS_FreeCString(context, work->pattern);
	JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
	tf_util_report_error(context, error);
	JS_FreeValue(context, error);
	JS_FreeValue(context, result);
	JS_FreeValue(context, work->promise[0]);
	JS_FreeValue(context, work->promise[1]);
	tf_free(work->names);
	tf_free(work);
}

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);
	databases_list_t* work = tf_malloc(sizeof(databases_list_t));
	*work = (databases_list_t) {
		.pattern = JS_ToCString(context, argv[0]),
	};
	JSValue result = JS_NewPromiseCapability(context, work->promise);
	tf_ssb_run_work(ssb, _databases_list_work, _databases_list_after_work, work);
	return result;
}
