/*-
 * Copyright 2016 Vsevolod Stakhov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include "lua_common.h"
#include "expression.h"

/***
 * @module rspamd_expression
 * This module can be used to implement different logic expressions in lua using
 * rspamd AST optimizer. There are some examples in individual methods definitions to help understanding of this module.
 */

/***
 * @function rspamd_expression.create(line, {parse_func, [process_func]}, pool)
 * Create expression from the line using atom parsing routines and the specified memory pool
 * @param {string} line expression line
 * @param {table} atom_functions parse_atom function and optional process_atom function
 * @param {rspamd_mempool} memory pool to use for this function
 * @return {expr, err} expression object and error message of `expr` is nil
 * @example
require "fun" ()
local rspamd_expression = require "rspamd_expression"
local rspamd_mempool = require "rspamd_mempool"

local function parse_func(str)
	-- extract token till the first space character
	local token = table.join('', take_while(function(s) return s ~= ' ' end, str))
	-- Return token name
	return token
end

local function process_func(token, task)
	-- Do something using token and task
end

local pool = rspamd_mempool.create()
local expr,err = rspamd_expression.create('A & B | !C', {parse_func, process_func}, pool)
-- Expression is destroyed when the corresponding pool is destroyed
pool:destroy()
 */
LUA_FUNCTION_DEF (expr, create);

/***
 * @method rspamd_expression:to_string()
 * Converts rspamd expression to string
 * @return {string} string representation of rspamd expression
 */
LUA_FUNCTION_DEF (expr, to_string);

/***
 * @method rspamd_expression:process([callback, ]input[, flags])
 * Executes the expression and pass input to process atom callbacks
 * @param {function} callback if not specified on process, then callback must be here
 * @param {any} input input data for processing callbacks
 * @return {number} result of the expression evaluation
 */
LUA_FUNCTION_DEF (expr, process);

/***
 * @method rspamd_expression:process_traced([callback, ]input[, flags])
 * Executes the expression and pass input to process atom callbacks. This function also saves the full trace
 * @param {function} callback if not specified on process, then callback must be here
 * @param {any} input input data for processing callbacks
 * @return {number, table of matched atoms} result of the expression evaluation
 */
LUA_FUNCTION_DEF (expr, process_traced);

/***
 * @method rspamd_expression:atoms()
 * Extract all atoms from the expression as table of strings
 * @return {table/strings} list of all atoms in the expression
 */
LUA_FUNCTION_DEF (expr, atoms);

static const struct luaL_reg exprlib_m[] = {
	LUA_INTERFACE_DEF (expr, to_string),
	LUA_INTERFACE_DEF (expr, atoms),
	LUA_INTERFACE_DEF (expr, process),
	LUA_INTERFACE_DEF (expr, process_traced),
	{"__tostring", lua_expr_to_string},
	{NULL, NULL}
};

static const struct luaL_reg exprlib_f[] = {
	LUA_INTERFACE_DEF (expr, create),
	{NULL, NULL}
};

static rspamd_expression_atom_t * lua_atom_parse (const gchar *line, gsize len,
			rspamd_mempool_t *pool, gpointer ud, GError **err);
static gdouble lua_atom_process (gpointer runtime_ud, rspamd_expression_atom_t *atom);

static const struct rspamd_atom_subr lua_atom_subr = {
	.parse = lua_atom_parse,
	.process = lua_atom_process,
	.priority = NULL,
	.destroy = NULL
};

struct lua_expression {
	struct rspamd_expression *expr;
	gint parse_idx;
	gint process_idx;
	lua_State *L;
	rspamd_mempool_t *pool;
};

static GQuark
lua_expr_quark (void)
{
	return g_quark_from_static_string ("lua-expression");
}

struct lua_expression *
rspamd_lua_expression (lua_State * L, gint pos)
{
	void *ud = rspamd_lua_check_udata (L, pos, "rspamd{expr}");
	luaL_argcheck (L, ud != NULL, pos, "'expr' expected");
	return ud ? *((struct lua_expression **)ud) : NULL;
}

static void
lua_expr_dtor (gpointer p)
{
	struct lua_expression *e = (struct lua_expression *)p;

	if (e->parse_idx != -1) {
		luaL_unref (e->L, LUA_REGISTRYINDEX, e->parse_idx);
	}

	if (e->process_idx != -1) {
		luaL_unref (e->L, LUA_REGISTRYINDEX, e->process_idx);
	}
}

