/*
 * Copyright (C) 2008 Voice System SRL
 *
 * 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
 *
 */


/*!
 * \file
 * \brief Profile related functions for the dialog module
 * \ingroup dialog
 * Module: \ref dialog
 */


#include "../../core/mem/shm_mem.h"
#include "../../core/dprint.h"
#include "../../core/ut.h"
#include "../../core/route.h"
#include "../../modules/tm/tm_load.h"
#include "../../core/trim.h"
#include "dlg_hash.h"
#include "dlg_handlers.h"
#include "dlg_profile.h"
#include "dlg_var.h"

/*! size of dialog profile hash */
#define PROFILE_HASH_SIZE 16

/*! tm bindings */
extern struct tm_binds d_tmb;

/*! global dialog message id */
static unsigned int current_dlg_msg_id = 0;
static unsigned int current_dlg_msg_pid = 0;

/*! pending dialog links */
static struct dlg_profile_link *current_pending_linkers = NULL;

/*! global dialog profile list */
static struct dlg_profile_table *profiles = NULL;


static struct dlg_profile_table *new_dlg_profile(
		str *name, unsigned int size, unsigned int has_value);

static sruid_t _dlg_profile_sruid;


/*!
 * \brief Add profile definitions to the global list
 * \see new_dlg_profile
 * \param profiles profile name
 * \param has_value set to 0 for a profile without value, otherwise it has a value
 * \return 0 on success, -1 on failure
 */
int add_profile_definitions(char *profiles, unsigned int has_value)
{
	char *p;
	char *d;
	str name;
	unsigned int i;

	if(profiles == NULL || strlen(profiles) == 0)
		return 0;

	p = profiles;
	do {
		/* locate name of profile */
		name.s = p;
		d = strchr(p, ';');
		if(d) {
			name.len = d - p;
			d++;
		} else {
			name.len = strlen(p);
		}

		/* we have the name -> trim it for spaces */
		trim_spaces_lr(name);

		/* check len name */
		if(name.len == 0)
			/* ignore */
			continue;

		/* check the name format */
		for(i = 0; i < name.len; i++) {
			if(!isalnum(name.s[i])) {
				LM_ERR("bad profile name <%.*s>, char %c - use only "
					   "alphanumerical characters\n",
						name.len, name.s, name.s[i]);
				return -1;
			}
		}

		/* name ok -> create the profile */
		LM_DBG("creating profile <%.*s>\n", name.len, name.s);

		if(new_dlg_profile(&name, PROFILE_HASH_SIZE, has_value) == NULL) {
			LM_ERR("failed to create new profile <%.*s>\n", name.len, name.s);
			return -1;
		}

	} while((p = d) != NULL);

	return 0;
}


/*!
 * \brief Search a dialog profile in the global list
 * \note Linear search, this won't have the best performance for huge profile lists
 * \param name searched dialog profile
 * \return pointer to the profile on success, NULL otherwise
 */
struct dlg_profile_table *search_dlg_profile(str *name)
{
	struct dlg_profile_table *profile;

	for(profile = profiles; profile; profile = profile->next) {
		if(name->len == profile->name.len
				&& memcmp(name->s, profile->name.s, name->len) == 0)
			return profile;
	}
	return NULL;
}


/*!
 * \brief Creates a new dialog profile
 * \see add_profile_definitions
 * \param name profile name
 * \param size profile size
 * \param has_value set to 0 for a profile without value, otherwise it has a value
 * \return pointer to the created dialog on success, NULL otherwise
 */
