/*
 *   libteamdctl.c - Teamd daemon control library
 *   Copyright (C) 2013-2015 Jiri Pirko <jiri@resnulli.us>
 *
 *   This library is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU Lesser General Public
 *   License as published by the Free Software Foundation; either
 *   version 2.1 of the License, or (at your option) any later version.
 *
 *   This library 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
 *   Lesser General Public License for more details.
 *
 *   You should have received a copy of the GNU Lesser General Public
 *   License along with this library; if not, write to the Free Software
 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

/**
 * @defgroup libteamdctl Libteamdctl
 * Teamd daemon control library
 *
 * @{
 *
 * Header
 * ------
 * ~~~~{.c}
 * #include <teamdctl.h>
 * ~~~~
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <ctype.h>
#include <syslog.h>
#include <errno.h>
#include <sys/socket.h>
#include <linux/if.h>
#include <private/misc.h>
#include <private/list.h>
#include <teamdctl.h>

#include "config.h"
#include "teamdctl_private.h"

/**
 * SECTION: logging
 */
void teamdctl_log(struct teamdctl *tdc, int priority,
		  const char *file, int line, const char *fn,
		  const char *format, ...)
{
	va_list args;

	va_start(args, format);
	tdc->log_fn(tdc, priority, file, line, fn, format, args);
	va_end(args);
}

static void log_stderr(struct teamdctl *tdc, int priority,
		       const char *file, int line, const char *fn,
		       const char *format, va_list args)
{
	fprintf(stderr, "libteamdctl: %s: ", fn);
	vfprintf(stderr, format, args);
	fprintf(stderr, "\n");
}

static int log_priority(const char *priority)
{
	char *endptr;
	int prio;

	prio = strtol(priority, &endptr, 10);
	if (endptr[0] == '\0' || isspace(endptr[0]))
		return prio;
	if (strncmp(priority, "err", 3) == 0)
		return LOG_ERR;
	if (strncmp(priority, "info", 4) == 0)
		return LOG_INFO;
	if (strncmp(priority, "debug", 5) == 0)
		return LOG_DEBUG;
	return 0;
}

/**
 * SECTION: reply cache
 */

static void reply_cache_clean(struct teamdctl *tdc)
{
	struct teamdctl_reply_cache_item *rcitem;
	struct teamdctl_reply_cache_item *tmp;

	list_for_each_node_entry_safe(rcitem, tmp,
				      &tdc->reply_cache_list, list) {
		list_del(&rcitem->list);
		free(rcitem->reply);
		free(rcitem);
	}
}

static void replace_str(char **p_str, char *new)
{
	if (*p_str)
		free(*p_str);
	*p_str = new;
}

static struct teamdctl_reply_cache_item *find_rcitem(struct teamdctl *tdc,
						     const char *id)
{
	struct teamdctl_reply_cache_item *rcitem;

	list_for_each_node_entry(rcitem, &tdc->reply_cache_list, list) {
		if (!strcmp(rcitem->id, id))
			return rcitem;
	}
	return NULL;
}

static char *reply_cache_query(struct teamdctl *tdc, const char *id)
{
	struct teamdctl_reply_cache_item *rcitem;

	rcitem = find_rcitem(tdc, id);
	if (rcitem)
		return rcitem->reply;
	return NULL;
}

static char *reply_cache_update(struct teamdctl *tdc, const char *id,
				char *reply)
{
	struct teamdctl_reply_cache_item *rcitem;

	rcitem = find_rcitem(tdc, id);
	if (rcitem)
		goto skip_create;

	rcitem = myzalloc(sizeof(*rcitem) + strlen(id) + 1);
	if (!rcitem) {
		free(reply);
		return NULL;
	}
	strcpy(rcitem->id, id);
	list_add_tail(&tdc->reply_cache_list, &rcitem->list);

skip_create:
	replace_str(&rcitem->reply, reply);
	return reply;
}

/**
 * @details Allocates library context and does initial setup.
 *
 * @return New libteam library context.
 **/
TEAMDCTL_EXPORT
struct teamdctl *teamdctl_alloc(void)
{
	struct teamdctl *tdc;
	const char *env;

	tdc = myzalloc(sizeof(*tdc));
	if (!tdc)
		return NULL;

	list_init(&tdc->reply_cache_list);
	tdc->log_fn = log_stderr;
	tdc->log_priority = LOG_ERR;
	/* environment overwrites config */
	env = getenv("TEAMDCTL_LOG");
	if (env != NULL)
		teamdctl_set_log_priority(tdc, log_priority(env));

