/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "llist.h"
#include "array.h"
#include "istream.h"
#include "sha1.h"
#include "str.h"
#include "hex-binary.h"
#include "eacces-error.h"
#include "ioloop.h"
#include "settings.h"
#include "dlua-script-private.h"
#include "settings.h"

#include <fcntl.h>
#include <unistd.h>

/* the registry entry with a pointer to struct dlua_script */
#define LUA_SCRIPT_REGISTRY_KEY	"DLUA_SCRIPT"

#define LUA_SCRIPT_INIT_FN "script_init"
#define LUA_SCRIPT_DEINIT_FN "script_deinit"

struct event_category event_category_lua = {
	.name = "lua",
};

static struct dlua_script *dlua_scripts = NULL;

#undef DEF
#define DEF(type, name) \
	SETTING_DEFINE_STRUCT_##type("lua_"#name, name, struct dlua_settings)

static const struct setting_define dlua_setting_defines[] = {
	DEF(FILE, file),
	DEF(STRLIST, settings),

	SETTING_DEFINE_LIST_END
};

static const struct dlua_settings dlua_default_settings = {
	.file = "",
	.settings = ARRAY_INIT,
};

const struct setting_parser_info dlua_setting_parser_info = {
	.name = "dlua",

	.defines = dlua_setting_defines,
	.defaults = &dlua_default_settings,

	.struct_size = sizeof(struct dlua_settings),
	.pool_offset1 = 1 + offsetof(struct dlua_settings, pool),
};

static int
dlua_script_create_finish(struct dlua_script *script, const char **error_r);

static void *dlua_alloc(void *ctx, void *ptr, size_t osize, size_t nsize)
{
	struct dlua_script *script =
		(struct dlua_script*)ctx;

	if (nsize == 0) {
		p_free(script->pool, ptr);
		return NULL;
	} else {
		return p_realloc(script->pool, ptr, osize, nsize);
	}
}

static const char *dlua_reader(lua_State *L, void *ctx, size_t *size_r)
{
	struct dlua_script *script =
		(struct dlua_script*)ctx;
	const unsigned char *data;
	i_stream_skip(script->in, script->last_read);
	if (i_stream_read_more(script->in, &data, size_r) == -1 &&
	    script->in->stream_errno != 0) {
		luaL_error(L, "read(%s) failed: %s",
			   script->filename,
			   i_stream_get_error(script->in));
		*size_r = 0;
		return NULL;
	}
	script->last_read = *size_r;
	return (const char*)data;
}

struct dlua_script *dlua_script_from_state(lua_State *L)
{
	struct dlua_script *script;

	/* get light pointer from globals */
	lua_pushstring(L, LUA_SCRIPT_REGISTRY_KEY);
	lua_gettable(L, LUA_REGISTRYINDEX);
	script = lua_touserdata(L, -1);
	lua_pop(L, 1);
	i_assert(script != NULL);

	return script;
}

