/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "dlua-script-private.h"

/*
 * dlua support for threads & thread local storage (TLS)
 *
 * The following code keeps a table in (global) registry.  This table is
 * indexed by the thread objects, each mapping to another table used to hold
 * thread local storage.  That is:
 *
 *   registry[thread] = {}  -- threads table
 *   registry[thread]["foo"] = ... -- TLS value for "foo"
 *
 * This serves two purposes:
 *
 *  (1) It provides TLS.
 *  (2) It acts as a reference to the thread object, preventing it from
 *      being garbage collected.
 *
 * The table is allocated during struct dlua_script's creation and is freed
 * during the scripts destruction.  Any lua threads created using
 * dlua_script_new_thread() will automatically get added to this table.
 */

/* the registry entry with a table with all the lua threads */
#define LUA_THREAD_REGISTRY_KEY "DLUA_THREADS"

static void warn_about_tls_leaks(struct dlua_script *script, lua_State *L);
static void get_tls_table(lua_State *L);

void dlua_init_thread_table(struct dlua_script *script)
{
	lua_newtable(script->L);
	lua_setfield(script->L, LUA_REGISTRYINDEX, LUA_THREAD_REGISTRY_KEY);

	/*
	 * Note that we are *not* adding the main lua state since it is not
	 * a thread.  This implies that it will not have any TLS.
	 */
}

static void warn_about_leaked_threads(struct dlua_script *script)
{
	lua_State *L = script->L;

	lua_getfield(L, LUA_REGISTRYINDEX, LUA_THREAD_REGISTRY_KEY);

	i_assert(lua_type(L, -1) == LUA_TTABLE);

	lua_pushnil(L);
	while (lua_next(L, -2) != 0) {
		/* stack: table, thread, per-thread table */

		/* check the key */
		if (lua_type(L, -2) != LUA_TTHREAD) {
			e_error(script->event, "Unexpected %s key in thread table",
				lua_typename(L, lua_type(L, -2)));
		} else {
			e_error(script->event, "Lua thread %p leaked", lua_tothread(L, -2));
		}

		/* check the value */
		if (lua_type(L, -1) != LUA_TTABLE) {
			e_error(script->event, "Unexpected %s value in thread table",
				lua_typename(L, lua_type(L, -1)));
		} else {
			warn_about_tls_leaks(script, L);
		}

		/* pop the value for lua_next() */
		lua_pop(L, 1);
	}

	lua_pop(L, 1);
}

void dlua_free_thread_table(struct dlua_script *script)
{
	/* all threads should have been closed by now */
	warn_about_leaked_threads(script);

	/* set the thread table to nil - letting GC clean everything up */
	lua_pushnil(script->L);
	lua_setfield(script->L, LUA_REGISTRYINDEX, LUA_THREAD_REGISTRY_KEY);
}

lua_State *dlua_script_new_thread(struct dlua_script *script)
{
	lua_State *thread;

	/* get the threads table */
	lua_getfield(script->L, LUA_REGISTRYINDEX, LUA_THREAD_REGISTRY_KEY);

	/* allocate a new thread */
	thread = lua_newthread(script->L);
	i_assert(thread != NULL);

	/* allocate new TLS table */
	lua_newtable(script->L);

	/* stack: threads-table, thread, TLS-table (top) */

	/* threads-table[thread] = TLS-table */
	lua_settable(script->L, -3);

	/* pop threads table */
	lua_pop(script->L, 1);

	return thread;
}

static void log_tls_leak(struct dlua_script *script, lua_State *L, bool full)
{
	const char *name = NULL;

	/* stack: TLS key, TLS value (top) */

	if (full) {
		lua_getmetatable(L, -1);

		if (dlua_table_get_string_by_str(L, -1, "__name", &name) < 0)
			name = NULL;

		lua_pop(L, 1); /* pop the metatable */
	}

	e_error(script->event, "Lua TLS data in %p thread leaked: key '%s', "
		"value %s %p (%s)", L, lua_tostring(L, -2),
		full ? "userdata" : "lightuserdata",
		lua_touserdata(L, -1), (name != NULL) ? name : "<no name>");
}