static rspamd_expression_atom_t *
lua_atom_parse (const gchar *line, gsize len,
			rspamd_mempool_t *pool, gpointer ud, GError **err)
{
	struct lua_expression *e = (struct lua_expression *)ud;
	rspamd_expression_atom_t *atom;
	gsize rlen;
	const gchar *tok;

	lua_rawgeti (e->L, LUA_REGISTRYINDEX, e->parse_idx);
	lua_pushlstring (e->L, line, len);

	if (lua_pcall (e->L, 1, 1, 0) != 0) {
		msg_info ("callback call failed: %s", lua_tostring (e->L, -1));
		lua_pop (e->L, 1);
		return NULL;
	}

	if (lua_type (e->L, -1) != LUA_TSTRING) {
		g_set_error (err, lua_expr_quark(), 500, "cannot parse lua atom");
		lua_pop (e->L, 1);
		return NULL;
	}

	tok = lua_tolstring (e->L, -1, &rlen);
	atom = rspamd_mempool_alloc0 (e->pool, sizeof (*atom));
	atom->str = rspamd_mempool_strdup (e->pool, tok);
	atom->len = rlen;
	atom->data = ud;

	lua_pop (e->L, 1);

	return atom;
}

struct lua_atom_process_data {
	lua_State *L;
	struct lua_expression *e;
	gint process_cb_pos;
	gint stack_item;
};

static gdouble
lua_atom_process (gpointer runtime_ud, rspamd_expression_atom_t *atom)
{
	struct lua_atom_process_data *pd = (struct lua_atom_process_data *)runtime_ud;
	gdouble ret = 0;
	guint nargs;
	gint err_idx;

	if (pd->stack_item != -1) {
		nargs = 2;
	}
	else {
		nargs = 1;
	}

	lua_pushcfunction (pd->L, &rspamd_lua_traceback);
	err_idx = lua_gettop (pd->L);

	/* Function position */
	lua_pushvalue (pd->L, pd->process_cb_pos);
	/* Atom name */
	lua_pushlstring (pd->L, atom->str, atom->len);

	/* If we have data passed */
	if (pd->stack_item != -1) {
		lua_pushvalue (pd->L, pd->stack_item);
	}

	if (lua_pcall (pd->L, nargs, 1, err_idx) != 0) {
		msg_info ("expression process callback failed: %s", lua_tostring (pd->L, -1));
	}
	else {
		ret = lua_tonumber (pd->L, -1);
	}

	lua_settop (pd->L, err_idx - 1);

	return ret;
}

static gint
lua_expr_process (lua_State *L)
{
	LUA_TRACE_POINT;
	struct lua_expression *e = rspamd_lua_expression (L, 1);
	struct lua_atom_process_data pd;
	gdouble res;
	gint flags = 0, old_top;

	pd.L = L;
	pd.e = e;
	old_top = lua_gettop (L);

	if (e->process_idx == -1) {
		if (!lua_isfunction (L, 2)) {
			return luaL_error (L, "expression process is called with no callback function");
		}

		pd.process_cb_pos = 2;

		if (lua_type (L, 3) != LUA_TNONE && lua_type (L, 3) != LUA_TNIL) {
			pd.stack_item = 3;
		}
		else {
			pd.stack_item = -1;
		}

		if (lua_isnumber (L, 4)) {
			flags = lua_tointeger (L, 4);
		}
	}
	else {
		lua_rawgeti (L, LUA_REGISTRYINDEX, e->process_idx);
		pd.process_cb_pos = lua_gettop (L);

		if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
			pd.stack_item = 2;
		}
		else {
			pd.stack_item = -1;
		}

		if (lua_isnumber (L, 3)) {
			flags = lua_tointeger (L, 3);
		}
	}

	res = rspamd_process_expression (e->expr, flags, &pd);

	lua_settop (L, old_top);
	lua_pushnumber (L, res);

	return 1;
}

static gint
lua_expr_process_traced (lua_State *L)
{
	LUA_TRACE_POINT;
	struct lua_expression *e = rspamd_lua_expression (L, 1);
	struct lua_atom_process_data pd;
	gdouble res;
	gint flags = 0, old_top;
	GPtrArray *trace;

	pd.L = L;
	pd.e = e;
	old_top = lua_gettop (L);

	if (e->process_idx == -1) {
		if (!lua_isfunction (L, 2)) {
			return luaL_error (L, "expression process is called with no callback function");
		}

		pd.process_cb_pos = 2;
		pd.stack_item = 3;

		if (lua_isnumber (L, 4)) {
			flags = lua_tointeger (L, 4);
		}
	}
	else {
		lua_rawgeti (L, LUA_REGISTRYINDEX, e->process_idx);
		pd.process_cb_pos = lua_gettop (L);
		pd.stack_item = 2;

		if (lua_isnumber (L, 3)) {
			flags = lua_tointeger (L, 3);
		}
	}

	res = rspamd_process_expression_track (e->expr, flags, &pd, &trace);

	lua_settop (L, old_top);
	lua_pushnumber (L, res);

	lua_createtable (L, trace->len, 0);

	for (guint i = 0; i < trace->len; i ++) {
		struct rspamd_expression_atom_s *atom = g_ptr_array_index (trace, i);

		lua_pushlstring (L, atom->str, atom->len);
		lua_rawseti (L, -2, i + 1);
	}

	g_ptr_array_free (trace, TRUE);

	return 2;
}