static struct dlg_profile_table *new_dlg_profile(
		str *name, unsigned int size, unsigned int has_value)
{
	struct dlg_profile_table *profile;
	struct dlg_profile_table *ptmp;
	unsigned int len;
	unsigned int i;

	if(name->s == NULL || name->len == 0 || size == 0) {
		LM_ERR("invalid parameters\n");
		return NULL;
	}

	for(len = 0, i = 0; i < 8 * sizeof(size); i++) {
		if(size & (1 << i))
			len++;
	}
	if(len != 1) {
		LM_ERR(" size %u is not power of 2!\n", size);
		return NULL;
	}

	profile = search_dlg_profile(name);
	if(profile != NULL) {
		LM_ERR("duplicate dialog profile registered <%.*s>\n", name->len,
				name->s);
		return NULL;
	}

	len = sizeof(struct dlg_profile_table)
		  + size * sizeof(struct dlg_profile_entry) + name->len + 1;
	profile = (struct dlg_profile_table *)shm_malloc(len);
	if(profile == NULL) {
		LM_ERR("no more shm mem\n");
		return NULL;
	}

	memset(profile, 0, len);
	profile->size = size;
	profile->has_value = (has_value == 0) ? 0 : 1;

	/* init lock */
	if(lock_init(&profile->lock) == NULL) {
		LM_ERR("failed to init lock\n");
		shm_free(profile);
		return NULL;
	}

	/* set inner pointers */
	profile->entries = (struct dlg_profile_entry *)(profile + 1);
	profile->name.s = ((char *)profile->entries)
					  + size * sizeof(struct dlg_profile_entry);

	/* copy the name of the profile */
	memcpy(profile->name.s, name->s, name->len);
	profile->name.len = name->len;
	profile->name.s[profile->name.len] = 0;

	/* link profile */
	for(ptmp = profiles; ptmp && ptmp->next; ptmp = ptmp->next)
		;
	if(ptmp == NULL)
		profiles = profile;
	else
		ptmp->next = profile;

	return profile;
}


/*!
 * \brief Destroy a dialog profile list
 * \param profile dialog profile
 */
static void destroy_dlg_profile(struct dlg_profile_table *profile)
{
	if(profile == NULL)
		return;

	lock_destroy(&profile->lock);
	shm_free(profile);
	return;
}


/*!
 * \brief Destroy the global dialog profile list
 */
void destroy_dlg_profiles(void)
{
	struct dlg_profile_table *profile;

	while(profiles) {
		profile = profiles;
		profiles = profiles->next;
		destroy_dlg_profile(profile);
	}
	return;
}


/*!
 * \brief Destroy dialog linkers
 * \param linker dialog linker
 */
void destroy_linkers(struct dlg_profile_link *linker)
{
	struct dlg_profile_entry *p_entry;
	struct dlg_profile_link *l;
	struct dlg_profile_hash *lh;

	while(linker) {
		l = linker;
		linker = linker->next;
		/* unlink from profile table */
		if(l->hash_linker.next) {
			p_entry = &l->profile->entries[l->hash_linker.hash];
			lock_get(&l->profile->lock);
			lh = &l->hash_linker;
			/* last element on the list? */
			if(lh == lh->next) {
				p_entry->first = NULL;
			} else {
				if(p_entry->first == lh)
					p_entry->first = lh->next;
				lh->next->prev = lh->prev;
				lh->prev->next = lh->next;
			}
			lh->next = lh->prev = NULL;
			p_entry->content--;
			lock_release(&l->profile->lock);
		}
		/* free memory */
		shm_free(l);
	}
}


/*!
 * \brief Cleanup a profile
 * \param msg SIP message
 * \param flags unused
 * \param param unused
 * \return 1
 */
int profile_cleanup(struct sip_msg *msg, unsigned int flags, void *param)
{
	dlg_cell_t *dlg;

	current_dlg_msg_id = 0;
	current_dlg_msg_pid = 0;
	dlg = dlg_get_ctx_dialog();
	if(dlg != NULL) {
		if(dlg->dflags & DLG_FLAG_TM) {
			unref_dlg(dlg, 1);
		} else {
			/* dialog didn't make it to tm */
			unref_dlg(dlg, 2);
		}
	}
	if(current_pending_linkers) {
		destroy_linkers(current_pending_linkers);
		current_pending_linkers = NULL;
	}

	/* need to return non-zero - 0 will break the exec of the request */
	return 1;
}


struct dlg_cell *get_dialog_from_tm(struct cell *t)
{
	if(t == NULL || t == T_UNDEFINED)
		return NULL;

	struct tm_callback *x = (struct tm_callback *)(t->tmcb_hl.first);

