/*
 * Asterisk -- An open source telephony toolkit.
 *
 * Copyright (C) 2023, Commend International
 *
 * Maximilian Fridrich <m.fridrich@commend.com>
 *
 * See http://www.asterisk.org for more information about
 * the Asterisk project. Please do not directly contact
 * any of the maintainers of this project for assistance;
 * the project provides a web site, mailing lists and IRC
 * channels for your use.
 *
 * This program is free software, distributed under the terms of
 * the GNU General Public License Version 2. See the LICENSE file
 * at the top of the source tree.
 */

/*! \file
 *
 * \brief Out-of-call refer support
 *
 * \author Maximilian Fridrich <m.fridrich@commend.com>
 */

/*** MODULEINFO
	<support_level>core</support_level>
 ***/

#include "asterisk.h"

#include "asterisk/_private.h"

#include "asterisk/module.h"
#include "asterisk/datastore.h"
#include "asterisk/pbx.h"
#include "asterisk/manager.h"
#include "asterisk/stasis_bridges.h"
#include "asterisk/stasis_channels.h"
#include "asterisk/strings.h"
#include "asterisk/astobj2.h"
#include "asterisk/vector.h"
#include "asterisk/app.h"
#include "asterisk/taskprocessor.h"
#include "asterisk/refer.h"

struct refer_data {
	/* Stored in stuff[] at struct end */
	char *name;
	/* Stored separately */
	char *value;
	/* Holds name */
	char stuff[0];
};

/*!
 * \brief A refer.
 */
struct ast_refer {
	AST_DECLARE_STRING_FIELDS(
		/*! Where the refer is going */
		AST_STRING_FIELD(to);
		/*! Where we "say" the refer came from */
		AST_STRING_FIELD(from);
		/*! Where to refer to */
		AST_STRING_FIELD(refer_to);
		/*! An endpoint associated with this refer */
		AST_STRING_FIELD(endpoint);
		/*! The technology of the endpoint associated with this refer */
		AST_STRING_FIELD(tech);
	);
	/* Whether to refer to Asterisk itself, if refer_to is an Asterisk endpoint. */
	int to_self;
	/*! Technology/dialplan specific variables associated with the refer */
	struct ao2_container *vars;
};

/*! \brief Lock for \c refer_techs vector */
static ast_rwlock_t refer_techs_lock;

/*! \brief Vector of refer technologies */
AST_VECTOR(, const struct ast_refer_tech *) refer_techs;

static int refer_data_cmp_fn(void *obj, void *arg, int flags)
{
	const struct refer_data *object_left = obj;
	const struct refer_data *object_right = arg;
	const char *right_key = arg;
	int cmp;

	switch (flags & OBJ_SEARCH_MASK) {
	case OBJ_SEARCH_OBJECT:
		right_key = object_right->name;
	case OBJ_SEARCH_KEY:
		cmp = strcasecmp(object_left->name, right_key);
		break;
	case OBJ_SEARCH_PARTIAL_KEY:
		cmp = strncasecmp(object_left->name, right_key, strlen(right_key));
		break;
	default:
		cmp = 0;
		break;
	}
	if (cmp) {
		return 0;
	}
	return CMP_MATCH;
}

static void refer_data_destructor(void *obj)
{
	struct refer_data *data = obj;
	ast_free(data->value);
}

static void refer_destructor(void *obj)
{
	struct ast_refer *refer = obj;

	ast_string_field_free_memory(refer);
	ao2_cleanup(refer->vars);
}

struct ast_refer *ast_refer_alloc(void)
{
	struct ast_refer *refer;

	if (!(refer = ao2_alloc_options(sizeof(*refer), refer_destructor, AO2_ALLOC_OPT_LOCK_NOLOCK))) {
		return NULL;
	}

	if (ast_string_field_init(refer, 128)) {
		ao2_ref(refer, -1);
		return NULL;
	}

	refer->vars = ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_MUTEX, 0,
		NULL, refer_data_cmp_fn);
	if (!refer->vars) {
		ao2_ref(refer, -1);
		return NULL;
	}
	refer->to_self = 0;

	return refer;
}