	dbg(tdc, "teamdctl %p created.", tdc);
	dbg(tdc, "log_priority=%d", tdc->log_priority);
	return tdc;
}

/**
 * @param tdc		libteam library context
 *
 * @details Do library context cleanup.
 **/
TEAMDCTL_EXPORT
void teamdctl_free(struct teamdctl *tdc)
{
	reply_cache_clean(tdc);
	free(tdc);
}

/**
 * @param tdc		libteamdctl library context
 * @param log_fn	function to be called for logging messages
 *
 * @details The built-in logging writes to stderr. It can be overridden
 *	    by a custom function, to plug log messages into the user's
 *	    logging functionality.
 **/
TEAMDCTL_EXPORT
void teamdctl_set_log_fn(struct teamdctl *tdc,
			 void (*log_fn)(struct teamdctl *tdc, int priority,
					const char *file, int line,
					const char *fn, const char *format,
					va_list args))
{
	tdc->log_fn = log_fn;
	dbg(tdc, "Custom logging function %p registered.", log_fn);
}

/**
 * @param tdc		libteamdctl library context
 *
 * @return The current logging priority.
 **/
TEAMDCTL_EXPORT
int teamdctl_get_log_priority(struct teamdctl *tdc)
{
	return tdc->log_priority;
}

/**
 * @param tdc		libteamdctl library context
 * @param priority	the new logging priority
 *
 * @details Set the current logging priority. The value controls which messages
 *	    are logged.
 **/
TEAMDCTL_EXPORT
void teamdctl_set_log_priority(struct teamdctl *tdc, int priority)
{
	tdc->log_priority = priority;
}

static int cli_method_call(struct teamdctl *tdc, const char *method_name,
			   char **p_reply, const char *fmt, ...)
{
	va_list ap;
	int err;

	va_start(ap, fmt);
	err = tdc->cli->method_call(tdc, method_name, p_reply,
				    tdc->cli_priv, fmt, ap);
	va_end(ap);
	return err;
}

static int cli_init(struct teamdctl *tdc, const char *team_name)
{
	int err;

	if (tdc->cli->priv_size) {
		tdc->cli_priv = myzalloc(tdc->cli->priv_size);
		if (!tdc->cli_priv)
			return -ENOMEM;
	}
	err = tdc->cli->init(tdc, team_name, tdc->cli_priv);
	if (err)
		goto free_priv;
	if (tdc->cli->test_method_call_required) {
		err = cli_method_call(tdc, "ConfigDump", NULL, "");
		if (err)
			goto free_priv;
	}
	return 0;
free_priv:
	if (tdc->cli->priv_size)
		free(tdc->cli_priv);
	return err;
}

static void cli_fini(struct teamdctl *tdc)
{
	tdc->cli->fini(tdc, tdc->cli_priv);
	free(tdc->cli_priv);
}

/**
 * @param tdc		libteamdctl library context
 * @param team_name	team device name
 * @param addr		address (for zeromq only)
 * @param cli_type	client type
 *
 * @details Connect to teamd instance controlling team driver instance
 *	    with interface name team_name. Use client type cli_type to connect.
 *	    That can be either "dbus" for connection over D-Bus, "usock" which
 *	    will use unix domain socket or NULL to select the type
 *	    automatically.
 *
 * @return Zero on success or negative number in case of an error.
 **/
TEAMDCTL_EXPORT
int teamdctl_connect(struct teamdctl *tdc, const char *team_name,
		     const char *addr, const char *cli_type)
{
	int err;
	int i;
	const struct teamdctl_cli *teamdctl_cli_list[] = {
		teamdctl_cli_usock_get(),
#ifdef ENABLE_DBUS
		teamdctl_cli_dbus_get(),
#endif
#ifdef ENABLE_ZMQ
		teamdctl_cli_zmq_get(),
#endif
	};
#define TEAMDCTL_CLI_LIST_SIZE ARRAY_SIZE(teamdctl_cli_list)

	if (tdc->cli)
		return -EBUSY;

	for (i = 0; i < TEAMDCTL_CLI_LIST_SIZE; i++) {
		const struct teamdctl_cli *cli = teamdctl_cli_list[i];
		int orig_log_prio = teamdctl_get_log_priority(tdc);

		if (cli_type && strcmp(cli_type, cli->name))
			continue;
		tdc->cli = cli;

		/* In case cli_type is not specified, we will try to connect
		 * using all avaivable clis. Once some of the cli connects,
		 * it will be selected. In that case, silence error messages
		 * that can appear during cli init by setting log priority to
		 * LOG_EMERG.
		 */
		if (!cli_type && orig_log_prio < LOG_DEBUG)
			teamdctl_set_log_priority(tdc, LOG_EMERG);

		tdc->addr = (char *) addr;

		err = cli_init(tdc, team_name);

		/* restore original log priority */
		teamdctl_set_log_priority(tdc, orig_log_prio);

		if (!err)
			break; /* usable cli found */

		if (cli_type) {
			err(tdc, "Failed to connect using CLI \"%s\".",
			    cli->name);
			goto err_out;
		}
	}
	if (i == TEAMDCTL_CLI_LIST_SIZE) {
		if (!cli_type)
			err(tdc, "Failed to connect using all CLIs.");
		else
			err(tdc, "Failed to connect using unknown CLI \"%s\".",
				 cli_type);
		err = -EINVAL;
		goto err_out;
	}

	dbg(tdc, "Connected using CLI \"%s\".", tdc->cli->name);

	err = teamdctl_refresh(tdc);
	if (err)
		goto err_out;
	return 0;
err_out:
	tdc->cli = NULL;
	return err;
}

