/*
 * Asterisk -- An open source telephony toolkit.
 *
 * Copyright (C) 2016, CFWare, LLC
 *
 * Corey Farrell <git@cfware.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 Custom function management routines.
 *
 * \author Corey Farrell <git@cfware.com>
 */

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

#include "asterisk.h"

#include "asterisk/_private.h"
#include "asterisk/cli.h"
#include "asterisk/linkedlists.h"
#include "asterisk/module.h"
#include "asterisk/pbx.h"
#include "asterisk/stasis_channels.h"
#include "asterisk/strings.h"
#include "asterisk/term.h"
#include "asterisk/utils.h"
#include "asterisk/xmldoc.h"
#include "pbx_private.h"

/*! \brief ast_app: A registered application */
struct ast_app {
	int (*execute)(struct ast_channel *chan, const char *data);
	AST_DECLARE_STRING_FIELDS(
		AST_STRING_FIELD(synopsis);     /*!< Synopsis text for 'show applications' */
		AST_STRING_FIELD(provided_by);  /*!< Provided-by text for 'show applications' */
		AST_STRING_FIELD(since);        /*!< Since text for 'show applications' */
		AST_STRING_FIELD(description);  /*!< Description (help text) for 'show application &lt;name&gt;' */
		AST_STRING_FIELD(syntax);       /*!< Syntax text for 'core show applications' */
		AST_STRING_FIELD(arguments);    /*!< Arguments description */
		AST_STRING_FIELD(seealso);      /*!< See also */
	);
#ifdef AST_XML_DOCS
	enum ast_doc_src docsrc;		/*!< Where the documentation come from. */
#endif
	AST_RWLIST_ENTRY(ast_app) list;		/*!< Next app in list */
	struct ast_module *module;		/*!< Module this app belongs to */
	char name[0];				/*!< Name of the application */
};

/*!
 * \brief Registered applications container.
 *
 * It is sorted by application name.
 */
static AST_RWLIST_HEAD_STATIC(apps, ast_app);

static struct ast_app *pbx_findapp_nolock(const char *name)
{
	struct ast_app *cur;
	int cmp;

	AST_RWLIST_TRAVERSE(&apps, cur, list) {
		cmp = strcasecmp(name, cur->name);
		if (cmp > 0) {
			continue;
		}
		if (!cmp) {
			/* Found it. */
			break;
		}
		/* Not in container. */
		cur = NULL;
		break;
	}

	return cur;
}

struct ast_app *pbx_findapp(const char *app)
{
	struct ast_app *ret;

	AST_RWLIST_RDLOCK(&apps);
	ret = pbx_findapp_nolock(app);
	AST_RWLIST_UNLOCK(&apps);

	return ret;
}

/*! \brief Dynamically register a new dial plan application */
int ast_register_application2(const char *app, int (*execute)(struct ast_channel *, const char *), const char *synopsis, const char *description, void *mod)
{
	struct ast_app *tmp;
	struct ast_app *cur;
	int length;
#ifdef AST_XML_DOCS
	char *tmpxml;
#endif

	AST_RWLIST_WRLOCK(&apps);
	cur = pbx_findapp_nolock(app);
	if (cur) {
		ast_log(LOG_WARNING, "Already have an application '%s'\n", app);
		AST_RWLIST_UNLOCK(&apps);
		return -1;
	}

	length = sizeof(*tmp) + strlen(app) + 1;

	if (!(tmp = ast_calloc(1, length))) {
		AST_RWLIST_UNLOCK(&apps);
		return -1;
	}

	if (ast_string_field_init(tmp, 128)) {
		AST_RWLIST_UNLOCK(&apps);
		ast_free(tmp);
		return -1;
	}

	strcpy(tmp->name, app);
	tmp->execute = execute;
	tmp->module = mod;

#ifdef AST_XML_DOCS
	/* Try to lookup the docs in our XML documentation database */
	if (ast_strlen_zero(synopsis) && ast_strlen_zero(description)) {
		/* load synopsis */
		tmpxml = ast_xmldoc_build_synopsis("application", app, ast_module_name(tmp->module));
		ast_string_field_set(tmp, synopsis, tmpxml);
		ast_free(tmpxml);

		/* load provied_by */
		tmpxml = ast_xmldoc_build_provided_by("application", app, ast_module_name(tmp->module));
		ast_string_field_set(tmp, provided_by, tmpxml);
		ast_free(tmpxml);

		/* load since */
		tmpxml = ast_xmldoc_build_since("application", app, ast_module_name(tmp->module));
		ast_string_field_set(tmp, since, tmpxml);
		ast_free(tmpxml);

		/* load description */
		tmpxml = ast_xmldoc_build_description("application", app, ast_module_name(tmp->module));
		ast_string_field_set(tmp, description, tmpxml);
		ast_free(tmpxml);

		/* load syntax */
		tmpxml = ast_xmldoc_build_syntax("application", app, ast_module_name(tmp->module));
		ast_string_field_set(tmp, syntax, tmpxml);
		ast_free(tmpxml);

		/* load arguments */
		tmpxml = ast_xmldoc_build_arguments("application", app, ast_module_name(tmp->module));
		ast_string_field_set(tmp, arguments, tmpxml);
		ast_free(tmpxml);

		/* load seealso */
		tmpxml = ast_xmldoc_build_seealso("application", app, ast_module_name(tmp->module));
		ast_string_field_set(tmp, seealso, tmpxml);
		ast_free(tmpxml);
		tmp->docsrc = AST_XML_DOC;
	} else {
#endif
		ast_string_field_set(tmp, synopsis, synopsis);
		ast_string_field_set(tmp, description, description);
#ifdef AST_XML_DOCS
		tmp->docsrc = AST_STATIC_DOC;
	}
#endif

	/* Store in alphabetical order */
	AST_RWLIST_TRAVERSE_SAFE_BEGIN(&apps, cur, list) {
		if (strcasecmp(tmp->name, cur->name) < 0) {
			AST_RWLIST_INSERT_BEFORE_CURRENT(tmp, list);
			break;
		}
	}
	AST_RWLIST_TRAVERSE_SAFE_END;
	if (!cur)
		AST_RWLIST_INSERT_TAIL(&apps, tmp, list);

	ast_verb(5, "Registered application '" COLORIZE_FMT "'\n", COLORIZE(COLOR_BRCYAN, 0, tmp->name));

	AST_RWLIST_UNLOCK(&apps);

	return 0;
}