struct ast_refer *ast_refer_ref(struct ast_refer *refer)
{
	ao2_ref(refer, 1);
	return refer;
}

struct ast_refer *ast_refer_destroy(struct ast_refer *refer)
{
	ao2_ref(refer, -1);
	return NULL;
}

int ast_refer_set_to(struct ast_refer *refer, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	ast_string_field_build_va(refer, to, fmt, ap);
	va_end(ap);

	return 0;
}

int ast_refer_set_from(struct ast_refer *refer, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	ast_string_field_build_va(refer, from, fmt, ap);
	va_end(ap);

	return 0;
}

int ast_refer_set_refer_to(struct ast_refer *refer, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	ast_string_field_build_va(refer, refer_to, fmt, ap);
	va_end(ap);

	return 0;
}

int ast_refer_set_to_self(struct ast_refer *refer, int val)
{
	refer->to_self = val;
	return 0;
}

int ast_refer_set_tech(struct ast_refer *refer, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	ast_string_field_build_va(refer, tech, fmt, ap);
	va_end(ap);

	return 0;
}

int ast_refer_set_endpoint(struct ast_refer *refer, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	ast_string_field_build_va(refer, endpoint, fmt, ap);
	va_end(ap);

	return 0;
}

const char *ast_refer_get_refer_to(const struct ast_refer *refer)
{
	return refer->refer_to;
}

const char *ast_refer_get_from(const struct ast_refer *refer)
{
	return refer->from;
}

const char *ast_refer_get_to(const struct ast_refer *refer)
{
	return refer->to;
}

int ast_refer_get_to_self(const struct ast_refer *refer)
{
	return refer->to_self;
}

const char *ast_refer_get_tech(const struct ast_refer *refer)
{
	return refer->tech;
}

const char *ast_refer_get_endpoint(const struct ast_refer *refer)
{
	return refer->endpoint;
}

static struct refer_data *refer_data_new(const char *name)
{
	struct refer_data *data;
	int name_len = strlen(name) + 1;

	if ((data = ao2_alloc_options(name_len + sizeof(*data), refer_data_destructor, AO2_ALLOC_OPT_LOCK_NOLOCK))) {
		data->name = data->stuff;
		strcpy(data->name, name);
	}

	return data;
}

static struct refer_data *refer_data_find(struct ao2_container *vars, const char *name)
{
	return ao2_find(vars, name, OBJ_SEARCH_KEY);
}

char *ast_refer_get_var_and_unlink(struct ast_refer *refer, const char *name)
{
	struct refer_data *data;
	char *val = NULL;

	if (!(data = ao2_find(refer->vars, name, OBJ_SEARCH_KEY | OBJ_UNLINK))) {
		return NULL;
	}

	val = ast_strdup(data->value);
	ao2_ref(data, -1);

	return val;
}

static int refer_set_var_full(struct ast_refer *refer, const char *name, const char *value)
{
	struct refer_data *data;

	if (!(data = refer_data_find(refer->vars, name))) {
		if (ast_strlen_zero(value)) {
			return 0;
		}
		if (!(data = refer_data_new(name))) {
			return -1;
		};
		data->value = ast_strdup(value);

		ao2_link(refer->vars, data);
	} else {
		if (ast_strlen_zero(value)) {
			ao2_unlink(refer->vars, data);
		} else {
			ast_free(data->value);
			data->value = ast_strdup(value);
		}
	}

	ao2_ref(data, -1);

	return 0;
}

int ast_refer_set_var_outbound(struct ast_refer *refer, const char *name, const char *value)
{
	return refer_set_var_full(refer, name, value);
}

const char *ast_refer_get_var(struct ast_refer *refer, const char *name)
{
	struct refer_data *data;
	const char *val = NULL;

	if (!(data = refer_data_find(refer->vars, name))) {
		return NULL;
	}

	val = data->value;
	ao2_ref(data, -1);

	return val;
}

struct ast_refer_var_iterator {
	struct ao2_iterator iter;
	struct refer_data *current_used;
};