/**
 * @param tdc		libteamdctl library context
 *
 * @details Disconnect from teamd instance.
 **/
TEAMDCTL_EXPORT
void teamdctl_disconnect(struct teamdctl *tdc)
{
	cli_fini(tdc);
	tdc->cli = NULL;
}


static int cache_config(struct teamdctl *tdc, const char *id, char **p_reply)
{
	int err;
	char *reply;

	err = cli_method_call(tdc, id, &reply, "");
	if (err)
		return err;
	reply = reply_cache_update(tdc, id, reply);
	if (!reply)
		return -ENOMEM;
	if (p_reply)
		*p_reply = reply;
	return 0;
}

/**
 * @param tdc		libteamdctl library context
 *
 * @details Refresh cache.
 *
 * @return Zero on success or negative number in case of an error.
 **/
TEAMDCTL_EXPORT
int teamdctl_refresh(struct teamdctl *tdc)
{
	int err;

	err = cache_config(tdc, "ConfigDump", NULL);
	if (err)
		return err;
	err = cache_config(tdc, "ConfigDumpActual", NULL);
	if (err)
		return err;
	err = cache_config(tdc, "StateDump", NULL);
	if (err)
		return err;
	return 0;
}

/**
 * @param tdc		libteamdctl library context
 * @param port_devname	port device name
 *
 * @details Adds specified port to team.
 *
 * @return Zero on success or negative number in case of an error.
 **/
TEAMDCTL_EXPORT
int teamdctl_port_add(struct teamdctl *tdc, const char *port_devname)
{
	return cli_method_call(tdc, "PortAdd", NULL, "s", port_devname);
}

/**
 * @param tdc		libteamdctl library context
 * @param port_devname	port device name
 *
 * @details Removes specified port from team.
 *
 * @return Zero on success or negative number in case of an error.
 **/
TEAMDCTL_EXPORT
int teamdctl_port_remove(struct teamdctl *tdc, const char *port_devname)
{
	return cli_method_call(tdc, "PortRemove", NULL, "s", port_devname);
}

/**
 * @param tdc			libteamdctl library context
 * @param port_devname		port device name
 * @param port_config_raw	port config
 *
 * @details Update config for specified port.
 *
 * @return Zero on success or negative number in case of an error.
 **/
TEAMDCTL_EXPORT
int teamdctl_port_config_update_raw(struct teamdctl *tdc,
				    const char *port_devname,
				    const char *port_config_raw)
{
	return cli_method_call(tdc, "PortConfigUpdate", NULL,
			       "ss", port_devname, port_config_raw);
}

/**
 * @param tdc		libteamdctl library context
 * @param port_devname	port device name
 * @param p_cfg		pointer to string which will be set
 *
 * @details Gets raw port config string.
 *	    Does direct method call avoiding possible stale data in the cache.
 *	    Note: the obtained string should not be modified or freed by caller.
 *
 * @return Zero on success or negative number in case of an error.
 **/
TEAMDCTL_EXPORT
int teamdctl_port_config_get_raw_direct(struct teamdctl *tdc,
					const char *port_devname,
					char **p_cfg)
{
	int err;
	char *reply;
#define PC_ID_PREFIX "_portcnf_"
	char id[sizeof(PC_ID_PREFIX) + IFNAMSIZ + 1];

	if (strlen(port_devname) > IFNAMSIZ)
		return -EINVAL;
	err = cli_method_call(tdc, "PortConfigDump", &reply, "s", port_devname);
	if (err)
		return err;
	sprintf(id, "%s%s", PC_ID_PREFIX, port_devname);
	reply = reply_cache_update(tdc, id, reply);
	if (!reply)
		return -ENOMEM;
	if (p_cfg)
		*p_cfg = reply;
	return 0;
}