int dlua_pcall(lua_State *L, const char *func_name, int nargs, int nresults,
	       const char **error_r)
{
	/* record the stack position */
	int ret = 0, debugh_idx, top = lua_gettop(L) - nargs;

	lua_getglobal(L, func_name);

	if (lua_isfunction(L, -1)) {
		/* stack on entry
			args
			func <-- top
		*/
		/* move func name before arguments */
		lua_insert(L, -(nargs + 1));
		/* stack now
			func
			args <-- top
		*/
		lua_getglobal(L, "debug");
		lua_getfield(L, -1, "traceback");
		lua_replace(L, -2);
		/* stack now
			func
			args
			traceback <-- top
		*/
		/* move error handler before func name */
		lua_insert(L, -(nargs + 2));
		/* stack now
			traceback
			func
			args <-- top
		*/
		/* record where traceback is so it's easy to get rid of even
		   if LUA_MULTRET is used. */
		debugh_idx = lua_gettop(L) - nargs - 1;
		ret = lua_pcall(L, nargs, nresults, -(nargs + 2));
		if (ret != LUA_OK) {
			*error_r = t_strdup_printf("lua_pcall(%s, %d, %d) failed: %s",
						   func_name, nargs, nresults,
						   lua_tostring(L, -1));
			/* Remove error and debug handler */
			lua_pop(L, 2);
			ret = -1;
		} else {
			/* remove debug handler from known location */
			lua_remove(L, debugh_idx);
			if (nresults == LUA_MULTRET)
				nresults = lua_gettop(L) - top;
			ret = nresults;
		}
		struct dlua_script *script = dlua_script_from_state(L);
		dlua_event_passthrough_abort(script);
	} else {
		/* ensure stack is clean, remove function and arguments */
		lua_pop(L, nargs + 1);
		*error_r = t_strdup_printf("'%s' is not a function",
					   func_name);
		ret = -1;
	}
#ifdef DEBUG
	if ((ret == -1 && lua_gettop(L) != top) ||
	    (ret >= 0 &&
	     lua_gettop(L) != top + ret)) {
		i_debug("LUA STACK UNCLEAN BEGIN for %s", func_name);
		dlua_dump_stack(L);
		i_debug("LUA STACK UNCLEAN END");
	}
#endif
	/* enforce that stack is clean after call */
	if (ret == -1)
		i_assert(lua_gettop(L) == top);
	else
		i_assert(ret >= 0 && lua_gettop(L) == top + ret);
	return ret;
}

static void dlua_call_deinit_function(struct dlua_script *script)
{
	const char *error;
	if (!dlua_script_has_function(script, LUA_SCRIPT_DEINIT_FN))
		return;
	if (dlua_pcall(script->L, LUA_SCRIPT_DEINIT_FN, 0, 0, &error) < 0)
		e_error(script->event, LUA_SCRIPT_DEINIT_FN"() failed: %s",
			error);
}

int dlua_script_init(struct dlua_script *script, const char **error_r)
{
	const struct dlua_settings *set;

	if (script->init)
		return 0;
	script->init = TRUE;

	if (dlua_script_create_finish(script, error_r) < 0)
		return -1;

	/* lets not fail on missing function... */
	if (!dlua_script_has_function(script, LUA_SCRIPT_INIT_FN))
		return 0;

	if (settings_get(script->event, &dlua_setting_parser_info, 0, &set,
			 error_r) < 0)
		return -1;

	if (!array_is_empty(&set->settings)) {
		i_assert((array_count(&set->settings) % 2 == 0));

		/* prepare a table for arguments */
		lua_createtable(script->L, 0,
				array_count(&set->settings) / 2);
		unsigned int count;
		const char *const *str_array = array_get(&set->settings,
							 &count);
		for (unsigned int i = 0; i < count; i += 2) {
			lua_pushstring(script->L, str_array[i + 1]);
			lua_setfield(script->L, -2, str_array[i]);
		}
	} else {
		lua_newtable(script->L);
	}

	if (dlua_pcall(script->L, LUA_SCRIPT_INIT_FN, 1, 0, error_r) < 0) {
		settings_free(set);
		return -1;
	}

	settings_free(set);
	i_assert(lua_gettop(script->L) == 0);
	return 0;
}

static int dlua_atpanic(lua_State *L)
{
	struct dlua_script *script = dlua_script_from_state(L);
	const char *error = lua_tostring(L, -1);
	i_panic("Lua script '%s': %s", script->filename, error);
}

static struct dlua_script *dlua_create_script(const char *name,
					      struct event *event_parent)
{
	pool_t pool = pool_allocfree_create(t_strdup_printf("lua script %s", name));
	struct dlua_script *script = p_new(pool, struct dlua_script, 1);
	script->pool = pool;
	script->filename = p_strdup(pool, name);
	/* lua API says that lua_newstate will return NULL only if it's out of
	   memory. this cannot really happen with our allocator as it will
	   call i_fatal_status anyways if it runs out of memory */
	script->L = lua_newstate(dlua_alloc, script);
	i_assert(script->L != NULL);
	script->ref = 1;
	lua_atpanic(script->L, dlua_atpanic);
	luaL_openlibs(script->L);
	script->event = event_create(event_parent);
	event_add_str(script->event, "script", script->filename);
	event_add_category(script->event, &event_category_lua);