static gint
lua_expr_create (lua_State *L)
{
	LUA_TRACE_POINT;
	struct lua_expression *e, **pe;
	const char *line;
	gsize len;
	gboolean no_process = FALSE;
	GError *err = NULL;
	rspamd_mempool_t *pool;

	/* Check sanity of the arguments */
	if (lua_type (L, 1) != LUA_TSTRING ||
			(lua_type (L, 2) != LUA_TTABLE && lua_type (L, 2) != LUA_TFUNCTION) ||
			rspamd_lua_check_mempool (L, 3) == NULL) {
		msg_info ("bad arguments to lua_expr_create");
		lua_pushnil (L);
		lua_pushstring (L, "bad arguments");
	}
	else {
		line = lua_tolstring (L, 1, &len);
		pool = rspamd_lua_check_mempool (L, 3);

		e = rspamd_mempool_alloc (pool, sizeof (*e));
		e->L = L;
		e->pool = pool;

		/* Check callbacks */
		if (lua_istable (L, 2)) {
			lua_pushvalue (L, 2);
			lua_pushnumber (L, 1);
			lua_gettable (L, -2);

			if (lua_type (L, -1) != LUA_TFUNCTION) {
				lua_pop (L, 2);
				lua_pushnil (L);
				lua_pushstring (L, "bad parse callback");

				return 2;
			}

			lua_pop (L, 1);

			lua_pushnumber (L, 2);
			lua_gettable (L, -2);

			if (lua_type (L, -1) != LUA_TFUNCTION) {
				if (lua_type (L, -1) != LUA_TNIL && lua_type (L, -1) != LUA_TNONE) {
					lua_pop (L, 2);
					lua_pushnil (L);
					lua_pushstring (L, "bad process callback");

					return 2;
				}
				else {
					no_process = TRUE;
				}
			}

			lua_pop (L, 1);
			/* Table is still on the top of stack */

			lua_pushnumber (L, 1);
			lua_gettable (L, -2);
			e->parse_idx = luaL_ref (L, LUA_REGISTRYINDEX);

			if (!no_process) {
				lua_pushnumber (L, 2);
				lua_gettable (L, -2);
				e->process_idx = luaL_ref (L, LUA_REGISTRYINDEX);
			}
			else {
				e->process_idx = -1;
			}

			lua_pop (L, 1); /* Table */
		}
		else {
			/* Process function is just a function, not a table */
			lua_pushvalue (L, 2);
			e->parse_idx = luaL_ref (L, LUA_REGISTRYINDEX);
			e->process_idx = -1;
		}

		if (!rspamd_parse_expression (line, len, &lua_atom_subr, e, pool, &err,
				&e->expr)) {
			lua_pushnil (L);
			lua_pushstring (L, err->message);
			g_error_free (err);
			lua_expr_dtor (e);

			return 2;
		}

		rspamd_mempool_add_destructor (pool, lua_expr_dtor, e);
		pe = lua_newuserdata (L, sizeof (struct lua_expression *));
		rspamd_lua_setclass (L, "rspamd{expr}", -1);
		*pe = e;
		lua_pushnil (L);
	}

	return 2;
}

static gint
lua_expr_to_string (lua_State *L)
{
	LUA_TRACE_POINT;
	struct lua_expression *e = rspamd_lua_expression (L, 1);
	GString *str;

	if (e != NULL && e->expr != NULL) {
		str = rspamd_expression_tostring (e->expr);
		if (str) {
			lua_pushlstring (L, str->str, str->len);
			g_string_free (str, TRUE);
		}
		else {
			lua_pushnil (L);
		}
	}
	else {
		lua_pushnil (L);
	}

	return 1;
}

struct lua_expr_atoms_cbdata {
	lua_State *L;
	gint idx;
};

static void
lua_exr_atom_cb (const rspamd_ftok_t *tok, gpointer ud)
{
	struct lua_expr_atoms_cbdata *cbdata = ud;

	lua_pushlstring (cbdata->L, tok->begin, tok->len);
	lua_rawseti (cbdata->L, -2, cbdata->idx ++);
}

static gint
lua_expr_atoms (lua_State *L)
{
	LUA_TRACE_POINT;
	struct lua_expression *e = rspamd_lua_expression (L, 1);
	struct lua_expr_atoms_cbdata cbdata;

	if (e != NULL && e->expr != NULL) {
		lua_newtable (L);
		cbdata.L = L;
		cbdata.idx = 1;
		rspamd_expression_atom_foreach (e->expr, lua_exr_atom_cb, &cbdata);
	}
	else {
		lua_pushnil (L);
	}

	return 1;
}

static gint
lua_load_expression (lua_State * L)
{
	lua_newtable (L);
	luaL_register (L, NULL, exprlib_f);

	return 1;
}

void
luaopen_expression (lua_State * L)
{
	rspamd_lua_new_class (L, "rspamd{expr}", exprlib_m);
	lua_pop (L, 1);
	rspamd_lua_add_preload (L, "rspamd_expression", lua_load_expression);
}
