/*
 * Copyright 2024 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 "lua_tensor.h"
#include "contrib/kann/kann.h"

/***
 * @module rspamd_kann
 * `rspamd_kann` is a Lua interface to kann library
 */

#define KANN_NODE_CLASS rspamd_kann_node_classname
#define KANN_NETWORK_CLASS rspamd_kann_classname

/* Simple macros to define behaviour */
#define KANN_LAYER_DEF(name) static int lua_kann_layer_##name(lua_State *L)
#define KANN_LAYER_INTERFACE(name) \
	{                              \
		#name, lua_kann_layer_##name}

#define KANN_TRANSFORM_DEF(name) static int lua_kann_transform_##name(lua_State *L)
#define KANN_TRANSFORM_INTERFACE(name) \
	{                                  \
		#name, lua_kann_transform_##name}

#define KANN_LOSS_DEF(name) static int lua_kann_loss_##name(lua_State *L)
#define KANN_LOSS_INTERFACE(name) \
	{                             \
		#name, lua_kann_loss_##name}

#define KANN_NEW_DEF(name) static int lua_kann_new_##name(lua_State *L)
#define KANN_NEW_INTERFACE(name) \
	{                            \
		#name, lua_kann_new_##name}


/*
 * Forwarded declarations
 */
static kad_node_t *lua_check_kann_node(lua_State *L, int pos);

/* Layers */
KANN_LAYER_DEF(input);
KANN_LAYER_DEF(dense);
KANN_LAYER_DEF(layernorm);
KANN_LAYER_DEF(rnn);
KANN_LAYER_DEF(lstm);
KANN_LAYER_DEF(gru);
KANN_LAYER_DEF(conv2d);
KANN_LAYER_DEF(conv1d);
KANN_LAYER_DEF(max1d);
KANN_LAYER_DEF(input3d);
KANN_LAYER_DEF(cost);

static int lua_kann_layer_layerdropout(lua_State *L); /* forward declaration */
static int lua_kann_layer_attn_pool(lua_State *L);    /* forward declaration */

static luaL_reg rspamd_kann_layers_f[] = {
	KANN_LAYER_INTERFACE(input),
	KANN_LAYER_INTERFACE(dense),
	KANN_LAYER_INTERFACE(layernorm),
	{"attn_pool", lua_kann_layer_attn_pool},  /* manually registered - extra args */
	{"dropout", lua_kann_layer_layerdropout}, /* manually registered - different naming */
	KANN_LAYER_INTERFACE(rnn),
	KANN_LAYER_INTERFACE(lstm),
	KANN_LAYER_INTERFACE(gru),
	KANN_LAYER_INTERFACE(conv2d),
	KANN_LAYER_INTERFACE(conv1d),
	KANN_LAYER_INTERFACE(max1d),
	KANN_LAYER_INTERFACE(input3d),
	KANN_LAYER_INTERFACE(cost),
	{NULL, NULL},
};

/* Transition and composition functions */

/* General transform */
KANN_TRANSFORM_DEF(add);
KANN_TRANSFORM_DEF(sub);
KANN_TRANSFORM_DEF(mul);
KANN_TRANSFORM_DEF(cmul);
KANN_TRANSFORM_DEF(matmul);

KANN_TRANSFORM_DEF(square);
KANN_TRANSFORM_DEF(sigm);
KANN_TRANSFORM_DEF(tanh);
KANN_TRANSFORM_DEF(relu);
KANN_TRANSFORM_DEF(gelu);
KANN_TRANSFORM_DEF(softmax);
KANN_TRANSFORM_DEF(1minus);
KANN_TRANSFORM_DEF(exp);
KANN_TRANSFORM_DEF(log);
KANN_TRANSFORM_DEF(sin);

static int lua_kann_transform_slice(lua_State *L);  /* forward declaration */
static int lua_kann_transform_concat(lua_State *L); /* forward declaration */

static luaL_reg rspamd_kann_transform_f[] = {
	KANN_TRANSFORM_INTERFACE(add),
	KANN_TRANSFORM_INTERFACE(sub),
	KANN_TRANSFORM_INTERFACE(mul),
	KANN_TRANSFORM_INTERFACE(cmul),
	KANN_TRANSFORM_INTERFACE(matmul),

	KANN_TRANSFORM_INTERFACE(square),
	KANN_TRANSFORM_INTERFACE(sigm),
	KANN_TRANSFORM_INTERFACE(tanh),
	KANN_TRANSFORM_INTERFACE(relu),
	KANN_TRANSFORM_INTERFACE(gelu),
	KANN_TRANSFORM_INTERFACE(softmax),
	KANN_TRANSFORM_INTERFACE(1minus),
	KANN_TRANSFORM_INTERFACE(exp),
	KANN_TRANSFORM_INTERFACE(log),
	KANN_TRANSFORM_INTERFACE(sin),
	{"slice", lua_kann_transform_slice},   /* manually registered - extra args */
	{"concat", lua_kann_transform_concat}, /* manually registered - extra args */
	{NULL, NULL},
};

/* Loss functions */
KANN_LOSS_DEF(mse);
KANN_LOSS_DEF(ce_multi);
KANN_LOSS_DEF(ce_bin);
KANN_LOSS_DEF(ce_bin_neg);
KANN_LOSS_DEF(ce_multi_weighted);
static luaL_reg rspamd_kann_loss_f[] = {
	KANN_LOSS_INTERFACE(mse),
	KANN_LOSS_INTERFACE(ce_multi),
	KANN_LOSS_INTERFACE(ce_bin),
	KANN_LOSS_INTERFACE(ce_bin_neg),
	KANN_LOSS_INTERFACE(ce_multi_weighted),
	{NULL, NULL},
};

/* Creation functions */
KANN_NEW_DEF(leaf);
KANN_NEW_DEF(scalar);
KANN_NEW_DEF(weight);
KANN_NEW_DEF(bias);
KANN_NEW_DEF(weight_conv2d);
KANN_NEW_DEF(weight_conv1d);
KANN_NEW_DEF(kann);

static luaL_reg rspamd_kann_new_f[] = {
	KANN_NEW_INTERFACE(leaf),
	KANN_NEW_INTERFACE(scalar),
	KANN_NEW_INTERFACE(weight),
	KANN_NEW_INTERFACE(bias),
	KANN_NEW_INTERFACE(weight_conv2d),
	KANN_NEW_INTERFACE(weight_conv1d),
	KANN_NEW_INTERFACE(kann),
	{NULL, NULL},
};

LUA_FUNCTION_DEF(kann, load);
LUA_FUNCTION_DEF(kann, destroy);
LUA_FUNCTION_DEF(kann, save);
LUA_FUNCTION_DEF(kann, train1);
LUA_FUNCTION_DEF(kann, apply1);
LUA_FUNCTION_DEF(kann, merge_weights);
LUA_FUNCTION_DEF(kann, is_compatible);

static luaL_reg rspamd_kann_m[] = {
	LUA_INTERFACE_DEF(kann, save),
	LUA_INTERFACE_DEF(kann, train1),
	LUA_INTERFACE_DEF(kann, apply1),
	LUA_INTERFACE_DEF(kann, merge_weights),
	LUA_INTERFACE_DEF(kann, is_compatible),
	{"__gc", lua_kann_destroy},
	{NULL, NULL},
};

static int
rspamd_kann_table_to_flags(lua_State *L, int table_pos)
{
	int result = 0;

	lua_pushvalue(L, table_pos);

	for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1)) {
		int fl = lua_tointeger(L, -1);

		result |= fl;
	}

	lua_pop(L, 1);

	return result;
}

static int
lua_load_kann(lua_State *L)
{
	lua_newtable(L);

	/* Flags */
	lua_pushstring(L, "flag");
	lua_newtable(L);
	lua_pushinteger(L, KANN_F_IN);
	lua_setfield(L, -2, "in");
	lua_pushinteger(L, KANN_F_COST);
	lua_setfield(L, -2, "cost");
	lua_pushinteger(L, KANN_F_OUT);
	lua_setfield(L, -2, "out");
	lua_pushinteger(L, KANN_F_TRUTH);
	lua_setfield(L, -2, "truth");
	lua_settable(L, -3);

	/* Cost type */
	lua_pushstring(L, "cost");
	lua_newtable(L);
	/* binary cross-entropy cost, used with sigmoid */
	lua_pushinteger(L, KANN_C_CEB);
	lua_setfield(L, -2, "ceb");
	/* multi-class cross-entropy cost, used with softmax */
	lua_pushinteger(L, KANN_C_CEM);
	lua_setfield(L, -2, "cem");
	/* binary cross-entropy-like cost, used with tanh */
	lua_pushinteger(L, KANN_C_CEB_NEG);
	lua_setfield(L, -2, "ceb_neg");
	lua_pushinteger(L, KANN_C_MSE);
	lua_setfield(L, -2, "mse");
	lua_settable(L, -3);

	/* RNN flag */
	lua_pushstring(L, "rnn");
	lua_newtable(L);
	/* apply layer normalization */
	lua_pushinteger(L, KANN_RNN_NORM);
	lua_setfield(L, -2, "norm");
	/* take the initial hidden values as variables */
	lua_pushinteger(L, KANN_RNN_VAR_H0);
	lua_setfield(L, -2, "var_h0");
	lua_settable(L, -3);

	/* Layers */
	lua_pushstring(L, "layer");
	lua_newtable(L);
	luaL_register(L, NULL, rspamd_kann_layers_f);
	lua_settable(L, -3);

	/* Transforms */
	lua_pushstring(L, "transform");
	lua_newtable(L);
	luaL_register(L, NULL, rspamd_kann_transform_f);
	lua_settable(L, -3);

	/* Cost */
	lua_pushstring(L, "loss");
	lua_newtable(L);
	luaL_register(L, NULL, rspamd_kann_loss_f);
	lua_settable(L, -3);

	/* Create functions */
	lua_pushstring(L, "new");
	lua_newtable(L);
	luaL_register(L, NULL, rspamd_kann_new_f);
	lua_settable(L, -3);

	/* Load ann from memory or file */
	lua_pushstring(L, "load");
	lua_pushcfunction(L, lua_kann_load);
	lua_settable(L, -3);

	return 1;
}

static kad_node_t *
lua_check_kann_node(lua_State *L, int pos)
{
	void *ud = rspamd_lua_check_udata(L, pos, KANN_NODE_CLASS);
	luaL_argcheck(L, ud != NULL, pos, "'kann_node' expected");
	return ud ? *((kad_node_t **) ud) : NULL;
}

static kann_t *
lua_check_kann(lua_State *L, int pos)
{
	void *ud = rspamd_lua_check_udata(L, pos, KANN_NETWORK_CLASS);
	luaL_argcheck(L, ud != NULL, pos, "'kann' expected");
	return ud ? *((kann_t **) ud) : NULL;
}

void luaopen_kann(lua_State *L)
{
	/* Metatables */
	rspamd_lua_new_class(L, KANN_NODE_CLASS, NULL); /* TODO: add methods */
	lua_pop(L, 1);                                  /* No need in metatable... */
	rspamd_lua_new_class(L, KANN_NETWORK_CLASS, rspamd_kann_m);
	lua_pop(L, 1); /* No need in metatable... */
	rspamd_lua_add_preload(L, "rspamd_kann", lua_load_kann);
	lua_settop(L, 0);
}

/* Layers implementation */
#define PUSH_KAD_NODE(n)                               \
	do {                                               \
		kad_node_t **pt;                               \
		pt = lua_newuserdata(L, sizeof(kad_node_t *)); \
		*pt = (n);                                     \
		rspamd_lua_setclass(L, KANN_NODE_CLASS, -1);   \
	} while (0)

#define PUSH_KAN_NETWORK(n)                             \
	do {                                                \
		kann_t **pn;                                    \
		pn = lua_newuserdata(L, sizeof(kann_t *));      \
		*pn = (n);                                      \
		rspamd_lua_setclass(L, KANN_NETWORK_CLASS, -1); \
	} while (0)

#define PROCESS_KAD_FLAGS(n, pos)                                                            \
	do {                                                                                     \
		int fl = 0;                                                                          \
		if (lua_type(L, (pos)) == LUA_TTABLE) { fl = rspamd_kann_table_to_flags(L, (pos)); } \
		else if (lua_type(L, (pos)) == LUA_TNUMBER) {                                        \
			fl = lua_tointeger(L, (pos));                                                    \
		}                                                                                    \
		(n)->ext_flag |= fl;                                                                 \
	} while (0)

/***
 * @function kann.layer.input(ninputs[, flags])
 * Creates an input layer for ANN
 * @param {int} ninputs number of inputs
 * @param {table|int} flags optional flags
 * @return {kann_node} kann node object (should be used to combine ANN)
*/
static int
lua_kann_layer_input(lua_State *L)
{
	int nnodes = luaL_checkinteger(L, 1);

	if (nnodes > 0) {
		kad_node_t *t;

		t = kann_layer_input(nnodes);

		PROCESS_KAD_FLAGS(t, 2);
		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments, nnodes required");
	}

	return 1;
}

/***
 * @function kann.layer.dense(in, ninputs[, flags])
 * Creates a dense layer (e.g. for hidden layer)
 * @param {kann_node} in kann node
 * @param {int} ninputs number of dense nodes
 * @param {table|int} flags optional flags
 * @return {kann_node} kann node object (should be used to combine ANN)
*/
static int
lua_kann_layer_dense(lua_State *L)
{
	kad_node_t *in = lua_check_kann_node(L, 1);
	int nnodes = luaL_checkinteger(L, 2);

	if (in != NULL && nnodes > 0) {
		kad_node_t *t;

		t = kann_layer_dense(in, nnodes);

		PROCESS_KAD_FLAGS(t, 3);
		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments, input + nnodes required");
	}

	return 1;
}

/***
 * @function kann.layer.dropout(in, ratio[, flags])
 * Creates a dropout layer
 * @param {kann_node} in kann node
 * @param {float} ratio drop ratio
 * @param {table|int} flags optional flags
 * @return {kann_node} kann node object (should be used to combine ANN)
*/
static int
lua_kann_layer_layerdropout(lua_State *L)
{
	kad_node_t *in = lua_check_kann_node(L, 1);
	double r = luaL_checknumber(L, 2);

	if (in != NULL) {
		kad_node_t *t;

		t = kann_layer_dropout(in, r);

		PROCESS_KAD_FLAGS(t, 3);
		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments, input + rate required");
	}

	return 1;
}

/***
 * @function kann.layer.attn_pool(in, n_words[, n_heads[, flags]])
 * Creates a multi-head attention pooling layer over a flattened sequence of
 * zero-padded word vectors. The input dimension must be a multiple of
 * n_words; the per-word dimension is derived as input_dim / n_words.
 * Output dimension is n_heads * per-word dimension.
 * @param {kann_node} in kann node, (batch, n_words * dim)
 * @param {int} n_words number of word positions in the sequence
 * @param {int} n_heads number of learned attention queries (default 4)
 * @param {table|int} flags optional flags
 * @return {kann_node} kann node object (should be used to combine ANN)
*/
static int
lua_kann_layer_attn_pool(lua_State *L)
{
	kad_node_t *in = lua_check_kann_node(L, 1);
	int n_words = luaL_checkinteger(L, 2);
	int n_heads = luaL_optinteger(L, 3, 4);

	if (in != NULL && n_words > 0 && n_heads > 0) {
		kad_node_t *t;

		t = kann_layer_attn_pool(in, n_words, n_heads);

		if (t == NULL) {
			return luaL_error(L, "invalid attn_pool: input dimension %d "
								 "is not a multiple of n_words %d",
							  in->n_d == 2 ? in->d[1] : -1, n_words);
		}

		PROCESS_KAD_FLAGS(t, 4);
		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments, input + n_words + n_heads required");
	}

	return 1;
}

/***
 * @function kann.transform.slice(in, axis, start, end)
 * Takes a slice [start, end) on the axis-th dimension (both 0-based, the
 * batch dimension is axis 0, so feature slicing of a plain input is axis 1).
 * @param {kann_node} in kann node
 * @param {int} axis 0-based dimension index
 * @param {int} start 0-based start offset (inclusive)
 * @param {int} end 0-based end offset (exclusive)
 * @return {kann_node} kann node object
*/
static int
lua_kann_transform_slice(lua_State *L)
{
	kad_node_t *in = lua_check_kann_node(L, 1);
	int axis = luaL_checkinteger(L, 2);
	int start = luaL_checkinteger(L, 3);
	int end = luaL_checkinteger(L, 4);

	if (in != NULL && start >= 0 && end > start) {
		kad_node_t *t;

		t = kad_slice(in, axis, start, end);

		if (t == NULL) {
			return luaL_error(L, "invalid slice [%d, %d) on axis %d", start, end, axis);
		}

		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments, input + axis + [start, end) required");
	}

	return 1;
}

/***
 * @function kann.transform.concat(axis, in1, in2[, ...])
 * Concatenates nodes on the axis-th dimension (0-based; feature
 * concatenation of plain vectors is axis 1).
 * @param {int} axis 0-based dimension index
 * @param {kann_node} in1, in2, ... nodes to concatenate
 * @return {kann_node} kann node object
*/
static int
lua_kann_transform_concat(lua_State *L)
{
	int axis = luaL_checkinteger(L, 1);
	int n = lua_gettop(L) - 1;
	kad_node_t *nodes[32];

	if (n < 2 || n > (int) G_N_ELEMENTS(nodes)) {
		return luaL_error(L, "concat requires from 2 to %d nodes, got %d",
						  (int) G_N_ELEMENTS(nodes), n);
	}

	for (int i = 0; i < n; i++) {
		nodes[i] = lua_check_kann_node(L, i + 2);

		if (nodes[i] == NULL) {
			return luaL_error(L, "invalid node at position %d", i + 1);
		}
	}

	kad_node_t *t = kad_concat_array(axis, n, nodes);

	if (t == NULL) {
		return luaL_error(L, "cannot concat nodes on axis %d (dimension mismatch?)", axis);
	}

	PUSH_KAD_NODE(t);

	return 1;
}

/***
 * @function kann.layer.dropout(in [, flags])
 * Creates a normalisation layer
 * @param {kann_node} in kann node
 * @param {table|int} flags optional flags
 * @return {kann_node} kann node object (should be used to combine ANN)
*/
static int
lua_kann_layer_layernorm(lua_State *L)
{
	kad_node_t *in = lua_check_kann_node(L, 1);

	if (in != NULL) {
		kad_node_t *t;

		t = kann_layer_layernorm(in);

		PROCESS_KAD_FLAGS(t, 2);
		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments, input required");
	}

	return 1;
}

/***
 * @function kann.layer.rnn(in, nnodes[, rnn_flags, [, flags]])
 * Creates a recursive NN layer
 * @param {kann_node} in kann node
 * @param {int} nnodes number of cells
 * @param {int} rnnflags rnn flags
 * @param {table|int} flags optional flags
 * @return {kann_node} kann node object (should be used to combine ANN)
*/
static int
lua_kann_layer_rnn(lua_State *L)
{
	kad_node_t *in = lua_check_kann_node(L, 1);
	int nnodes = luaL_checkinteger(L, 2);
	int rnnflags = 0;

	if (in != NULL && nnodes > 0) {
		kad_node_t *t;

		if (lua_type(L, 3) == LUA_TNUMBER) {
			rnnflags = lua_tointeger(L, 3);
		}

		t = kann_layer_rnn(in, nnodes, rnnflags);

		PROCESS_KAD_FLAGS(t, 4);
		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments, input + nnodes required");
	}

	return 1;
}

/***
 * @function kann.layer.lstm(in, nnodes[, rnn_flags, [, flags]])
 * Creates a recursive NN layer using LSTM cells
 * @param {kann_node} in kann node
 * @param {int} nnodes number of cells
 * @param {int} rnnflags rnn flags
 * @param {table|int} flags optional flags
 * @return {kann_node} kann node object (should be used to combine ANN)
*/
static int
lua_kann_layer_lstm(lua_State *L)
{
	kad_node_t *in = lua_check_kann_node(L, 1);
	int nnodes = luaL_checkinteger(L, 2);
	int rnnflags = 0;

	if (in != NULL && nnodes > 0) {
		kad_node_t *t;

		if (lua_type(L, 3) == LUA_TNUMBER) {
			rnnflags = lua_tointeger(L, 3);
		}

		t = kann_layer_lstm(in, nnodes, rnnflags);

		PROCESS_KAD_FLAGS(t, 4);
		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments, input + nnodes required");
	}

	return 1;
}

/***
 * @function kann.layer.rnn(in, nnodes[, rnn_flags, [, flags]])
 * Creates a recursive NN layer using GRU cells
 * @param {kann_node} in kann node
 * @param {int} nnodes number of cells
 * @param {int} rnnflags rnn flags
 * @param {table|int} flags optional flags
 * @return {kann_node} kann node object (should be used to combine ANN)
*/
static int
lua_kann_layer_gru(lua_State *L)
{
	kad_node_t *in = lua_check_kann_node(L, 1);
	int nnodes = luaL_checkinteger(L, 2);
	int rnnflags = 0;

	if (in != NULL && nnodes > 0) {
		kad_node_t *t;

		if (lua_type(L, 3) == LUA_TNUMBER) {
			rnnflags = lua_tointeger(L, 3);
		}

		t = kann_layer_gru(in, nnodes, rnnflags);

		PROCESS_KAD_FLAGS(t, 4);
		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments, input + nnodes required");
	}

	return 1;
}

/***
 * @function kann.layer.conv2d(in, n_flt, k_rows, k_cols, stride_rows, stride_cols, pad_rows, pad_columns[, flags])
 * Creates a 2D convolution layer
 * @param {kann_node} in kann node
 * @param {int} n_flt number of filters
 * @param {int} k_rows kernel rows
 * @param {int} k_cols kernel columns
 * @param {int} stride_rows stride rows
 * @param {int} stride_cols stride columns
 * @param {int} pad_rows padding rows
 * @param {int} pad_columns padding columns
 * @param {table|int} flags optional flags
 * @return {kann_node} kann node object (should be used to combine ANN)
*/
static int
lua_kann_layer_conv2d(lua_State *L)
{
	kad_node_t *in = lua_check_kann_node(L, 1);
	int n_flt = luaL_checkinteger(L, 2);
	int k_rows = luaL_checkinteger(L, 3);
	int k_cols = luaL_checkinteger(L, 4);
	int stride_r = luaL_checkinteger(L, 5);
	int stride_c = luaL_checkinteger(L, 6);
	int pad_r = luaL_checkinteger(L, 7);
	int pad_c = luaL_checkinteger(L, 8);

	if (in != NULL) {
		kad_node_t *t;
		t = kann_layer_conv2d(in, n_flt, k_rows, k_cols, stride_r, stride_c,
							  pad_r, pad_c);

		PROCESS_KAD_FLAGS(t, 9);
		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments, input, nflt, kx, ky, stridex, stridey, padx, pady are required");
	}

	return 1;
}

/***
 * @function kann.layer.conv1d(in, n_flt, kern_size, stride_size, pad_size[, flags])
 * Creates 1D convolution layer
 * @param {kann_node} in kann node
 * @param {int} n_flt number of filters
 * @param {int} kern_size kernel rows
 * @param {int} stride_size stride rows
 * @param {int} pad_size padding rows
 * @param {table|int} flags optional flags
 * @return {kann_node} kann node object (should be used to combine ANN)
*/
static int
lua_kann_layer_conv1d(lua_State *L)
{
	kad_node_t *in = lua_check_kann_node(L, 1);
	int n_flt = luaL_checkinteger(L, 2);
	int k_size = luaL_checkinteger(L, 3);
	int stride = luaL_checkinteger(L, 4);
	int pad = luaL_checkinteger(L, 5);

	if (in != NULL) {
		kad_node_t *t;
		t = kann_layer_conv1d(in, n_flt, k_size, stride, pad);

		PROCESS_KAD_FLAGS(t, 6);
		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments, input, nflt, k, stride, pad required");
	}

	return 1;
}

/***
 * @function kann.layer.max1d(in, kern_size, stride_size, pad_size[, flags])
 * Creates 1D max pooling layer (for use after conv1d)
 * @param {kann_node} in kann node (must be 3D: NCW)
 * @param {int} kern_size kernel size (use width for global pooling)
 * @param {int} stride_size stride
 * @param {int} pad_size padding
 * @param {table|int} flags optional flags
 * @return {kann_node} kann node object (should be used to combine ANN)
*/
static int
lua_kann_layer_max1d(lua_State *L)
{
	kad_node_t *in = lua_check_kann_node(L, 1);
	int k_size = luaL_checkinteger(L, 2);
	int stride = luaL_checkinteger(L, 3);
	int pad = luaL_checkinteger(L, 4);

	if (in != NULL) {
		kad_node_t *t;
		t = kad_max1d(in, k_size, stride, pad);

		if (t == NULL) {
			return luaL_error(L, "max1d requires 3D (NCW) input");
		}

		PROCESS_KAD_FLAGS(t, 5);
		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments, input, k, stride, pad required");
	}

	return 1;
}

/***
 * @function kann.layer.input3d(channels, width[, flags])
 * Creates a 3D input layer in NCW format (for conv1d networks)
 * @param {int} channels number of channels (e.g. embedding dimension)
 * @param {int} width sequence length (e.g. max words)
 * @param {table|int} flags optional flags
 * @return {kann_node} kann node object (should be used to combine ANN)
*/
static int
lua_kann_layer_input3d(lua_State *L)
{
	int channels = luaL_checkinteger(L, 1);
	int width = luaL_checkinteger(L, 2);

	if (channels > 0 && width > 0) {
		kad_node_t *t;

		t = kad_feed(3, 1, channels, width);
		t->ext_flag |= KANN_F_IN;

		PROCESS_KAD_FLAGS(t, 3);
		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments, channels and width required");
	}

	return 1;
}

/***
 * @function kann.layer.cost(in, nout, cost_type[, flags])
 * Creates cost layer
 * @param {kann_node} in kann node
 * @param {int} nout number of outputs
 * @param {int} cost_type see kann.cost table
 * @param {table|int} flags optional flags
 * @return {kann_node} kann node object (should be used to combine ANN)
*/
static int
lua_kann_layer_cost(lua_State *L)
{
	kad_node_t *in = lua_check_kann_node(L, 1);
	int nout = luaL_checkinteger(L, 2);
	int cost_type = luaL_checkinteger(L, 3);

	if (in != NULL && nout > 0) {
		kad_node_t *t;
		t = kann_layer_cost(in, nout, cost_type);

		PROCESS_KAD_FLAGS(t, 4);
		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments, input, nout and cost_type are required");
	}

	return 1;
}

/* Generic helpers */
static int
lua_kann_call_unary_function(lua_State *L, const char *name,
							 kad_node_t *(*func)(kad_node_t *) )
{
	kad_node_t *in = lua_check_kann_node(L, 1);

	if (in != NULL) {
		kad_node_t *t;
		t = func(in);

		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments for %s, input required", name);
	}

	return 1;
}
static int
lua_kann_call_binary_function(lua_State *L, const char *name,
							  kad_node_t *(*func)(kad_node_t *, kad_node_t *) )
{
	kad_node_t *x = lua_check_kann_node(L, 1);
	kad_node_t *y = lua_check_kann_node(L, 2);

	if (x != NULL && y != NULL) {
		kad_node_t *t;
		t = func(x, y);

		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments for %s, 2 inputs required", name);
	}

	return 1;
}

#define LUA_UNARY_TRANSFORM_FUNC_IMPL(name)                        \
	static int lua_kann_transform_##name(lua_State *L)             \
	{                                                              \
		return lua_kann_call_unary_function(L, #name, kad_##name); \
	}

#define LUA_BINARY_TRANSFORM_FUNC_IMPL(name)                        \
	static int lua_kann_transform_##name(lua_State *L)              \
	{                                                               \
		return lua_kann_call_binary_function(L, #name, kad_##name); \
	}

#define LUA_LOSS_FUNC_IMPL(name)                                    \
	static int lua_kann_loss_##name(lua_State *L)                   \
	{                                                               \
		return lua_kann_call_binary_function(L, #name, kad_##name); \
	}

/* Transform functions registered via macro helpers */
LUA_BINARY_TRANSFORM_FUNC_IMPL(add)
LUA_BINARY_TRANSFORM_FUNC_IMPL(sub)
LUA_BINARY_TRANSFORM_FUNC_IMPL(mul)
LUA_BINARY_TRANSFORM_FUNC_IMPL(cmul)
LUA_BINARY_TRANSFORM_FUNC_IMPL(matmul)

LUA_UNARY_TRANSFORM_FUNC_IMPL(square)
LUA_UNARY_TRANSFORM_FUNC_IMPL(sigm)
LUA_UNARY_TRANSFORM_FUNC_IMPL(tanh)
LUA_UNARY_TRANSFORM_FUNC_IMPL(relu)
LUA_UNARY_TRANSFORM_FUNC_IMPL(gelu)
LUA_UNARY_TRANSFORM_FUNC_IMPL(softmax)
LUA_UNARY_TRANSFORM_FUNC_IMPL(1minus)
LUA_UNARY_TRANSFORM_FUNC_IMPL(exp)
LUA_UNARY_TRANSFORM_FUNC_IMPL(log)
LUA_UNARY_TRANSFORM_FUNC_IMPL(sin)

/* Generic cost functions */
LUA_LOSS_FUNC_IMPL(mse)
LUA_LOSS_FUNC_IMPL(ce_multi)
LUA_LOSS_FUNC_IMPL(ce_bin)
LUA_LOSS_FUNC_IMPL(ce_bin_neg)

/* The only case of ternary weight function */
static int
lua_kann_loss_ce_multi_weighted(lua_State *L)
{
	kad_node_t *pred = lua_check_kann_node(L, 1);
	kad_node_t *truth = lua_check_kann_node(L, 2);
	kad_node_t *weight = lua_check_kann_node(L, 3);

	if (pred != NULL && truth != NULL && weight != NULL) {
		kad_node_t *t;
		t = kad_ce_multi_weighted(pred, truth, weight);

		PUSH_KAD_NODE(t);
	}
	else {
		return luaL_error(L, "invalid arguments for ce_multi_weighted, 3 inputs required");
	}

	return 1;
}

/* Creation functions */
static int
lua_kann_new_scalar(lua_State *L)
{
	int flag = luaL_checkinteger(L, 1);
	double x = luaL_checknumber(L, 2);
	kad_node_t *t;

	t = kann_new_scalar(flag, x);

	PROCESS_KAD_FLAGS(t, 3);
	PUSH_KAD_NODE(t);

	return 1;
}

static int
lua_kann_new_weight(lua_State *L)
{
	int nrow = luaL_checkinteger(L, 1);
	int ncol = luaL_checkinteger(L, 2);
	kad_node_t *t;

	t = kann_new_weight(nrow, ncol);

	PROCESS_KAD_FLAGS(t, 3);
	PUSH_KAD_NODE(t);

	return 1;
}

static int
lua_kann_new_bias(lua_State *L)
{
	int n = luaL_checkinteger(L, 1);
	kad_node_t *t;

	t = kann_new_bias(n);

	PROCESS_KAD_FLAGS(t, 2);
	PUSH_KAD_NODE(t);

	return 1;
}

static int
lua_kann_new_weight_conv2d(lua_State *L)
{
	int nout = luaL_checkinteger(L, 1);
	int nin = luaL_checkinteger(L, 2);
	int krow = luaL_checkinteger(L, 3);
	int kcol = luaL_checkinteger(L, 4);
	kad_node_t *t;

	t = kann_new_weight_conv2d(nout, nin, krow, kcol);

	PROCESS_KAD_FLAGS(t, 5);
	PUSH_KAD_NODE(t);

	return 1;
}

static int
lua_kann_new_weight_conv1d(lua_State *L)
{
	int nout = luaL_checkinteger(L, 1);
	int nin = luaL_checkinteger(L, 2);
	int klen = luaL_checkinteger(L, 3);
	kad_node_t *t;

	t = kann_new_weight_conv1d(nout, nin, klen);

	PROCESS_KAD_FLAGS(t, 4);
	PUSH_KAD_NODE(t);

	return 1;
}

static int
lua_kann_new_leaf(lua_State *L)
{
	int dim = luaL_checkinteger(L, 1), i, *ar;
	kad_node_t *t;

	if (dim >= 1 && dim < KAD_MAX_DIM && lua_istable(L, 2)) {
		ar = g_new0(int, KAD_MAX_DIM);

		for (i = 0; i < dim; i++) {
			lua_rawgeti(L, 2, i + 1);
			ar[i] = lua_tointeger(L, -1);
			lua_pop(L, 1);
		}

		t = kann_new_leaf_array(NULL, NULL, 0, 0.0, dim, ar);

		PROCESS_KAD_FLAGS(t, 3);
		PUSH_KAD_NODE(t);

		g_free(ar);
	}
	else {
		return luaL_error(L, "invalid arguments for new.leaf, "
							 "dim and vector of elements are required");
	}

	return 1;
}

static int
lua_kann_new_kann(lua_State *L)
{
	kad_node_t *cost = lua_check_kann_node(L, 1);
	kann_t *k;

	if (cost) {
		k = kann_new(cost, 0);

		PUSH_KAN_NETWORK(k);
	}
	else {
		return luaL_error(L, "invalid arguments for new.kann, "
							 "cost node is required");
	}

	return 1;
}

static int
lua_kann_destroy(lua_State *L)
{
	kann_t *k = lua_check_kann(L, 1);

	kann_delete(k);

	return 0;
}

static int
lua_kann_save(lua_State *L)
{
	kann_t *k = lua_check_kann(L, 1);

	if (k) {
		if (lua_istable(L, 2)) {
			lua_getfield(L, 2, "filename");

			if (lua_isstring(L, -1)) {
				const char *fname = lua_tostring(L, -1);
				FILE *f;

				f = fopen(fname, "w");

				if (!f) {
					lua_pop(L, 1);

					return luaL_error(L, "cannot open %s for writing: %s",
									  fname, strerror(errno));
				}

				kann_save_fp(f, k);
				fclose(f);

				/* Pop the `filename` field before pushing the result */
				lua_pop(L, 1);
				lua_pushboolean(L, true);
			}
			else {
				lua_pop(L, 1);

				return luaL_error(L, "invalid arguments: missing filename");
			}
		}
		else {
			/* Save to Rspamd text */
#ifndef HAVE_OPENMEMSTREAM
			return luaL_error(L, "no support of saving to memory on your system");
#endif
			FILE *f;
			char *buf = NULL;
			size_t buflen;
			struct rspamd_lua_text *t;

			f = open_memstream(&buf, &buflen);
			g_assert(f != NULL);

			kann_save_fp(f, k);
			fclose(f);

			t = lua_newuserdata(L, sizeof(*t));
			rspamd_lua_setclass(L, rspamd_text_classname, -1);
			t->flags = RSPAMD_TEXT_FLAG_OWN;
			t->start = (const char *) buf;
			t->len = buflen;
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_kann_load(lua_State *L)
{
	kann_t *k;
	FILE *f = NULL;

	if (lua_istable(L, 1)) {
		/*
		 * load is a module function (rspamd_kann.load), not a method, so the
		 * options table is at arg 1 (unlike save where arg 1 is self).
		 */
		lua_getfield(L, 1, "filename");

		if (lua_isstring(L, -1)) {
			const char *fname = lua_tostring(L, -1);

			f = fopen(fname, "rb");
		}
		else {
			lua_pop(L, 1);

			return luaL_error(L, "invalid arguments: missing filename");
		}

		lua_pop(L, 1);
	}
	else if (lua_isstring(L, 1)) {
		gsize dlen;
		const char *data;

		data = lua_tolstring(L, 1, &dlen);

#ifndef HAVE_FMEMOPEN
		return luaL_error(L, "no support of loading from memory on your system");
#endif
		f = fmemopen((void *) data, dlen, "rb");
	}
	else if (lua_isuserdata(L, 1)) {
		struct rspamd_lua_text *t;

		t = lua_check_text(L, 1);

		if (!t) {
			return luaL_error(L, "invalid arguments");
		}

#ifndef HAVE_FMEMOPEN
		return luaL_error(L, "no support of loading from memory on your system");
#endif
		f = fmemopen((void *) t->start, t->len, "rb");
	}

	if (f == NULL) {
		return luaL_error(L, "invalid arguments or cannot open file");
	}

	k = kann_load_fp(f);
	fclose(f);

	if (k == NULL) {
		lua_pushnil(L);
	}
	else {
		PUSH_KAN_NETWORK(k);
	}

	return 1;
}

struct rspamd_kann_train_cbdata {
	lua_State *L;
	kann_t *k;
	int cbref;
};

static void
lua_kann_train_cb(int iter, float train_cost, float val_cost, void *ud)
{
	struct rspamd_kann_train_cbdata *cbd = (struct rspamd_kann_train_cbdata *) ud;

	if (cbd->cbref != -1) {
		int err_idx;
		lua_State *L = cbd->L;

		lua_pushcfunction(L, &rspamd_lua_traceback);
		err_idx = lua_gettop(L);

		lua_rawgeti(L, LUA_REGISTRYINDEX, cbd->cbref);
		lua_pushinteger(L, iter);
		lua_pushnumber(L, train_cost);
		lua_pushnumber(L, val_cost);

		if (lua_pcall(L, 3, 0, err_idx) != 0) {
			msg_err("cannot run lua train callback: %s",
					lua_tostring(L, -1));
		}

		lua_settop(L, err_idx - 1);
	}
}

#define FREE_VEC(a, n)                                \
	do {                                              \
		for (int i = 0; i < (n); i++) g_free((a)[i]); \
		g_free(a);                                    \
	} while (0)

static int
lua_kann_train1(lua_State *L)
{
	kann_t *k = lua_check_kann(L, 1);
	struct rspamd_lua_tensor *pca = NULL;

	/* Default train params */
	double lr = 0.001;
	int64_t mini_size = 64;
	int64_t max_epoch = 25;
	int64_t max_drop_streak = 10;
	double frac_val = 0.1;
	int cbref = -1;

	if (k && lua_istable(L, 2) && lua_istable(L, 3)) {
		int n = rspamd_lua_table_size(L, 2);
		int n_in = kann_dim_in(k);
		int n_out = kann_dim_out(k);

		if (n_in <= 0) {
			return luaL_error(L, "invalid inputs count: %d", n_in);
		}

		if (n_out <= 0) {
			return luaL_error(L, "invalid outputs count: %d", n_out);
		}

		if (n != rspamd_lua_table_size(L, 3) || n == 0) {
			return luaL_error(L, "invalid dimensions: outputs size must be "
								 "equal to inputs and non zero");
		}

		if (lua_istable(L, 4)) {
			GError *err = NULL;

			if (!rspamd_lua_parse_table_arguments(L, 4, &err,
												  RSPAMD_LUA_PARSE_ARGUMENTS_IGNORE_MISSING,
												  "lr=N;mini_size=I;max_epoch=I;max_drop_streak=I;frac_val=N;cb=F;pca=u{tensor}",
												  &lr, &mini_size, &max_epoch, &max_drop_streak, &frac_val, &cbref, &pca)) {
				n = luaL_error(L, "invalid params: %s",
							   err ? err->message : "unknown error");
				g_error_free(err);

				return n;
			}
		}

		if (pca) {
			/* Check pca matrix validity */
			if (pca->ndims != 2) {
				return luaL_error(L, "invalid pca tensor: matrix expected, got a row");
			}

			if (pca->dim[0] != n_in) {
				return luaL_error(L, "invalid pca tensor: "
									 "matrix must have %d rows and it has %d rows instead",
								  n_in, pca->dim[0]);
			}
		}

		float **x, **y, *tmp_row = NULL;

		/* Fill vectors row by row */
		x = (float **) g_malloc0(sizeof(float *) * n);
		y = (float **) g_malloc0(sizeof(float *) * n);

		if (pca) {
			tmp_row = g_malloc(sizeof(float) * pca->dim[1]);
		}

		for (int s = 0; s < n; s++) {
			/* Inputs */
			lua_rawgeti(L, 2, s + 1);
			x[s] = (float *) g_malloc(sizeof(float) * n_in);

			if (pca == NULL) {
				if (rspamd_lua_table_size(L, -1) != n_in) {
					FREE_VEC(x, n);
					FREE_VEC(y, n);

					n = luaL_error(L, "invalid params at pos %d: "
									  "bad input dimension %d; %d expected",
								   s + 1,
								   (int) rspamd_lua_table_size(L, -1),
								   n_in);
					lua_pop(L, 1);

					return n;
				}

				for (int i = 0; i < n_in; i++) {
					lua_rawgeti(L, -1, i + 1);
					x[s][i] = lua_tonumber(L, -1);

					lua_pop(L, 1);
				}
			}
			else {
				if (rspamd_lua_table_size(L, -1) != pca->dim[1]) {
					FREE_VEC(x, n);
					FREE_VEC(y, n);
					g_free(tmp_row);

					n = luaL_error(L, "(pca on) invalid params at pos %d: "
									  "bad input dimension %d; %d expected",
								   s + 1,
								   (int) rspamd_lua_table_size(L, -1),
								   pca->dim[1]);
					lua_pop(L, 1);

					return n;
				}


				for (int i = 0; i < pca->dim[1]; i++) {
					lua_rawgeti(L, -1, i + 1);
					tmp_row[i] = lua_tonumber(L, -1);

					lua_pop(L, 1);
				}

				kad_sgemm_simple(0, 1, 1, n_in,
								 pca->dim[1], tmp_row, pca->data,
								 x[s]);
			}

			lua_pop(L, 1);

			/* Outputs */
			y[s] = (float *) g_malloc(sizeof(float) * n_out);
			lua_rawgeti(L, 3, s + 1);

			if (rspamd_lua_table_size(L, -1) != n_out) {
				FREE_VEC(x, n);
				FREE_VEC(y, n);
				g_free(tmp_row);

				n = luaL_error(L, "invalid params at pos %d: "
								  "bad output dimension %d; "
								  "%d expected",
							   s + 1,
							   (int) rspamd_lua_table_size(L, -1),
							   n_out);
				lua_pop(L, 1);

				return n;
			}

			for (int i = 0; i < n_out; i++) {
				lua_rawgeti(L, -1, i + 1);
				y[s][i] = lua_tonumber(L, -1);

				lua_pop(L, 1);
			}

			lua_pop(L, 1);
		}

		struct rspamd_kann_train_cbdata cbd;

		cbd.cbref = cbref;
		cbd.k = k;
		cbd.L = L;

		int niters = kann_train_fnn1(k, lr,
									 mini_size, max_epoch, max_drop_streak,
									 frac_val, n, x, y, lua_kann_train_cb, &cbd);

		lua_pushinteger(L, niters);

		FREE_VEC(x, n);
		FREE_VEC(y, n);
		g_free(tmp_row);
	}
	else {
		return luaL_error(L, "invalid arguments: kann, inputs, outputs and"
							 " optional params are expected");
	}

	return 1;
}

static int
lua_kann_apply1(lua_State *L)
{
	kann_t *k = lua_check_kann(L, 1);
	struct rspamd_lua_tensor *pca = NULL;

	if (k) {
		if (lua_istable(L, 2)) {
			gsize vec_len = rspamd_lua_table_size(L, 2);
			float *vec = (float *) g_malloc(sizeof(float) * vec_len),
				  *pca_out = NULL;
			int i_out;
			int n_in = kann_dim_in(k);

			if (n_in <= 0) {
				g_free(vec);
				return luaL_error(L, "invalid inputs count: %d", n_in);
			}

			if (lua_isuserdata(L, 3)) {
				pca = lua_check_tensor(L, 3);

				if (pca) {
					if (pca->ndims != 2) {
						g_free(vec);
						return luaL_error(L, "invalid pca tensor: matrix expected, got a row");
					}

					if (pca->dim[0] != n_in) {
						g_free(vec);
						return luaL_error(L, "invalid pca tensor: "
											 "matrix must have %d rows and it has %d rows instead",
										  n_in, pca->dim[0]);
					}
				}
				else {
					g_free(vec);
					return luaL_error(L, "invalid params: pca matrix expected");
				}
			}
			else {
				if (n_in != vec_len) {
					g_free(vec);
					return luaL_error(L, "invalid params: bad input dimension %d; %d expected",
									  (int) vec_len, n_in);
				}
			}

			for (gsize i = 0; i < vec_len; i++) {
				lua_rawgeti(L, 2, i + 1);
				vec[i] = lua_tonumber(L, -1);
				lua_pop(L, 1);
			}

			i_out = kann_find(k, KANN_F_OUT, 0);

			if (i_out <= 0) {
				g_free(vec);
				return luaL_error(L, "invalid ANN: output layer is missing or is "
									 "at the input pos");
			}

			kann_set_batch_size(k, 1);
			if (pca) {
				pca_out = g_malloc(sizeof(float) * n_in);

				kad_sgemm_simple(0, 1, 1, n_in,
								 vec_len, vec, pca->data,
								 pca_out);

				kann_feed_bind(k, KANN_F_IN, 0, &pca_out);
			}
			else {
				kann_feed_bind(k, KANN_F_IN, 0, &vec);
			}

			kad_eval_at(k->n, k->v, i_out);

			gsize outlen = kad_len(k->v[i_out]);
			lua_createtable(L, outlen, 0);

			for (gsize i = 0; i < outlen; i++) {
				lua_pushnumber(L, k->v[i_out]->x[i]);
				lua_rawseti(L, -2, i + 1);
			}

			g_free(vec);
			g_free(pca_out);
		}
		else if (lua_isuserdata(L, 2)) {
			struct rspamd_lua_tensor *t = lua_check_tensor(L, 2);

			if (t && t->ndims == 1) {
				int i_out;
				int n_in = kann_dim_in(k);

				if (n_in != t->dim[0]) {
					return luaL_error(L, "invalid params: bad input dimension %d; %d expected",
									  (int) t->dim[0], n_in);
				}

				i_out = kann_find(k, KANN_F_OUT, 0);

				if (i_out <= 0) {
					return luaL_error(L, "invalid ANN: output layer is missing or is "
										 "at the input pos");
				}

				kann_set_batch_size(k, 1);
				kann_feed_bind(k, KANN_F_IN, 0, &t->data);
				kad_eval_at(k->n, k->v, i_out);

				int outlen = kad_len(k->v[i_out]);
				struct rspamd_lua_tensor *out;
				out = lua_newtensor(L, 1, &outlen, false, false);
				/* Ensure that kann and tensor have the same understanding of floats */
				G_STATIC_ASSERT(sizeof(float) == sizeof(rspamd_tensor_num_t));
				memcpy(out->data, k->v[i_out]->x, outlen * sizeof(float));
			}
			else {
				return luaL_error(L, "invalid arguments: 1D rspamd{tensor} expected");
			}
		}
		else {
			return luaL_error(L, "invalid arguments: 1D rspamd{tensor} expected");
		}
	}
	else {
		return luaL_error(L, "invalid arguments: rspamd{kann} expected");
	}

	return 1;
}

/***
 * @function kann:merge_weights(other_ann, alpha)
 * Merge weights from another ANN into this one using linear interpolation.
 * w_new = (1 - alpha) * w_self + alpha * w_other
 * @param {kann} other_ann source ANN to merge from
 * @param {number} alpha weight for source ANN (0.0 - 1.0)
 * @return {boolean} true on success, false on error
 */
static int
lua_kann_merge_weights(lua_State *L)
{
	kann_t *self = lua_check_kann(L, 1);
	kann_t *other = lua_check_kann(L, 2);
	double alpha = luaL_checknumber(L, 3);

	if (self && other) {
		if (alpha < 0.0 || alpha > 1.0) {
			return luaL_error(L, "alpha must be between 0.0 and 1.0, got %f", alpha);
		}

		/* Check compatibility first */
		if (!kann_is_compatible(self, other)) {
			lua_pushboolean(L, false);
			lua_pushstring(L, "incompatible ANN architectures");
			return 2;
		}

		int ret = kann_merge_weights(self, other, (float) alpha);

		if (ret == 0) {
			lua_pushboolean(L, true);
			return 1;
		}
		else {
			lua_pushboolean(L, false);
			lua_pushstring(L, "merge failed");
			return 2;
		}
	}
	else {
		return luaL_error(L, "invalid arguments: two kann objects required");
	}
}

/***
 * @function kann:is_compatible(other_ann)
 * Check if two ANNs have compatible architecture for weight merging.
 * @param {kann} other_ann ANN to check compatibility with
 * @return {boolean} true if compatible, false otherwise
 */
static int
lua_kann_is_compatible(lua_State *L)
{
	kann_t *self = lua_check_kann(L, 1);
	kann_t *other = lua_check_kann(L, 2);

	if (self && other) {
		int ret = kann_is_compatible(self, other);
		lua_pushboolean(L, ret == 1);
		return 1;
	}
	else {
		return luaL_error(L, "invalid arguments: two kann objects required");
	}
}
