/*
 *   teamd_dbus.c - Teamd dbus api
 *   Copyright (C) 2012-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
 */

#include "config.h"

#ifdef ENABLE_DBUS

#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <dbus/dbus.h>
#include <private/misc.h>
#include <team.h>

#include "teamd.h"
#include "teamd_dbus.h"
#include "teamd_dbus_common.h"
#include "teamd_ctl.h"

static const char *introspection_xml =
	"<node name='" TEAMD_DBUS_PATH "'>"
	"  <interface name='" TEAMD_DBUS_IFACE "'>"
	"    <method name='PortConfigUpdate'>"
	"      <arg type='s' name='port_devname' direction='in'/>"
	"      <arg type='s' name='port_config' direction='in'/>"
	"    </method>"
	"    <method name='PortConfigDump'>"
	"      <arg type='s' name='port_devname' direction='in'/>"
	"    </method>"
	"    <method name='PortAdd'>"
	"      <arg type='s' name='port_devname' direction='in'/>"
	"    </method>"
	"    <method name='PortRemove'>"
	"      <arg type='s' name='port_devname' direction='in'/>"
	"    </method>"
	"    <method name='ConfigDump'>"
	"    </method>"
	"    <method name='ConfigDumpActual'>"
	"    </method>"
	"    <method name='StateDump'>"
	"    </method>"
	"    <method name='StateItemValueGet'>"
	"      <arg type='s' name='state_item_path' direction='in'/>"
	"    </method>"
	"    <method name='StateItemValueSet'>"
	"      <arg type='s' name='state_item_path' direction='in'/>"
	"      <arg type='s' name='value' direction='in'/>"
	"    </method>"
	"  </interface>"
	"</node>";

static DBusMessage *introspect(DBusMessage *message)
{
	DBusMessage *reply;

	reply = dbus_message_new_method_return(message);
	if (!reply)
		return NULL;
	dbus_message_append_args(reply, DBUS_TYPE_STRING, &introspection_xml,
				 DBUS_TYPE_INVALID);
	return reply;
}

struct dbus_ops_priv {
	DBusMessage *reply;
	DBusMessage *message;
};

static int dbus_op_get_args(void *ops_priv, const char *fmt, ...)
{
	va_list ap;
	struct dbus_ops_priv *dbus_ops_priv = ops_priv;
	DBusMessage *message = dbus_ops_priv->message;
	DBusMessageIter iter;
	int arg_type;
	char **pstr;
	int err = 0;

	dbus_message_iter_init(message, &iter);
	va_start(ap, fmt);
	while (*fmt) {
		arg_type = dbus_message_iter_get_arg_type(&iter);
		if (arg_type == DBUS_TYPE_INVALID) {
			teamd_log_err("Insufficient number of arguments in message.");
			err = -EINVAL;
			goto out;
		}
		switch (*fmt++) {
		case 's': /* string */
			if (arg_type != DBUS_TYPE_STRING) {
				teamd_log_err("Unexpected argument type found in message.");
				err = -EINVAL;
				goto out;
			}
			pstr = va_arg(ap, char **);
			dbus_message_iter_get_basic(&iter, pstr);
			break;
		default:
			teamd_log_err("Unknown argument type requested");
			err = -EINVAL;
			goto out;
		}
		dbus_message_iter_next(&iter);
	}
out:
	va_end(ap);
	return err;
}

static int dbus_op_reply_err(void *ops_priv, const char *err_code,
			     const char *err_msg)
{
	struct dbus_ops_priv *dbus_ops_priv = ops_priv;
	int err;
	char *err_code_buf;

	err = asprintf(&err_code_buf, TEAMD_DBUS_IFACE "%s", err_code);
	if (err == -1)
		return -errno;
	dbus_ops_priv->reply = dbus_message_new_error(dbus_ops_priv->message,
						      err_code_buf,
						      err_msg);
	free(err_code_buf);
	return 0;
}

static int dbus_op_reply_succ(void *ops_priv, const char *msg)
{
	struct dbus_ops_priv *dbus_ops_priv = ops_priv;
	DBusMessage *reply;

	if (!msg)
		return 0;
	reply = dbus_message_new_method_return(dbus_ops_priv->message);
	if (reply)
		dbus_message_append_args(reply, DBUS_TYPE_STRING, &msg,
					 DBUS_TYPE_INVALID);
	dbus_ops_priv->reply = reply;
	return 0;
}

