/*
 * Copyright (C) 2007 1&1 Internet AG
 *
 * This file is part of Kamailio, a free SIP server.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * Kamailio is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version
 *
 * Kamailio is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <string.h>

#include "../../core/mem/shm_mem.h"
#include "../../core/sr_module.h"
#include "../../core/mem/mem.h"
#include "../../core/usr_avp.h"
#include "../../core/locking.h"
#include "../../core/error.h"
#include "../../core/ut.h"
#include "../../core/mod_fix.h"
#include "../../core/rpc_lookup.h"


#include "db_matrix.h"

MODULE_VERSION


#define MAXCOLS 1000


str matrix_db_url = str_init(DEFAULT_RODB_URL);


/**
 * Generic parameter that holds a string, an int or a pseudo-variable
 * @todo replace this with gparam_t
 */
struct multiparam_t
{
	enum
	{
		MP_INT,
		MP_STR,
		MP_AVP,
		MP_PVE,
	} type;
	union
	{
		int n;
		str s;
		struct
		{
			avp_flags_t flags;
			avp_name_t name;
		} a;
		pv_elem_t *p;
	} u;
};


int matrix_rpc_init(void);

/* ---- fixup functions: */
static int matrix_fixup(void **param, int param_no);

/* ---- exported commands: */
static int lookup_matrix(struct sip_msg *msg, struct multiparam_t *_first,
		struct multiparam_t *_second, struct multiparam_t *_dstavp);

/* ---- module init functions: */
static int mod_init(void);
static int child_init(int rank);
static void mod_destroy(void);

/* clang-format off */
static cmd_export_t cmds[] = {
		{"matrix", (cmd_function)lookup_matrix, 3,
			matrix_fixup, 0, REQUEST_ROUTE | FAILURE_ROUTE},
		{0, 0, 0, 0, 0, 0}
	};

static param_export_t params[] = {
	matrix_DB_URL
	matrix_DB_TABLE
	matrix_DB_COLS
	{0, 0, 0}
};

struct module_exports exports = {
	"matrix",           /* module name */
	DEFAULT_DLFLAGS,    /* dlopen flags */
	cmds,               /* exported functions */
	params,             /* exported parameters */
	0,                  /* RPC method exports */
	0,                  /* exported pseudo-variables */
	0,                  /* response handling function */
	mod_init,           /* module initialization function */
	child_init,         /* per-child init function */
	mod_destroy         /* module destroy function */
};
/* clang-format on */


struct first_t
{
	struct first_t *next;
	int id;
	short int second_list[MAXCOLS + 1];
};


struct matrix_t
{
	struct first_t *head;
};


static gen_lock_t *lock = NULL;
static struct matrix_t *matrix = NULL;


/**
 * fixes the module functions' parameters if it is a phone number.
 * supports string, pseudo-variables and AVPs.
 *
 * @param param the parameter
 *
 * @return 0 on success, -1 on failure
 */
static int mp_fixup(void **param)
{
	pv_spec_t avp_spec;
	struct multiparam_t *mp;
	str s;

	mp = (struct multiparam_t *)pkg_malloc(sizeof(struct multiparam_t));
	if(mp == NULL) {
		LM_ERR("out of pkg memory\n");
		return -1;
	}
	memset(mp, 0, sizeof(struct multiparam_t));

	s.s = (char *)(*param);
	s.len = strlen(s.s);

	if(s.s[0] != '$') {
		/* This is string */
		mp->type = MP_STR;
		mp->u.s = s;
	} else {
		/* This is a pseudo-variable */
		if(pv_parse_spec(&s, &avp_spec) == 0) {
			LM_ERR("pv_parse_spec failed for '%s'\n", (char *)(*param));
			pkg_free(mp);
			return -1;
		}
		if(avp_spec.type == PVT_AVP) {
			/* This is an AVP - could be an id or name */
			mp->type = MP_AVP;
			if(pv_get_avp_name(
					   0, &(avp_spec.pvp), &(mp->u.a.name), &(mp->u.a.flags))
					!= 0) {
				LM_ERR("Invalid AVP definition <%s>\n", (char *)(*param));
				pkg_free(mp);
				return -1;
			}
		} else {
			mp->type = MP_PVE;
			if(pv_parse_format(&s, &(mp->u.p)) < 0) {
				LM_ERR("pv_parse_format failed for '%s'\n", (char *)(*param));
				pkg_free(mp);
				return -1;
			}
		}
	}
	*param = (void *)mp;

	return 0;
}


/**
 * fixes the module functions' parameters in case of AVP names.
 *
 * @param param the parameter
 *
 * @return 0 on success, -1 on failure
 */