	while(x) {
		membar_depends();
		if(x->types == TMCB_MAX && x->callback == dlg_tmcb_dummy) {
			return (struct dlg_cell *)(x->param);
		}
		x = x->next;
	}

	return NULL;
}

/*!
 * \brief Calculate the hash profile from a dialog
 * \see core_hash
 * \param value hash source
 * \param dlg dialog cell
 * \param profile dialog profile table (for hash size)
 * \return value hash if the value has a value, hash over dialog otherwise
 */
inline static unsigned int calc_hash_profile(
		str *value, struct dlg_cell *dlg, struct dlg_profile_table *profile)
{
	if(profile->has_value) {
		/* do hash over the value */
		return core_hash(value, NULL, profile->size);
	} else {
		/* do hash over dialog pointer */
		return ((unsigned long)dlg) % profile->size;
	}
}


/*!
 * \brief Link a dialog profile
 * \param linker dialog linker
 * \param dlg dialog cell
 */
static void link_dlg_profile(
		struct dlg_profile_link *linker, struct dlg_cell *dlg)
{
	unsigned int hash;
	struct dlg_profile_entry *p_entry;
	struct dlg_entry *d_entry;

	/* add the linker to the dialog */
	/* FIXME zero h_id is not 100% for testing if the dialog is inserted
	 * into the hash table -> we need circular lists  -bogdan */
	if(dlg->h_id) {
		d_entry = &d_table->entries[dlg->h_entry];
		dlg_lock(d_table, d_entry);
		linker->next = dlg->profile_links;
		dlg->profile_links = linker;
		linker->hash_linker.dlg = dlg;
		dlg_unlock(d_table, d_entry);
	} else {
		linker->next = dlg->profile_links;
		dlg->profile_links = linker;
		linker->hash_linker.dlg = dlg;
	}

	/* calculate the hash position */
	hash = calc_hash_profile(&linker->hash_linker.value, dlg, linker->profile);
	linker->hash_linker.hash = hash;

	/* insert into profile hash table */
	p_entry = &linker->profile->entries[hash];
	lock_get(&linker->profile->lock);
	if(p_entry->first) {
		linker->hash_linker.prev = p_entry->first->prev;
		linker->hash_linker.next = p_entry->first;
		p_entry->first->prev->next = &linker->hash_linker;
		p_entry->first->prev = &linker->hash_linker;
	} else {
		p_entry->first = linker->hash_linker.next = linker->hash_linker.prev =
				&linker->hash_linker;
	}
	p_entry->content++;
	lock_release(&linker->profile->lock);
}


/*!
 * \brief Set the global variables to the current dialog
 * \param msg SIP message
 * \param dlg dialog cell
 */
void set_current_dialog(sip_msg_t *msg, dlg_cell_t *dlg)
{
	struct dlg_profile_link *linker;
	struct dlg_profile_link *tlinker;

	LM_DBG("setting current dialog [%u:%u]\n", dlg->h_entry, dlg->h_id);
	/* if linkers are not from current request, just discard them */
	if(msg->id != current_dlg_msg_id || msg->pid != current_dlg_msg_pid) {
		current_dlg_msg_id = msg->id;
		current_dlg_msg_pid = msg->pid;
		destroy_linkers(current_pending_linkers);
	} else {
		/* add the linker, one by one, to the dialog */
		linker = current_pending_linkers;
		while(linker) {
			tlinker = linker;
			linker = linker->next;
			/* process tlinker */
			tlinker->next = NULL;
			link_dlg_profile(tlinker, dlg);
		}
	}
	current_pending_linkers = NULL;
}


/*!
 * \brief Set a dialog profile
 * \param msg SIP message
 * \param value value
 * \param profile dialog profile table
 * \return 0 on success, -1 on failure
 */
int set_dlg_profile(
		struct sip_msg *msg, str *value, struct dlg_profile_table *profile)
{
	dlg_cell_t *dlg = NULL;
	dlg_profile_link_t *linker;

	/* get current dialog */
	dlg = dlg_get_msg_dialog(msg);

	if(dlg == NULL && !is_route_type(REQUEST_ROUTE)) {
		LM_CRIT("BUG - dialog not found in a non REQUEST route (%d)\n",
				REQUEST_ROUTE);
		return -1;
	}