static const struct teamd_ctl_method_ops teamd_dbus_ctl_method_ops = {
	.get_args = dbus_op_get_args,
	.reply_err = dbus_op_reply_err,
	.reply_succ = dbus_op_reply_succ,
};

static DBusHandlerResult message_handler(DBusConnection *con,
					 DBusMessage *message,
					 void *user_data)
{
	const char *method;
	const char *path;
	const char *msg_interface;
	DBusMessage *reply = NULL;
	struct teamd_context *ctx = user_data;

	method = dbus_message_get_member(message);
	path = dbus_message_get_path(message);
	msg_interface = dbus_message_get_interface(message);
	if (!method || !path || !msg_interface)
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
	teamd_log_dbg("dbus: %s.%s (%s)", msg_interface, method, path);

	if (!strcmp(method, "Introspect") &&
	    !strcmp(msg_interface, "org.freedesktop.DBus.Introspectable")) {
		reply = introspect(message);
	}

	if (!strcmp(msg_interface, TEAMD_DBUS_IFACE) &&
	    teamd_ctl_method_exists(method)) {
		struct dbus_ops_priv dbus_ops_priv;

		dbus_ops_priv.reply = NULL;
		dbus_ops_priv.message = message;
		teamd_ctl_method_call(ctx, method, &teamd_dbus_ctl_method_ops,
				      &dbus_ops_priv);
		reply = dbus_ops_priv.reply;
	}

	if (!dbus_message_get_no_reply(message)) {
		if (!reply)
			reply = dbus_message_new_method_return(message);
		if (reply) {
			if (!dbus_connection_send(con, reply, NULL))
				teamd_log_err("dbus: Failed to send reply.");
			dbus_message_unref(reply);
		}
	}

	return DBUS_HANDLER_RESULT_HANDLED;
}

static const DBusObjectPathVTable vtable = {
	.message_function = message_handler,
};

static int teamd_dbus_iface_init(struct teamd_context *ctx)
{
	if (dbus_connection_register_object_path(ctx->dbus.con,
						 TEAMD_DBUS_PATH, &vtable,
						 ctx) == FALSE) {
		teamd_log_err("dbus: Could not set up message handler");
		return -EINVAL;
	}
	return 0;
}

static void teamd_dbus_iface_fini(struct teamd_context *ctx)
{
	dbus_connection_unregister_object_path(ctx->dbus.con,
					       TEAMD_DBUS_PATH);
}

static int teamd_dbus_con_init(struct teamd_context *ctx)
{
	DBusError error;
	int err = 0;

	dbus_error_init(&error);
	ctx->dbus.con = dbus_bus_get(DBUS_BUS_SYSTEM, &error);
	if (!ctx->dbus.con) {
		teamd_log_err("dbus: Could not acquire the system bus: %s - %s",
			      error.name, error.message);
		err = -EINVAL;
		goto free_err;
	}
	dbus_connection_set_exit_on_disconnect(ctx->dbus.con, FALSE);
free_err:
	dbus_error_free(&error);
	return err;
}

static void teamd_dbus_con_fini(struct teamd_context *ctx)
{
	dbus_connection_unref(ctx->dbus.con);
}

static int callback_watch(struct teamd_context *ctx, int events, void *priv)
{
	DBusWatch *watch = priv;

	dbus_connection_ref(ctx->dbus.con);
	if (events & TEAMD_LOOP_FD_EVENT_READ)
		dbus_watch_handle(watch, DBUS_WATCH_READABLE);
	if (events & TEAMD_LOOP_FD_EVENT_WRITE)
		dbus_watch_handle(watch, DBUS_WATCH_WRITABLE);
	if (events & TEAMD_LOOP_FD_EVENT_EXCEPTION)
		dbus_watch_handle(watch, DBUS_WATCH_ERROR);
	dbus_connection_unref(ctx->dbus.con);
	return 0;
}

#define WATCH_CB_NAME "dbus_watch"