static int avp_name_fixup(void **param)
{
	pv_spec_t avp_spec;
	struct multiparam_t *mp;
	str s;

	s.s = (char *)(*param);
	s.len = strlen(s.s);
	if(s.len <= 0)
		return -1;
	if(pv_parse_spec(&s, &avp_spec) == 0 || avp_spec.type != PVT_AVP) {
		LM_ERR("Malformed or non AVP definition <%s>\n", (char *)(*param));
		return -1;
	}

	mp = (struct multiparam_t *)pkg_malloc(sizeof(struct multiparam_t));
	if(mp == NULL) {
		LM_ERR("out of pkg memory\n");
		return -1;
	}
	memset(mp, 0, sizeof(struct multiparam_t));

	mp->type = MP_AVP;
	if(pv_get_avp_name(0, &(avp_spec.pvp), &(mp->u.a.name), &(mp->u.a.flags))
			!= 0) {
		LM_ERR("Invalid AVP definition <%s>\n", (char *)(*param));
		pkg_free(mp);
		return -1;
	}

	*param = (void *)mp;

	return 0;
}


static int matrix_fixup(void **param, int param_no)
{
	if(param_no == 1) {
		/* source id */
		if(mp_fixup(param) < 0) {
			LM_ERR("cannot fixup parameter %d\n", param_no);
			return -1;
		}
	} else if(param_no == 2) {
		/* destination id */
		if(mp_fixup(param) < 0) {
			LM_ERR("cannot fixup parameter %d\n", param_no);
			return -1;
		}
	} else if(param_no == 3) {
		/* destination avp name */
		if(avp_name_fixup(param) < 0) {
			LM_ERR("cannot fixup parameter %d\n", param_no);
			return -1;
		}
	}

	return 0;
}


static void matrix_clear(void)
{
	struct first_t *srcitem;
	if(matrix) {
		while(matrix->head) {
			srcitem = matrix->head;
			matrix->head = srcitem->next;
			shm_free(srcitem);
		}
	}
}


static int matrix_insert(int first, short int second, int res)
{
	struct first_t *srcitem;
	int i;

	if((second < 0) || (second > MAXCOLS)) {
		LM_ERR("invalid second value %d\n", second);
		return -1;
	}
	LM_DBG("searching for %d, %d\n", first, second);
	if(matrix) {
		srcitem = matrix->head;
		while(srcitem) {
			if(srcitem->id == first) {
				srcitem->second_list[second] = res;
				LM_DBG("inserted (%d, %d, %d)\n", first, second, res);
				return 0;
			}
			srcitem = srcitem->next;
		}
		/* not found */
		srcitem = shm_malloc(sizeof(struct first_t));
		if(srcitem == NULL) {
			LM_ERR("out of shared memory.\n");
			return -1;
		}
		memset(srcitem, 0, sizeof(struct first_t));

		/* Mark all new cells as empty */
		for(i = 0; i <= MAXCOLS; i++)
			srcitem->second_list[i] = -1;

		srcitem->next = matrix->head;
		srcitem->id = first;
		srcitem->second_list[second] = res;
		matrix->head = srcitem;
	}

	LM_DBG("inserted new row for (%d, %d, %d)\n", first, second, res);
	return 0;
}


/* Returns the res id if the matrix contains an entry for the given indices, -1 otherwise.
*/
static int internal_lookup(int first, short int second)
{
	struct first_t *item;

	if((second < 0) || (second > MAXCOLS)) {
		LM_ERR("invalid second value %d\n", second);
		return -1;
	}

	if(matrix) {
		item = matrix->head;
		while(item) {
			if(item->id == first) {
				return item->second_list[second];
			}
			item = item->next;
		}
	}

	return -1;
}


static int lookup_matrix(struct sip_msg *msg, struct multiparam_t *_srctree,
		struct multiparam_t *_second, struct multiparam_t *_dstavp)
{
	int first;
	int second;
	struct usr_avp *avp;
	int_str avp_val;

	switch(_srctree->type) {
		case MP_INT:
			first = _srctree->u.n;
			break;
		case MP_AVP:
			avp = search_first_avp(
					_srctree->u.a.flags, _srctree->u.a.name, &avp_val, 0);
			if(!avp) {
				LM_ERR("cannot find srctree AVP\n");
				return -1;
			}
			if((avp->flags & AVP_VAL_STR)) {
				LM_ERR("cannot process string value in srctree AVP\n");
				return -1;
			} else
				first = avp_val.n;
			break;
		default:
			LM_ERR("invalid srctree type\n");
			return -1;
	}

	switch(_second->type) {
		case MP_INT:
			second = _second->u.n;
			break;
		case MP_AVP:
			avp = search_first_avp(
					_second->u.a.flags, _second->u.a.name, &avp_val, 0);
			if(!avp) {
				LM_ERR("cannot find second_value AVP\n");
				return -1;
			}
			if((avp->flags & AVP_VAL_STR)) {
				LM_ERR("cannot process string value in second_value AVP\n");
				return -1;
			} else
				second = avp_val.n;
			break;
		default:
			LM_ERR("invalid second_value type\n");
			return -1;
	}


	/* critical section start: avoids dirty reads when updating d-tree */
	lock_get(lock);