	/* build new linker */
	linker = (struct dlg_profile_link *)shm_malloc(
			sizeof(struct dlg_profile_link)
			+ (profile->has_value ? value->len : 0));
	if(linker == NULL) {
		LM_ERR("no more shm memory\n");
		goto error;
	}
	memset(linker, 0, sizeof(struct dlg_profile_link));

	/* set backpointers to profile and linker (itself) */
	linker->profile = profile;
	linker->hash_linker.linker = linker;

	/* set the value */
	if(profile->has_value) {
		linker->hash_linker.value.s = (char *)(linker + 1);
		memcpy(linker->hash_linker.value.s, value->s, value->len);
		linker->hash_linker.value.len = value->len;
	}
	sruid_next_safe(&_dlg_profile_sruid);
	strcpy(linker->hash_linker.puid, _dlg_profile_sruid.uid.s);
	linker->hash_linker.puid_len = _dlg_profile_sruid.uid.len;

	if(dlg != NULL) {
		/* add linker directly to the dialog and profile */
		link_dlg_profile(linker, dlg);
	} else {
		/* if existing linkers are not from current request, just discard them */
		if(msg->id != current_dlg_msg_id || msg->pid != current_dlg_msg_pid) {
			current_dlg_msg_id = msg->id;
			current_dlg_msg_pid = msg->pid;
			destroy_linkers(current_pending_linkers);
			current_pending_linkers = NULL;
		}
		/* no dialog yet -> set linker as pending */
		if(msg->id != current_dlg_msg_id || msg->pid != current_dlg_msg_pid) {
			current_dlg_msg_id = msg->id;
			current_dlg_msg_pid = msg->pid;
			destroy_linkers(current_pending_linkers);
		}

		linker->next = current_pending_linkers;
		current_pending_linkers = linker;
	}

	dlg_release(dlg);
	return 0;
error:
	dlg_release(dlg);
	return -1;
}


/*!
 * \brief Unset a dialog profile
 * \param msg SIP message
 * \param value value
 * \param profile dialog profile table
 * \return 1 on success, -1 on failure
 */
int unset_dlg_profile(
		struct sip_msg *msg, str *value, struct dlg_profile_table *profile)
{
	dlg_cell_t *dlg;
	dlg_profile_link_t *linker;
	dlg_profile_link_t *linker_prev;
	dlg_entry_t *d_entry;

	if(is_route_type(REQUEST_ROUTE)) {
		LM_ERR("dialog delete profile cannot be used in request route\n");
		return -1;
	}

	/* get current dialog */
	dlg = dlg_get_msg_dialog(msg);

	if(dlg == NULL) {
		LM_WARN("dialog is NULL for delete profile\n");
		return -1;
	}

	/* check the dialog linkers */
	d_entry = &d_table->entries[dlg->h_entry];
	dlg_lock(d_table, d_entry);
	linker = dlg->profile_links;
	linker_prev = NULL;
	for(; linker; linker_prev = linker, linker = linker->next) {
		if(linker->profile == profile) {
			if(profile->has_value == 0) {
				goto found;
			} else if(value && value->len == linker->hash_linker.value.len
					  && memcmp(value->s, linker->hash_linker.value.s,
								 value->len)
								 == 0) {
				goto found;
			}
			/* allow further search - maybe the dialog is inserted twice in
			 * the same profile, but with different values -bogdan
			 */
		}
	}
	dlg_unlock(d_table, d_entry);
	dlg_release(dlg);
	return -1;

found:
	/* table still locked */
	/* remove the linker element from dialog */
	if(linker_prev == NULL) {
		dlg->profile_links = linker->next;
	} else {
		linker_prev->next = linker->next;
	}
	linker->next = NULL;
	dlg_unlock(d_table, d_entry);
	/* remove linker from profile table and free it */
	destroy_linkers(linker);
	dlg_release(dlg);
	return 1;
}


/*!
 * \brief Check if a dialog belongs to a profile
 * \param msg SIP message
 * \param profile dialog profile table
 * \param value value
 * \return 1 on success, -1 on failure
 */