/**
 * @param tdc		libteamdctl library context
 *
 * @details Gets raw config string.
 *	    Using reply cache. Return value is never NULL.
 *	    To refresh the cache, use teamdctl_refresh function.
 *	    Note: the obtained string should not be modified or freed by caller.
 *
 * Return Pointer to cached config string.
 **/
TEAMDCTL_EXPORT
char *teamdctl_config_get_raw(struct teamdctl *tdc)
{
	return reply_cache_query(tdc, "ConfigDump");
}

/**
 * @param tdc		libteamdctl library context
 * @param p_cfg		pointer to string which will be set
 *
 * @details Gets raw config string.
 *	    Does direct method call avoiding possible stale data in the cache.
 *	    Note: the obtained string should not be modified or freed by caller.
 *
 * @return Zero on success or negative number in case of an error.
 **/
TEAMDCTL_EXPORT
int teamdctl_config_get_raw_direct(struct teamdctl *tdc, char **p_cfg)
{
	return cache_config(tdc, "ConfigDump", p_cfg);
}

/**
 * @param tdc		libteamdctl library context
 *
 * @details Gets raw actual config string.
 *	    Using reply cache. Return value is never NULL.
 *	    To refresh the cache, use teamdctl_refresh function.
 *	    Note: the obtained string should not be modified or freed by caller.
 *
 * @return Pointer to cached actual config string.
 **/
TEAMDCTL_EXPORT
char *teamdctl_config_actual_get_raw(struct teamdctl *tdc)
{
	return reply_cache_query(tdc, "ConfigDumpActual");
}

/**
 * @param tdc		libteamdctl library context
 * @param p_cfg		pointer to string which will be set
 *
 * @details Gets raw actual config string.
 *	    Does direct method call avoiding possible stale data in the cache.
 *	    Note: the obtained string should not be modified or freed by caller.
 *
 * @return Zero on success or negative number in case of an error.
 **/
TEAMDCTL_EXPORT
int teamdctl_config_actual_get_raw_direct(struct teamdctl *tdc, char **p_cfg)
{
	return cache_config(tdc, "ConfigDumpActual", p_cfg);
}

/**
 * @param tdc		libteamdctl library context
 *
 * @details Gets raw state string.
 *	    Using reply cache. Return value is never NULL.
 *	    To refresh the cache, use teamdctl_refresh function.
 *	    Note: the obtained string should not be modified or freed by caller.
 *
 * @return Pointer to cached state string.
 **/
TEAMDCTL_EXPORT
char *teamdctl_state_get_raw(struct teamdctl *tdc)
{
	return reply_cache_query(tdc, "StateDump");
}

/**
 * @param tdc		libteamdctl library context
 * @param p_cfg		pointer to string which will be set
 *
 * @details Gets raw state string.
 *	    Does direct method call avoiding possible stale data in the cache.
 *	    Note: the obtained string should not be modified or freed by caller.
 *
 * @return Zero on success or negative number in case of an error.
 **/
TEAMDCTL_EXPORT
int teamdctl_state_get_raw_direct(struct teamdctl *tdc, char **p_cfg)
{
	return cache_config(tdc, "StateDump", p_cfg);
}

/**
 * @param tdc		libteamdctl library context
 * @param item_path	path to item
 * @param p_value	pointer where reply string will be stored
 *
 * @details Get state item value. Note that caller is responsible to
 *	    free *p_value.
 *
 * @return Zero on success or negative number in case of an error.
 **/
TEAMDCTL_EXPORT
int teamdctl_state_item_value_get(struct teamdctl *tdc, const char *item_path,
				  char **p_value)
{
	return cli_method_call(tdc, "StateItemValueGet", p_value,
			       "s", item_path);
}
/**
 * @param tdc		libteamdctl library context
 * @param item_path	path to item
 * @param value		new value to be set
 *
 * @details Set state item value.
 *
 * @return Zero on success or negative number in case of an error.
 **/
TEAMDCTL_EXPORT
int teamdctl_state_item_value_set(struct teamdctl *tdc, const char *item_path,
				  const char *value)
{
	return cli_method_call(tdc, "StateItemValueSet", NULL,
			       "ss", item_path, value);
}

/**
 * @}
 */