static void warn_about_tls_leaks(struct dlua_script *script, lua_State *L)
{
	i_assert(lua_type(L, -1) == LUA_TTABLE);

	lua_pushnil(L);
	while (lua_next(L, -2) != 0) {
		/* stack: table, key, value (top) */

		switch (lua_type(L, -1)) {
		case LUA_TNIL:
		case LUA_TNUMBER:
		case LUA_TBOOLEAN:
		case LUA_TSTRING:
		case LUA_TFUNCTION:
		case LUA_TTHREAD:
			/* these are trivially freed by the Lua GC */
			break;
		case LUA_TTABLE:
			/* recurse into the table */
			warn_about_tls_leaks(script, L);
			break;
		case LUA_TUSERDATA:
			log_tls_leak(script, L, TRUE);
			break;
		case LUA_TLIGHTUSERDATA:
			log_tls_leak(script, L, FALSE);
			break;
		}

		/* pop the value for lua_next() */
		lua_pop(L, 1);
	}
}

void dlua_script_close_thread(struct dlua_script *script, lua_State **_L)
{
	if (*_L == NULL)
		return;

	/* log any TLS userdata leaks */
	get_tls_table(*_L);
	warn_about_tls_leaks(script, *_L);
	lua_pop(*_L, 1);

	/* get the threads table */
	lua_getfield(*_L, LUA_REGISTRYINDEX, LUA_THREAD_REGISTRY_KEY);

	/* push the thread to destroy */
	i_assert(lua_pushthread(*_L) != 1);

	lua_pushnil(*_L);

	/* stack: threads-table, thread, nil (top) */

	/*
	 * threads-table[thread] = nil
	 *
	 * This assignment (1) frees all TLS for the thread, and (2) removes
	 * the reference to the thread saving it from GC.
	 */
	lua_settable(*_L, -3);

	/* pop threads table */
	lua_pop(*_L, 1);

	*_L = NULL;
}

/* get the current thread's TLS table */
static void get_tls_table(lua_State *L)
{
	int ret;

	/* get the threads table */
	ret = dlua_table_get_by_str(L, LUA_REGISTRYINDEX, LUA_TTABLE,
				    LUA_THREAD_REGISTRY_KEY);
	if (ret < 1)
		luaL_error(L, "lua threads table is %s",
			   (ret == 0) ? "missing" : "not a table");

	/* get the TLS-table */
	ret = dlua_table_get_by_thread(L, -1, LUA_TTABLE);
	if (ret < 1)
		luaL_error(L, "lua TLS table for thread %p is not a table", L);

	/* stack: threads-table, TLS-table (top) */

	/* remove threads-table from stack */
	lua_remove(L, -2);
}

void dlua_tls_set_ptr(lua_State *L, const char *name, void *ptr)
{
	get_tls_table(L);
	lua_pushlightuserdata(L, ptr);
	lua_setfield(L, -2, name);
	lua_pop(L, 1);
}

void *dlua_tls_get_ptr(lua_State *L, const char *name)
{
	void *ptr;

	get_tls_table(L);
	lua_getfield(L, -1, name);

	ptr = lua_touserdata(L, -1);

	lua_pop(L, 2);

	return ptr;
}

void dlua_tls_set_int(lua_State *L, const char *name, lua_Integer i)
{
	get_tls_table(L);
	lua_pushinteger(L, i);
	lua_setfield(L, -2, name);
	lua_pop(L, 1);
}

lua_Integer dlua_tls_get_int(lua_State *L, const char *name)
{
	lua_Integer i;

	get_tls_table(L);
	lua_getfield(L, -1, name);

	i = lua_tointeger(L, -1);

	lua_pop(L, 2);

	return i;
}

void dlua_tls_clear(lua_State *L, const char *name)
{
	get_tls_table(L);
	lua_pushnil(L);
	lua_setfield(L, -2, name);
	lua_pop(L, 1);
}