	dlua_init_thread_table(script);

	DLLIST_PREPEND(&dlua_scripts, script);
	return script;
}

static int dlua_run_script(struct dlua_script *script, const char **error_r)
{
	/* put the error handler before script being called */
	lua_getglobal(script->L, "debug");
	lua_getfield(script->L, -1, "traceback");
	lua_replace(script->L, -2);
	lua_insert(script->L, -2);

	/* we don't want anything to be returned here */
	/* stack before lua_pcall
		debug.traceback
		loaded script as function
	*/
	int err = lua_pcall(script->L, 0, 0, 1);
	dlua_event_passthrough_abort(script);
	if (err != LUA_OK) {
		*error_r = t_strdup_printf("lua_pcall(%s) failed: %s",
					   script->filename,
					   lua_tostring(script->L, -1));
		/* pop error and debug handler */
		lua_pop(script->L, 2);
		err = -1;
	} else {
		/* pop debug handler */
		lua_pop(script->L, 1);
	}
	return err;
}

static int
dlua_script_create_finish(struct dlua_script *script, const char **error_r)
{
	/* store pointer as light data to registry before calling the script */
	lua_pushstring(script->L, LUA_SCRIPT_REGISTRY_KEY);
	lua_pushlightuserdata(script->L, script);
	lua_settable(script->L, LUA_REGISTRYINDEX);

	if (dlua_run_script(script, error_r) < 0)
		return -1;
	i_assert(lua_gettop(script->L) == 0);
	return 0;
}

int dlua_script_create_string(const char *str, struct dlua_script **script_r,
			      struct event *event_parent, const char **error_r)
{
	struct dlua_script *script;
	unsigned char scripthash[SHA1_RESULTLEN];
	const char *fn;

	*script_r = NULL;
	sha1_get_digest(str, strlen(str), scripthash);
	fn = binary_to_hex(scripthash, sizeof(scripthash));

	script = dlua_create_script(fn, event_parent);
	if (luaL_loadstring(script->L, str) == LUA_OK) {
		*script_r = script;
		return 0;
	}
	*error_r = t_strdup_printf("lua_load(<string>) failed: %s",
				   lua_tostring(script->L, -1));
	lua_pop(script->L, 1);
	dlua_script_unref(&script);
	return -1;
}

int dlua_script_create_file(const char *file, struct dlua_script **script_r,
			    struct event *event_parent, const char **error_r)
{
	struct dlua_script *script;

	/* lua reports file access errors poorly */
	if (access(file, O_RDONLY) < 0) {
		if (errno == EACCES)
			*error_r = eacces_error_get("access", file);
		else
			*error_r = t_strdup_printf("access(%s) failed: %m",
						   file);
		return -1;
	}

	script = dlua_create_script(file, event_parent);
	if (luaL_loadfile(script->L, file) != LUA_OK) {
		*error_r = t_strdup_printf("lua_load(%s) failed: %s",
					   file, lua_tostring(script->L, -1));
		dlua_script_unref(&script);
		return -1;
	}

	*script_r = script;
	return 0;
}

int dlua_script_create_stream(struct istream *is, struct dlua_script **script_r,
			      struct event *event_parent, const char **error_r)
{
	struct dlua_script *script;
	const char *filename = i_stream_get_name(is);

	i_assert(filename != NULL && *filename != '\0');

	script = dlua_create_script(filename, event_parent);
	script->in = is;
	script->filename = p_strdup(script->pool, filename);
	if (lua_load(script->L, dlua_reader, script, filename, 0) != LUA_OK) {
		*error_r = t_strdup_printf("lua_load(%s) failed: %s",
					   filename, lua_tostring(script->L, -1));
		dlua_script_unref(&script);
		return -1;
	}

	*script_r = script;
	return 0;
}