struct ast_refer_var_iterator *ast_refer_var_iterator_init(const struct ast_refer *refer)
{
	struct ast_refer_var_iterator *iter;

	iter = ast_calloc(1, sizeof(*iter));
	if (!iter) {
		return NULL;
	}

	iter->iter = ao2_iterator_init(refer->vars, 0);

	return iter;
}

int ast_refer_var_iterator_next(struct ast_refer_var_iterator *iter, const char **name, const char **value)
{
	struct refer_data *data;

	if (!iter) {
		return 0;
	}

	data = ao2_iterator_next(&iter->iter);
	if (!data) {
		return 0;
	}

	*name = data->name;
	*value = data->value;

	iter->current_used = data;

	return 1;
}

void ast_refer_var_unref_current(struct ast_refer_var_iterator *iter)
{
	ao2_cleanup(iter->current_used);
	iter->current_used = NULL;
}

void ast_refer_var_iterator_destroy(struct ast_refer_var_iterator *iter)
{
	if (iter) {
		ao2_iterator_destroy(&iter->iter);
		ast_refer_var_unref_current(iter);
		ast_free(iter);
	}
}

/*!
 * \internal \brief Find a \c ast_refer_tech by its technology name
 *
 * \param tech_name The name of the refer technology
 *
 * \note \c refer_techs should be locked via \c refer_techs_lock prior to
 *       calling this function
 *
 * \retval NULL if no \ref ast_refer_tech has been registered
 * \return \ref ast_refer_tech if registered
 */
static const struct ast_refer_tech *refer_find_by_tech_name(const char *tech_name)
{
	const struct ast_refer_tech *current;
	int i;

	for (i = 0; i < AST_VECTOR_SIZE(&refer_techs); i++) {
		current = AST_VECTOR_GET(&refer_techs, i);
		if (!strcmp(current->name, tech_name)) {
			return current;
		}
	}

	return NULL;
}

int ast_refer_send(struct ast_refer *refer)
{
	char *tech_name = NULL;
	const struct ast_refer_tech *refer_tech;
	int res = -1;

	if (ast_strlen_zero(refer->to)) {
		ao2_ref(refer, -1);
		return -1;
	}

	tech_name = ast_strdupa(refer->to);
	tech_name = strsep(&tech_name, ":");

	ast_rwlock_rdlock(&refer_techs_lock);
	refer_tech = refer_find_by_tech_name(tech_name);

	if (!refer_tech) {
		ast_log(LOG_ERROR, "Unknown refer tech: %s\n", tech_name);
		ast_rwlock_unlock(&refer_techs_lock);
		ao2_ref(refer, -1);
		return -1;
	}

	ao2_lock(refer);
	res = refer_tech->refer_send(refer);
	ao2_unlock(refer);

	ast_rwlock_unlock(&refer_techs_lock);

	ao2_ref(refer, -1);

	return res;
}

int ast_refer_tech_register(const struct ast_refer_tech *tech)
{
	const struct ast_refer_tech *match;

	ast_rwlock_wrlock(&refer_techs_lock);

	match = refer_find_by_tech_name(tech->name);
	if (match) {
		ast_log(LOG_ERROR, "Refer technology already registered for '%s'\n",
		        tech->name);
		ast_rwlock_unlock(&refer_techs_lock);
		return -1;
	}

	if (AST_VECTOR_APPEND(&refer_techs, tech)) {
		ast_log(LOG_ERROR, "Failed to register refer technology for '%s'\n",
		        tech->name);
		ast_rwlock_unlock(&refer_techs_lock);
		return -1;
	}
	ast_verb(5, "Refer technology '%s' registered.\n", tech->name);

	ast_rwlock_unlock(&refer_techs_lock);

	return 0;
}

/*!
 * \brief Comparison callback for \c ast_refer_tech vector removal
 *
 * \param vec_elem The element in the vector being compared
 * \param srch The element being looked up
 *
 * \retval non-zero The items are equal
 * \retval 0 The items are not equal
 */