static void print_app_docs(struct ast_app *aa, int fd)
{
	char *synopsis = NULL, *provided_by = NULL, *since = NULL, *description = NULL, *syntax = NULL, *arguments = NULL, *seealso = NULL;

#ifdef AST_XML_DOCS
	if (aa->docsrc == AST_XML_DOC) {
		synopsis = ast_xmldoc_printable(S_OR(aa->synopsis, "Not available"), 1);
		provided_by = ast_xmldoc_printable(S_OR(aa->provided_by, "Not available"), 1);
		since = ast_xmldoc_printable(S_OR(aa->since, "Not available"), 1);
		description = ast_xmldoc_printable(S_OR(aa->description, "Not available"), 1);
		syntax = ast_xmldoc_printable(S_OR(aa->syntax, "Not available"), 1);
		arguments = ast_xmldoc_printable(S_OR(aa->arguments, "Not available"), 1);
		seealso = ast_xmldoc_printable(S_OR(aa->seealso, "Not available"), 1);
	} else
#endif
	{
		synopsis = ast_strdup(S_OR(aa->synopsis, "Not Available"));
		provided_by = ast_strdup(S_OR(aa->provided_by, "Not Available"));
		since = ast_strdup(S_OR(aa->since, "Not Available"));
		description = ast_strdup(S_OR(aa->description, "Not Available"));
		syntax = ast_strdup(S_OR(aa->syntax, "Not Available"));
		arguments = ast_strdup(S_OR(aa->arguments, "Not Available"));
		seealso = ast_strdup(S_OR(aa->seealso, "Not Available"));
	}
		/* check allocated memory. */
	if (!synopsis || !provided_by || !since || !description || !syntax || !arguments || !seealso) {
		goto free_docs;
	}

	ast_cli(fd, "\n"
		"%s  -= Info about Application '%s' =- %s\n\n"
		COLORIZE_FMT "\n"
		"%s\n\n"
		COLORIZE_FMT "\n"
		"%s\n\n"
		COLORIZE_FMT "\n"
		"%s\n\n"
		COLORIZE_FMT "\n"
		"%s\n\n"
		COLORIZE_FMT "\n"
		"%s\n\n"
		COLORIZE_FMT "\n"
		"%s\n\n"
		COLORIZE_FMT "\n"
		"%s\n\n",
		ast_term_color(COLOR_MAGENTA, 0), aa->name, ast_term_reset(),
		COLORIZE(COLOR_MAGENTA, 0, "[Synopsis]"), synopsis,
		COLORIZE(COLOR_MAGENTA, 0, "[Provided By]"), provided_by,
		COLORIZE(COLOR_MAGENTA, 0, "[Since]"), since,
		COLORIZE(COLOR_MAGENTA, 0, "[Description]"), description,
		COLORIZE(COLOR_MAGENTA, 0, "[Syntax]"), syntax,
		COLORIZE(COLOR_MAGENTA, 0, "[Arguments]"), arguments,
		COLORIZE(COLOR_MAGENTA, 0, "[See Also]"), seealso
		);

free_docs:
	ast_free(synopsis);
	ast_free(provided_by);
	ast_free(since);
	ast_free(description);
	ast_free(syntax);
	ast_free(arguments);
	ast_free(seealso);
}