	avp_val.n = internal_lookup(first, second);

	/* critical section end */
	lock_release(lock);

	if(avp_val.n < 0) {
		LM_INFO("lookup failed\n");
		return -1;
	}

	/* set avp ! */
	if(add_avp(_dstavp->u.a.flags, _dstavp->u.a.name, avp_val) < 0) {
		LM_ERR("add AVP failed\n");
		return -1;
	}
	LM_INFO("result from lookup: %ld\n", avp_val.n);
	return 1;
}


/**
 * Rebuild matrix using database entries
 * \return negative on failure, positive on success, indicating the number of matrix entries
 */
static int db_reload_matrix(void)
{
	db_key_t columns[3] = {
			&matrix_first_col, &matrix_second_col, &matrix_res_col};
	db1_res_t *res;
	int i;
	int n = 0;

	if(matrix_dbf.use_table(matrix_dbh, &matrix_table) < 0) {
		LM_ERR("cannot use table '%.*s'.\n", matrix_table.len, matrix_table.s);
		return -1;
	}
	if(matrix_dbf.query(matrix_dbh, NULL, NULL, NULL, columns, 0, 3, NULL, &res)
			< 0) {
		LM_ERR("error while executing query.\n");
		return -1;
	}

	/* critical section start: avoids dirty reads when updating d-tree */
	lock_get(lock);

	matrix_clear();

	if(RES_COL_N(res) > 2) {
		for(i = 0; i < RES_ROW_N(res); i++) {
			if((!RES_ROWS(res)[i].values[0].nul)
					&& (!RES_ROWS(res)[i].values[1].nul)) {
				if((RES_ROWS(res)[i].values[0].type == DB1_INT)
						&& (RES_ROWS(res)[i].values[1].type == DB1_INT)
						&& (RES_ROWS(res)[i].values[2].type == DB1_INT)) {
					matrix_insert(RES_ROWS(res)[i].values[0].val.int_val,
							RES_ROWS(res)[i].values[1].val.int_val,
							RES_ROWS(res)[i].values[2].val.int_val);
					n++;
				} else {
					LM_ERR("got invalid result type from query.\n");
				}
			}
		}
	}

	/* critical section end */
	lock_release(lock);

	matrix_dbf.free_result(matrix_dbh, res);

	LM_INFO("loaded %d matrix entries.\n", n);
	return n;
}


static int init_shmlock(void)
{
	lock = lock_alloc();
	if(!lock) {
		LM_CRIT("cannot allocate memory for lock.\n");
		return -1;
	}
	if(lock_init(lock) == 0) {
		LM_CRIT("cannot initialize lock.\n");
		return -1;
	}

	return 0;
}


static void destroy_shmlock(void)
{
	if(lock) {
		lock_destroy(lock);
		lock_dealloc((void *)lock);
		lock = NULL;
	}
}


static void matrix_rpc_reload(rpc_t *rpc, void *c)
{
	if(matrix_db_open() != 0) {
		rpc->fault(c, 500, "Failed to connect to db");
		return;
	}
	if(db_reload_matrix() < 0) {
		rpc->fault(c, 500, "Reload failed");
	}
	rpc->rpl_printf(c, "Ok. Reload successful.");
	matrix_db_close();
}

static const char *matrix_rpc_reload_doc[2] = {
		"Reload matrix records from database", 0};


rpc_export_t matrix_rpc_cmds[] = {
		{"matrix.reload", matrix_rpc_reload, matrix_rpc_reload_doc, 0},
		{0, 0, 0, 0}};

int matrix_rpc_init(void)
{
	if(rpc_register_array(matrix_rpc_cmds) != 0) {
		LM_ERR("failed to register RPC commands\n");
		return -1;
	}
	return 0;
}

static int init_matrix(void)
{
	matrix = shm_malloc(sizeof(struct matrix_t));
	if(!matrix) {
		LM_ERR("out of shared memory\n");
		return -1;
	}
	memset(matrix, 0, sizeof(struct matrix_t));
	if(db_reload_matrix() < 0) {
		LM_ERR("cannot populate matrix\n");
		return -1;
	}

	return 0;
}


static void destroy_matrix(void)
{
	if(matrix) {
		matrix_clear();
		shm_free(matrix);
	}
}


static int mod_init(void)
{
	if(matrix_rpc_init() < 0) {
		LM_ERR("failed to init RPC commands");
		return -1;
	}

	if(init_shmlock() != 0)
		return -1;
	if(matrix_db_init() != 0)
		return -1;
	if(matrix_db_open() != 0)
		return -1;
	if(init_matrix() != 0)
		return -1;
	matrix_db_close();
	return 0;
}


static int child_init(int rank)
{
	if(rank == PROC_INIT || rank == PROC_TCP_MAIN)
		return 0;
	if(matrix_db_open() != 0)
		return -1;
	return 0;
}


static void mod_destroy(void)
{
	destroy_matrix();
	destroy_shmlock();
	matrix_db_close();
}