int is_dlg_in_profile(
		struct sip_msg *msg, struct dlg_profile_table *profile, str *value)
{
	struct dlg_cell *dlg;
	struct dlg_profile_link *linker;
	struct dlg_entry *d_entry;
	int ret;

	LM_DBG("Getting current dialog");
	/* get current dialog */
	dlg = dlg_get_msg_dialog(msg);

	if(dlg == NULL) {
		LM_DBG("Error: Current dlg is null");

		return -1;
	}
	LM_DBG("Current dlg found");

	ret = -1;
	/* check the dialog linkers */
	d_entry = &d_table->entries[dlg->h_entry];
	dlg_lock(d_table, d_entry);
	for(linker = dlg->profile_links; linker; linker = linker->next) {
		LM_DBG("Running through linkers");
		if(linker->profile == profile) {
			LM_DBG("Profile matches");
			if(profile->has_value == 0) {
				LM_DBG("Profile has value is zero returning true");
				dlg_unlock(d_table, d_entry);
				ret = 1;
				goto done;
			} else if(value && value->len == linker->hash_linker.value.len
					  && memcmp(value->s, linker->hash_linker.value.s,
								 value->len)
								 == 0) {
				LM_DBG("Profile has value equal to passed value returning "
					   "true");
				dlg_unlock(d_table, d_entry);
				ret = 1;
				goto done;
			}
			/* allow further search - maybe the dialog is inserted twice in
			 * the same profile, but with different values -bogdan
			 */
		}
	}
	dlg_unlock(d_table, d_entry);

done:
	dlg_release(dlg);
	return ret;
}


/*!
 * \brief Get the size of a profile
 * \param profile evaluated profile
 * \param value value
 * \return the profile size
 */
unsigned int get_profile_size(struct dlg_profile_table *profile, str *value)
{
	unsigned int n, i;
	struct dlg_profile_hash *ph;

	if(profile->has_value == 0 || value == NULL) {
		/* iterate through the hash and count all records */
		lock_get(&profile->lock);
		for(i = 0, n = 0; i < profile->size; i++)
			n += profile->entries[i].content;
		lock_release(&profile->lock);
		return n;
	} else {
		/* iterate through the hash entry and count only matching */
		/* calculate the hash position */
		i = calc_hash_profile(value, NULL, profile);
		n = 0;
		lock_get(&profile->lock);
		ph = profile->entries[i].first;
		if(ph) {
			do {
				/* compare */
				if(value->len == ph->value.len
						&& memcmp(value->s, ph->value.s, value->len) == 0) {
					/* found */
					n++;
				}
				/* next */
				ph = ph->next;
			} while(ph != profile->entries[i].first);
		}
		lock_release(&profile->lock);
		return n;
	}
}

/*
 * Determine if message is in a dialog currently being tracked
 */
int is_known_dlg(struct sip_msg *msg)
{
	dlg_cell_t *dlg;

	dlg = dlg_get_msg_dialog(msg);

	if(dlg == NULL)
		return -1;

	dlg_release(dlg);

	return 1;
}

#ifdef MI_REMOVED
/****************************** MI commands *********************************/

/*!
 * \brief Output a profile via MI interface
 * \param cmd_tree MI command tree
 * \param param unused
 * \return MI root output on success, NULL on failure
 */
struct mi_root *mi_get_profile(struct mi_root *cmd_tree, void *param)
{
	struct mi_node *node;
	struct mi_root *rpl_tree = NULL;
	struct mi_node *rpl = NULL;
	struct mi_attr *attr;
	struct dlg_profile_table *profile;
	str *value;
	str *profile_name;
	unsigned int size;
	int len;
	char *p;

	node = cmd_tree->node.kids;
	if(node == NULL || !node->value.s || !node->value.len)
		return init_mi_tree(400, MI_SSTR(MI_MISSING_PARM));
	profile_name = &node->value;

	if(node->next) {
		node = node->next;
		if(!node->value.s || !node->value.len)
			return init_mi_tree(400, MI_SSTR(MI_BAD_PARM));
		if(node->next)
			return init_mi_tree(400, MI_SSTR(MI_MISSING_PARM));
		value = &node->value;
	} else {
		value = NULL;
	}