static dbus_bool_t add_watch(DBusWatch *watch, void *priv)
{
	struct teamd_context *ctx = priv;
	unsigned int flags;
	int err;
	int fd;
	int fd_events;

	fd = dbus_watch_get_unix_fd(watch);
	flags = dbus_watch_get_flags(watch);
	fd_events = TEAMD_LOOP_FD_EVENT_EXCEPTION;
	if (flags & DBUS_WATCH_READABLE)
		fd_events |= TEAMD_LOOP_FD_EVENT_READ;
	if (flags & DBUS_WATCH_WRITABLE)
		fd_events |= TEAMD_LOOP_FD_EVENT_WRITE;

	err = teamd_loop_callback_fd_add(ctx, WATCH_CB_NAME, watch,
					 callback_watch, fd, fd_events);
	if (err)
		return FALSE;
	if (dbus_watch_get_enabled(watch))
		teamd_loop_callback_enable(ctx, WATCH_CB_NAME, watch);
	return TRUE;
}

static void remove_watch(DBusWatch *watch, void *priv)
{
	struct teamd_context *ctx = priv;

	teamd_loop_callback_del(ctx, WATCH_CB_NAME, watch);
}

static void toggle_watch(DBusWatch *watch, void *priv)
{
	struct teamd_context *ctx = priv;

	if (dbus_watch_get_enabled(watch))
		teamd_loop_callback_enable(ctx, WATCH_CB_NAME, watch);
	else
		teamd_loop_callback_disable(ctx, WATCH_CB_NAME, watch);
}

static int callback_timeout(struct teamd_context *ctx, int events, void *priv)
{
	DBusTimeout *timeout = priv;

	dbus_timeout_handle(timeout);
	return 0;
}

#define TIMEOUT_CB_NAME "dbus_timeout"

static dbus_bool_t add_timeout(DBusTimeout *timeout, void *priv)
{
	struct teamd_context *ctx = priv;
	int err;
	struct timespec ts;

	ms_to_timespec(&ts, dbus_timeout_get_interval(timeout));
	err = teamd_loop_callback_timer_add_set(ctx, TIMEOUT_CB_NAME, timeout,
						callback_timeout, NULL, &ts);
	if (err)
		return FALSE;
	if (dbus_timeout_get_enabled(timeout))
		teamd_loop_callback_enable(ctx, TIMEOUT_CB_NAME, timeout);
	return TRUE;
}

static void remove_timeout(DBusTimeout *timeout, void *priv)
{
	struct teamd_context *ctx = priv;

	teamd_loop_callback_del(ctx, TIMEOUT_CB_NAME, timeout);
}

static void toggle_timeout(DBusTimeout *timeout, void *priv)
{
	struct teamd_context *ctx = priv;

	if (dbus_timeout_get_enabled(timeout))
		teamd_loop_callback_enable(ctx, TIMEOUT_CB_NAME, timeout);
	else
		teamd_loop_callback_disable(ctx, TIMEOUT_CB_NAME, timeout);
}

static void wakeup_main(void *priv)
{
	struct teamd_context *ctx = priv;

	teamd_run_loop_restart(ctx);
}

struct dispatch_priv {
	int fd_r;
	int fd_w;
	struct teamd_context *ctx;
};

static int callback_dispatch(struct teamd_context *ctx, int events, void *priv)
{
	struct dispatch_priv *dp = priv;
	char byte;
	int err;

	err = read(dp->fd_r, &byte, 1);
	if (err == -1) {
		if (errno != EINTR && errno != EAGAIN) {
			teamd_log_err("dbus: dispatch, read() failed.");
			return -errno;
		}
	} else {
		while (dbus_connection_dispatch(ctx->dbus.con) ==
			DBUS_DISPATCH_DATA_REMAINS);
	}
	return 0;
}

static void wakeup_dispatch(struct dispatch_priv *dp)
{
	int err;

retry:
	err = write(dp->fd_w, "a", 1);
	if (err == -1 && errno == EINTR)
		goto retry;
}

static void dispatch_status(DBusConnection *conn, DBusDispatchStatus status,
			    void *priv) {
	struct dispatch_priv *dp = priv;

	if (status == DBUS_DISPATCH_COMPLETE)
		return;
	wakeup_dispatch(dp);
}

#define DISPATCH_CB_NAME "dbus_dispatch"

