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

#include "lib.h"
#include "array.h"
#include "dict.h"
#include "dlua-script-private.h"
#include "dict-lua-private.h"
#include "dlua-wrapper.h"

struct lua_dict_iter {
	pool_t pool;
	struct dict_iterate_context *iter;
	ARRAY(int) refs;
	int error_ref;

	lua_State *L;
	bool yielded:1;
};

static void lua_dict_iter_unref(struct lua_dict_iter *iter)
{
	const char *error;

	/* deinit iteration if it hasn't been done yet */
	if (dict_iterate_deinit(&iter->iter, &error) < 0) {
		e_error(dlua_script_from_state(iter->L)->event,
			"Dict iteration failed: %s", error);
	}

	pool_unref(&iter->pool);
}

DLUA_WRAP_C_DATA(dict_iter, struct lua_dict_iter, lua_dict_iter_unref, NULL);

static int lua_dict_iterate_step(lua_State *L);

/* resume after a yield */
static int lua_dict_iterate_step_continue(lua_State *L,
					  int status ATTR_UNUSED,
					  lua_KContext ctx ATTR_UNUSED)
{
	return lua_dict_iterate_step(L);
}

static void lua_dict_iterate_more(struct lua_dict_iter *iter);

/*
 * Iteration step function
 *
 * Takes two args (a userdata state, and previous value) and returns the
 * next value.
 */
static int lua_dict_iterate_step(lua_State *L)
{
	struct lua_dict_iter *iter;
	const int *refs;
	unsigned int nrefs;

	DLUA_REQUIRE_ARGS(L, 2);

	iter = xlua_dict_iter_getptr(L, 1, NULL);
	iter->yielded = FALSE;

	lua_dict_iterate_more(iter);

	if (iter->iter != NULL) {
		/* iteration didn't end yet - yield */
		return lua_dict_iterate_step_continue(L,
			      lua_yieldk(L, 0, 0, lua_dict_iterate_step_continue), 0);
	}

	/* dict iteration ended - return first key-value pair */
	refs = array_get(&iter->refs, &nrefs);
	i_assert(nrefs % 2 == 0);

	if (nrefs == 0) {
		if (iter->error_ref != 0) {
			/* dict iteration generated an error - raise it now */
			lua_rawgeti(L, LUA_REGISTRYINDEX, iter->error_ref);
			luaL_unref(L, LUA_REGISTRYINDEX, iter->error_ref);
			return lua_error(L);
		}

		return 0; /* return nil */
	}

	/* get the key & value from the registry */
	lua_rawgeti(L, LUA_REGISTRYINDEX, refs[0]);
	lua_rawgeti(L, LUA_REGISTRYINDEX, refs[1]);
	luaL_unref(L, LUA_REGISTRYINDEX, refs[0]);
	luaL_unref(L, LUA_REGISTRYINDEX, refs[1]);

	array_delete(&iter->refs, 0, 2);

	return 2;
}

static void lua_dict_iterate_more(struct lua_dict_iter *iter)
{
	const char *key, *const *values;
	lua_State *L = iter->L;
	const char *error;

	if (iter->iter == NULL)
		return; /* done iterating the dict */

	while (dict_iterate_values(iter->iter, &key, &values)) {
		int ref;

		/* stash key */
		lua_pushstring(L, key);
		ref = luaL_ref(L, LUA_REGISTRYINDEX);
		array_push_back(&iter->refs, &ref);

		/* stash values */
		lua_newtable(L);
		for (unsigned int i = 0; values[i] != NULL; i++) {
			lua_pushstring(L, values[i]);
			lua_seti(L, -2, i + 1);
		}
		ref = luaL_ref(L, LUA_REGISTRYINDEX);
		array_push_back(&iter->refs, &ref);
	}

	if (dict_iterate_has_more(iter->iter))
		return;

	if (dict_iterate_deinit(&iter->iter, &error) < 0) {
		lua_pushstring(L, error);
		iter->error_ref = luaL_ref(L, LUA_REGISTRYINDEX);
	}
}

/* dict iter callback */
static void lua_dict_iterate_callback(struct lua_dict_iter *iter)
{
	if (iter->yielded)
		return;
	iter->yielded = TRUE;
	dlua_pcall_yieldable_resume(iter->L, 1);
}

/*
 * Iterate a dict at key [-(3|4),+2,e]
 *
 * Args:
 *   1) userdata: sturct dict *dict
 *   2) string: key
 *   3) integer: flags
 *   4*) string: username
 *
 * Returns:
 *   Returns a iteration step function and dict iter userdata.
 *   Username will be NULL if not provided in args.
 */
int lua_dict_iterate(lua_State *L)
{
	enum dict_iterate_flags flags;
	struct lua_dict_iter *iter;
	struct dict *dict;
	const char *path, *username = NULL;
	pool_t pool;

	DLUA_REQUIRE_ARGS_IN(L, 3, 4);

	dict = dlua_check_dict(L, 1);
	path = luaL_checkstring(L, 2);
	flags = luaL_checkinteger(L, 3);
	if (lua_gettop(L) >= 4)
		username = luaL_checkstring(L, 4);
	lua_dict_check_key_prefix(L, path, username);

	struct dict_op_settings set = {
		.username = username,
	};

	/* set up iteration */
	pool = pool_alloconly_create("lua dict iter", 128);
	iter = p_new(pool, struct lua_dict_iter, 1);
	iter->pool = pool;
	iter->iter = dict_iterate_init(dict, &set, path, flags |
				      DICT_ITERATE_FLAG_ASYNC);
	p_array_init(&iter->refs, iter->pool, 32);
	iter->L = L;

	dict_iterate_set_async_callback(iter->iter,
					lua_dict_iterate_callback, iter);

	/* push return values: func, state */
	lua_pushcfunction(L, lua_dict_iterate_step);
	xlua_pushdict_iter(L, iter, FALSE);
	return 2;
}