static int refer_tech_cmp(const struct ast_refer_tech *vec_elem, const struct ast_refer_tech *srch)
{
	if (!vec_elem->name || !srch->name) {
		return (vec_elem->name == srch->name) ? 1 : 0;
	}
	return !strcmp(vec_elem->name, srch->name);
}

int ast_refer_tech_unregister(const struct ast_refer_tech *tech)
{
	int match;

	ast_rwlock_wrlock(&refer_techs_lock);
	match = AST_VECTOR_REMOVE_CMP_UNORDERED(&refer_techs, tech, refer_tech_cmp,
	                                        AST_VECTOR_ELEM_CLEANUP_NOOP);
	ast_rwlock_unlock(&refer_techs_lock);

	if (match) {
		ast_log(LOG_ERROR, "No '%s' refer technology found.\n", tech->name);
		return -1;
	}

	ast_verb(5, "Refer technology '%s' unregistered.\n", tech->name);

	return 0;
}

/*!
 * \internal
 * \brief Clean up other resources on Asterisk shutdown
 */
static void refer_shutdown(void)
{
	AST_VECTOR_FREE(&refer_techs);
	ast_rwlock_destroy(&refer_techs_lock);
}

/*!
 * \internal
 * \brief Initialize stuff during Asterisk startup.
 *
 * Cleanup isn't a big deal in this function.  If we return non-zero,
 * Asterisk is going to exit.
 *
 * \retval 0 success
 * \retval non-zero failure
 */
int ast_refer_init(void)
{
	ast_rwlock_init(&refer_techs_lock);
	if (AST_VECTOR_INIT(&refer_techs, 8)) {
		return -1;
	}
	ast_register_cleanup(refer_shutdown);
	return 0;
}

int ast_refer_notify_transfer_request(struct ast_channel *source, const char *referred_by,
				const char *exten, const char *protocol_id,
				struct ast_channel *dest, struct ast_refer_params *params,
				enum ast_control_transfer state)
{
	RAII_VAR(struct ast_ari_transfer_message *, transfer_message, NULL, ao2_cleanup);
	RAII_VAR(struct stasis_message *, msg, NULL, ao2_cleanup);
	RAII_VAR(struct ast_bridge *, source_bridge, NULL, ao2_cleanup);
	RAII_VAR(struct ast_bridge *, dest_bridge, NULL, ao2_cleanup);

	transfer_message = ast_ari_transfer_message_create(source, referred_by, exten, protocol_id, dest, params, state);
	if (!transfer_message) {
		return -1;
	}
	source_bridge = ast_bridge_transfer_acquire_bridge(source);
	if (source_bridge) {
		RAII_VAR(struct ast_channel *, peer, NULL, ast_channel_cleanup);

		ast_bridge_lock(source_bridge);
		transfer_message->source_bridge = ast_bridge_get_snapshot(source_bridge);
		peer = ast_bridge_peer_nolock(source_bridge, source);
		if (peer) {
			ast_channel_lock(peer);
			transfer_message->source_peer = ao2_bump(ast_channel_snapshot(peer));
			ast_channel_unlock(peer);
		}
		ast_bridge_unlock(source_bridge);
	}

	if (dest) {
		dest_bridge = ast_bridge_transfer_acquire_bridge(dest);
		if (dest_bridge) {
			RAII_VAR(struct ast_channel *, peer, NULL, ast_channel_cleanup);

			ast_bridge_lock(dest_bridge);
			transfer_message->dest_bridge = ast_bridge_get_snapshot(dest_bridge);
			peer = ast_bridge_peer_nolock(dest_bridge, dest);
			if (peer) {
				ast_channel_lock(peer);
				transfer_message->dest_peer = ao2_bump(ast_channel_snapshot(peer));
				ast_channel_unlock(peer);
			}
			ast_bridge_unlock(dest_bridge);
		}
	}

	msg = stasis_message_create(ast_channel_transfer_request_type(), transfer_message);
	if (msg) {
		ast_channel_lock(source);
		stasis_publish(ast_channel_topic(source), msg);
		ast_channel_unlock(source);
	}

	return 0;
}