/*!
 * \brief 'show application' CLI command implementation function...
 */
static char *handle_show_application(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
{
	struct ast_app *aa;
	int app, no_registered_app = 1;

	switch (cmd) {
	case CLI_INIT:
		e->command = "core show application";
		e->usage =
			"Usage: core show application <application> [<application> [<application> [...]]]\n"
			"       Describes a particular application.\n";
		return NULL;
	case CLI_GENERATE:
		/*
		 * There is a possibility to show information about more than one
		 * application at one time. You can type 'show application Dial Echo' and
		 * you will see information about these two applications ...
		 */
		return ast_complete_applications(a->line, a->word, -1);
	}

	if (a->argc < 4) {
		return CLI_SHOWUSAGE;
	}

	AST_RWLIST_RDLOCK(&apps);
	AST_RWLIST_TRAVERSE(&apps, aa, list) {
		/* Check for each app that was supplied as an argument */
		for (app = 3; app < a->argc; app++) {
			if (strcasecmp(aa->name, a->argv[app])) {
				continue;
			}

			/* We found it! */
			no_registered_app = 0;

			print_app_docs(aa, a->fd);
		}
	}
	AST_RWLIST_UNLOCK(&apps);

	/* we found at least one app? no? */
	if (no_registered_app) {
		ast_cli(a->fd, "Your application(s) is (are) not registered\n");
		return CLI_FAILURE;
	}

	return CLI_SUCCESS;
}

static char *handle_show_applications(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
{
	struct ast_app *aa;
	int like = 0, describing = 0;
	int total_match = 0;    /* Number of matches in like clause */
	int total_apps = 0;     /* Number of apps registered */

	switch (cmd) {
	case CLI_INIT:
		e->command = "core show applications [like|describing]";
		e->usage =
			"Usage: core show applications [{like|describing} <text>]\n"
			"       List applications which are currently available.\n"
			"       If 'like', <text> will be a substring of the app name\n"
			"       If 'describing', <text> will be a substring of the description\n";
		return NULL;
	case CLI_GENERATE:
		return NULL;
	}

	AST_RWLIST_RDLOCK(&apps);

	if (AST_RWLIST_EMPTY(&apps)) {
		ast_cli(a->fd, "There are no registered applications\n");
		AST_RWLIST_UNLOCK(&apps);
		return CLI_SUCCESS;
	}

	/* core list applications like <keyword> */
	if ((a->argc == 5) && (!strcmp(a->argv[3], "like"))) {
		like = 1;
	} else if ((a->argc > 4) && (!strcmp(a->argv[3], "describing"))) {
		describing = 1;
	}

	/* core list applications describing <keyword1> [<keyword2>] [...] */
	if ((!like) && (!describing)) {
		ast_cli(a->fd, "    -= Registered Asterisk Applications =-\n");
	} else {
		ast_cli(a->fd, "    -= Matching Asterisk Applications =-\n");
	}

	AST_RWLIST_TRAVERSE(&apps, aa, list) {
		int printapp = 0;
		total_apps++;
		if (like) {
			if (strcasestr(aa->name, a->argv[4])) {
				printapp = 1;
				total_match++;
			}
		} else if (describing) {
			if (aa->description) {
				/* Match all words on command line */
				int i;
				printapp = 1;
				for (i = 4; i < a->argc; i++) {
					if (!strcasestr(aa->description, a->argv[i])) {
						printapp = 0;
					} else {
						total_match++;
					}
				}
			}
		} else {
			printapp = 1;
		}

		if (printapp) {
			ast_cli(a->fd,"  %20s: %s\n", aa->name, aa->synopsis ? aa->synopsis : "<Synopsis not available>");
		}
	}
	if ((!like) && (!describing)) {
		ast_cli(a->fd, "    -= %d Applications Registered =-\n",total_apps);
	} else {
		ast_cli(a->fd, "    -= %d Applications Matching =-\n",total_match);
	}

	AST_RWLIST_UNLOCK(&apps);

	return CLI_SUCCESS;
}

int ast_unregister_application(const char *app)
{
	struct ast_app *cur;
	int cmp;

	/* Anticipate need for conlock in unreference_cached_app(), in order to avoid
	 * possible deadlock with pbx_extension_helper()/pbx_findapp()
	 */
	ast_rdlock_contexts();

	AST_RWLIST_WRLOCK(&apps);
	AST_RWLIST_TRAVERSE_SAFE_BEGIN(&apps, cur, list) {
		cmp = strcasecmp(app, cur->name);
		if (cmp > 0) {
			continue;
		}
		if (!cmp) {
			/* Found it. */
			unreference_cached_app(cur);
			AST_RWLIST_REMOVE_CURRENT(list);
			ast_verb(5, "Unregistered application '%s'\n", cur->name);
			ast_string_field_free_memory(cur);
			ast_free(cur);
			break;
		}
		/* Not in container. */
		cur = NULL;
		break;
	}
	AST_RWLIST_TRAVERSE_SAFE_END;
	AST_RWLIST_UNLOCK(&apps);

	ast_unlock_contexts();

	return cur ? 0 : -1;
}

char *ast_complete_applications(const char *line, const char *word, int state)
{
	struct ast_app *app;
	int which = 0;
	int cmp;
	char *ret = NULL;
	size_t wordlen = strlen(word);

	AST_RWLIST_RDLOCK(&apps);
	AST_RWLIST_TRAVERSE(&apps, app, list) {
		cmp = strncasecmp(word, app->name, wordlen);
		if (cmp < 0) {
			/* No more matches. */
			break;
		} else if (!cmp) {
			/* Found match. */
			if (state != -1) {
				if (++which <= state) {
					/* Not enough matches. */
					continue;
				}
				ret = ast_strdup(app->name);
				break;
			}
			if (ast_cli_completion_add(ast_strdup(app->name))) {
				break;
			}
		}
	}
	AST_RWLIST_UNLOCK(&apps);

	return ret;
}

const char *app_name(struct ast_app *app)
{
	return app->name;
}

/*!
   \note This function is special. It saves the stack so that no matter
   how many times it is called, it returns to the same place */
int pbx_exec(struct ast_channel *c,	/*!< Channel */
	     struct ast_app *app,	/*!< Application */
	     const char *data)		/*!< Data for execution */
{
	int res;
	struct ast_module_user *u = NULL;
	const char *saved_c_appl;
	const char *saved_c_data;

	/* save channel values */
	saved_c_appl= ast_channel_appl(c);
	saved_c_data= ast_channel_data(c);

	ast_channel_lock(c);
	ast_channel_appl_set(c, app->name);
	ast_channel_data_set(c, data);
	ast_channel_publish_snapshot(c);
	ast_channel_unlock(c);

	if (app->module)
		u = __ast_module_user_add(app->module, c);
	res = app->execute(c, S_OR(data, ""));
	if (app->module && u)
		__ast_module_user_remove(app->module, u);
	/* restore channel values */
	ast_channel_appl_set(c, saved_c_appl);
	ast_channel_data_set(c, saved_c_data);
	return res;
}

int ast_pbx_exec_application(struct ast_channel *chan, const char *app_name, const char *app_args)
{
	int res = -1;
	struct ast_app *app;

	app = pbx_findapp(app_name);
	if (!app) {
		ast_log(LOG_WARNING, "Could not find application (%s)\n", app_name);
	} else {
		struct ast_str *substituted_args = NULL;

		if (!ast_strlen_zero(app_args) && (substituted_args = ast_str_create(16))) {
			ast_str_substitute_variables(&substituted_args, 0, chan, app_args);
			res = pbx_exec(chan, app, ast_str_buffer(substituted_args));
			ast_free(substituted_args);
		} else {
			if (!ast_strlen_zero(app_args)) {
				ast_log(LOG_WARNING, "Could not substitute application argument variables for %s\n", app_name);
			}
			res = pbx_exec(chan, app, app_args);
		}
		/* Manually make a snapshot now, since pbx_exec won't necessarily get called again immediately. */
		ast_channel_publish_snapshot(chan);
	}
	return res;
}

static struct ast_cli_entry app_cli[] = {
	AST_CLI_DEFINE(handle_show_applications, "Shows registered dialplan applications"),
	AST_CLI_DEFINE(handle_show_application, "Describe a specific dialplan application"),
};

static void unload_pbx_app(void)
{
	ast_cli_unregister_multiple(app_cli, ARRAY_LEN(app_cli));
}

int load_pbx_app(void)
{
	ast_cli_register_multiple(app_cli, ARRAY_LEN(app_cli));
	ast_register_cleanup(unload_pbx_app);

	return 0;
}