	/* search for the profile */
	profile = search_dlg_profile(profile_name);
	if(profile == NULL)
		return init_mi_tree(404, MI_SSTR("Profile not found"));

	size = get_profile_size(profile, value);

	rpl_tree = init_mi_tree(200, MI_SSTR(MI_OK));
	if(rpl_tree == 0)
		return 0;
	rpl = &rpl_tree->node;

	node = add_mi_node_child(rpl, MI_DUP_VALUE, "profile", 7, NULL, 0);
	if(node == 0) {
		free_mi_tree(rpl_tree);
		return NULL;
	}

	attr = add_mi_attr(
			node, MI_DUP_VALUE, "name", 4, profile->name.s, profile->name.len);
	if(attr == NULL) {
		goto error;
	}

	if(value) {
		attr = add_mi_attr(
				node, MI_DUP_VALUE, "value", 5, value->s, value->len);
	} else {
		attr = add_mi_attr(node, MI_DUP_VALUE, "value", 5, NULL, 0);
	}
	if(attr == NULL) {
		goto error;
	}

	p = int2str((unsigned long)size, &len);
	attr = add_mi_attr(node, MI_DUP_VALUE, "count", 5, p, len);
	if(attr == NULL) {
		goto error;
	}

	return rpl_tree;
error:
	free_mi_tree(rpl_tree);
	return NULL;
}


/*!
 * \brief List the profiles via MI interface
 * \param cmd_tree MI command tree
 * \param param unused
 * \return MI root output on success, NULL on failure
 */
struct mi_root *mi_profile_list(struct mi_root *cmd_tree, void *param)
{
	struct mi_node *node;
	struct mi_root *rpl_tree = NULL;
	struct mi_node *rpl = NULL;
	struct dlg_profile_table *profile;
	struct dlg_profile_hash *ph;
	str *profile_name;
	str *value;
	unsigned int i;

	node = cmd_tree->node.kids;
	if(node == NULL || !node->value.s || !node->value.len)
		return init_mi_tree(400, MI_SSTR(MI_MISSING_PARM));
	profile_name = &node->value;

	if(node->next) {
		node = node->next;
		if(!node->value.s || !node->value.len)
			return init_mi_tree(400, MI_SSTR(MI_BAD_PARM));
		if(node->next)
			return init_mi_tree(400, MI_SSTR(MI_MISSING_PARM));
		value = &node->value;
	} else {
		value = NULL;
	}

	/* search for the profile */
	profile = search_dlg_profile(profile_name);
	if(profile == NULL)
		return init_mi_tree(404, MI_SSTR("Profile not found"));

	rpl_tree = init_mi_tree(200, MI_SSTR(MI_OK));
	if(rpl_tree == 0)
		return 0;
	rpl = &rpl_tree->node;

	/* go through the hash and print the dialogs */
	if(profile->has_value == 0 || value == NULL) {
		/* no value */
		lock_get(&profile->lock);
		for(i = 0; i < profile->size; i++) {
			ph = profile->entries[i].first;
			if(ph) {
				do {
					/* print dialog */
					if(mi_print_dlg(rpl, ph->dlg, 0) != 0)
						goto error;
					/* next */
					ph = ph->next;
				} while(ph != profile->entries[i].first);
			}
			lock_release(&profile->lock);
		}
	} else {
		/* check for value also */
		lock_get(&profile->lock);
		for(i = 0; i < profile->size; i++) {
			ph = profile->entries[i].first;
			if(ph) {
				do {
					if(value->len == ph->value.len
							&& memcmp(value->s, ph->value.s, value->len) == 0) {
						/* print dialog */
						if(mi_print_dlg(rpl, ph->dlg, 0) != 0)
							goto error;
					}
					/* next */
					ph = ph->next;
				} while(ph != profile->entries[i].first);
			}
			lock_release(&profile->lock);
		}
	}

	return rpl_tree;
error:
	free_mi_tree(rpl_tree);
	return NULL;
}
#endif