int dlua_script_create_auto(struct event *event_parent,
			    struct dlua_script **script_r,
			    const char **error_r)
{
	const struct dlua_settings *set;
	struct settings_file file;
	int ret = 1;

	if (settings_get(event_parent, &dlua_setting_parser_info, 0, &set,
			 error_r) < 0)
		return -1;

	settings_file_get(set->file, set->pool, &file);
	if (set->file[0] == '\0') {
		*error_r = "lua_file setting is empty";
		ret = 0;
	} else if (file.path[0] == '\0') {
		*error_r = "Lua doesn't support inline content for lua_file";
		ret = -1;
	}
	if (ret < 1)  {
		settings_free(set);
		return ret;
	}

	ret = dlua_script_create_file(file.path, script_r, event_parent,
				      error_r);
	if (ret < 0) {
		i_assert(*error_r != NULL);
		*error_r = t_strdup_printf("Lua script '%s': %s", file.path,
					   *error_r);
	} else
		ret = 1;
	settings_free(set);
	return ret;
}

static void dlua_script_destroy(struct dlua_script *script)
{
	dlua_call_deinit_function(script);

	/* close all threads */
	dlua_free_thread_table(script);

	/* close base lua */
	lua_close(script->L);

	/* remove from list */
	DLLIST_REMOVE(&dlua_scripts, script);

	event_unref(&script->event);
	/* then just release memory */
	pool_unref(&script->pool);
}

void dlua_script_ref(struct dlua_script *script)
{
	i_assert(script->ref > 0);
	script->ref++;
}

void dlua_script_unref(struct dlua_script **_script)
{
	struct dlua_script *script = *_script;
	*_script = NULL;

	if (script == NULL) return;

	i_assert(script->ref > 0);
	if (--script->ref > 0)
		return;

	dlua_script_destroy(script);
}

bool dlua_script_has_function(struct dlua_script *script, const char *fn)
{
	i_assert(script != NULL);
	lua_getglobal(script->L, "_G");
	lua_pushstring(script->L, fn);
	lua_rawget(script->L, -2);
	bool ret = lua_isfunction(script->L, -1);
	lua_pop(script->L, 2);
	return ret;
}

void dlua_set_members(lua_State *L, const struct dlua_table_values *values,
		     int idx)
{
	i_assert(L != NULL);
	i_assert(lua_istable(L, idx));
	while(values->name != NULL) {
		switch(values->type) {
		case DLUA_TABLE_VALUE_STRING:
			lua_pushstring(L, values->v.s);
			break;
		case DLUA_TABLE_VALUE_INTEGER:
			lua_pushnumber(L, values->v.i);
			break;
		case DLUA_TABLE_VALUE_DOUBLE:
			lua_pushnumber(L, values->v.d);
			break;
		case DLUA_TABLE_VALUE_BOOLEAN:
			lua_pushboolean(L, values->v.b);
			break;
		case DLUA_TABLE_VALUE_NULL:
			lua_pushnil(L);
			break;
		default:
			i_unreached();
		}
		lua_setfield(L, idx - 1, values->name);
		values++;
	}
}

void dlua_dump_stack(lua_State *L)
{
	/* get everything in stack */
	int top = lua_gettop(L);
	for (int i = 1; i <= top; i++) T_BEGIN {  /* repeat for each level */
		int t = lua_type(L, i);
		string_t *line = t_str_new(32);
		str_printfa(line, "#%d: ", i);
		switch (t) {
		case LUA_TSTRING:  /* strings */
			str_printfa(line, "`%s'", lua_tostring(L, i));
			break;
		case LUA_TBOOLEAN:  /* booleans */
			str_printfa(line, "`%s'", lua_toboolean(L, i) ? "true" : "false");
			break;
		case LUA_TNUMBER:  /* numbers */
			str_printfa(line, "%g", lua_tonumber(L, i));
			break;
		default:  /* other values */
			str_printfa(line, "%s", lua_typename(L, t));
			break;
		}
		i_debug("%s", str_c(line));
	} T_END;
}

/* assorted wrappers */
void dlua_register(struct dlua_script *script, const char *name,
		   lua_CFunction f)
{
	lua_register(script->L, name, f);
}