static int dispatch_init(struct dispatch_priv **pdp, struct teamd_context *ctx)
{
	struct dispatch_priv *dp;
	int fds[2];
	int err;

	dp = myzalloc(sizeof(*dp));
	if (!dp)
		return -ENOMEM;

	err = pipe(fds);
	if (err) {
		err = -errno;
		goto free_dp;
	}
	dp->fd_r = fds[0];
	dp->fd_w = fds[1];
	dp->ctx = ctx;

	err = teamd_loop_callback_fd_add(ctx, DISPATCH_CB_NAME, dp,
					 callback_dispatch,
					 dp->fd_r, TEAMD_LOOP_FD_EVENT_READ);
	teamd_loop_callback_enable(ctx, DISPATCH_CB_NAME, dp);
	if (err)
		goto close_pipe;
	*pdp = dp;
	return 0;
close_pipe:
	close(dp->fd_w);
	close(dp->fd_r);
free_dp:
	free(dp);
	return err;
}

static void dispatch_exit(void *priv)
{
	struct dispatch_priv *dp = priv;

	teamd_loop_callback_del(dp->ctx, DISPATCH_CB_NAME, dp);
	close(dp->fd_w);
	close(dp->fd_r);
	free(dp);
}

static int teamd_dbus_mainloop_init(struct teamd_context *ctx)
{
	struct dispatch_priv *dp = NULL; /* gcc needs this initialized */
	int err;

	err = dispatch_init(&dp, ctx);
	if (err) {
		teamd_log_err("dbus: failed to init dispatch.");
		return err;
	}
	dbus_connection_set_dispatch_status_function(ctx->dbus.con,
						     dispatch_status,
						     dp, dispatch_exit);
	if (dbus_connection_set_watch_functions(ctx->dbus.con, add_watch,
						remove_watch, toggle_watch,
						ctx, NULL) == FALSE) {
		teamd_log_err("dbus: failed to init watch functions.");
		return -EINVAL;
	}
	if (dbus_connection_set_timeout_functions(ctx->dbus.con, add_timeout,
						  remove_timeout,
						  toggle_timeout,
						  ctx, NULL) == FALSE) {
		teamd_log_err("dbus: failed to init timeout functions.");
		return -EINVAL;
	}
	dbus_connection_set_wakeup_main_function(ctx->dbus.con, wakeup_main,
						 ctx, NULL);
	/* Do initial dispatch for early messages */
	wakeup_dispatch(dp);
	return 0;
}

int teamd_dbus_init(struct teamd_context *ctx)
{
	int err;
	char *id;

	if (!ctx->dbus.enabled)
		return 0;
	err = teamd_dbus_con_init(ctx);
	if (err)
		return err;
	err = teamd_dbus_iface_init(ctx);
	if (err)
		goto con_fini;
	err = teamd_dbus_mainloop_init(ctx);
	if (err)
		goto iface_fini;
	id = dbus_connection_get_server_id(ctx->dbus.con),
	teamd_log_dbg("dbus: connected to %s with name %s", id,
		      dbus_bus_get_unique_name(ctx->dbus.con));
	dbus_free(id);
	return 0;
iface_fini:
	teamd_dbus_iface_fini(ctx);
con_fini:
	teamd_dbus_con_fini(ctx);
	return err;
}

void teamd_dbus_fini(struct teamd_context *ctx)
{
	if (!ctx->dbus.enabled)
		return;
	teamd_dbus_iface_fini(ctx);
	teamd_dbus_con_fini(ctx);
}

int teamd_dbus_expose_name(struct teamd_context *ctx)
{
	DBusError error;
	int err;
	char *service_name;

	if (!ctx->dbus.enabled)
		return 0;

	err = asprintf(&service_name, TEAMD_DBUS_SERVICE ".%s",
		       ctx->team_devname);
	if (err == -1)
		return -errno;

	dbus_error_init(&error);
	err = dbus_bus_request_name(ctx->dbus.con, service_name, 0,
				    &error);
	if (err == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
		teamd_log_dbg("dbus: have name %s", service_name);
		err = 0;
	} else if (dbus_error_is_set(&error)) {
		teamd_log_err("dbus: Failed to acquire %s: %s: %s",
			      service_name, error.name, error.message);
		err = -EINVAL;
	} else {
		teamd_log_err("dbus: name %s already taken.", service_name);
		err = -EBUSY;
	}
	dbus_error_free(&error);
	free(service_name);
	return err;
}

#endif /* ENABLE_DBUS */
