/*
 * dLeyna
 *
 * Copyright (C) 2013-2017 Intel Corporation. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU Lesser General Public License,
 * version 2.1, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope 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 program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Regis Merlino <regis.merlino@intel.com>
 *
 */

#include <gio/gio.h>
#include <string.h>

#include <libdleyna/core/connector.h>
#include <libdleyna/core/error.h>
#include <libdleyna/core/log.h>

typedef struct dleyna_dbus_object_t_ dleyna_dbus_object_t;
struct dleyna_dbus_object_t_ {
	guint id;
	gchar *root_path;
	const dleyna_connector_dispatch_cb_t *dispatch_table;
	guint dispatch_table_size;
	dleyna_connector_interface_filter_cb_t filter_cb;
};

typedef struct dleyna_dbus_call_info_t_ dleyna_dbus_call_info_t;
struct dleyna_dbus_call_info_t_ {
	dleyna_dbus_object_t *object;
	guint interface_index;
};

typedef struct dleyna_dbus_context_t_ dleyna_dbus_context_t;
struct dleyna_dbus_context_t_ {
	GHashTable *objects;
	GHashTable *clients;
	GDBusNodeInfo *root_node_info;
	GDBusNodeInfo *server_node_info;
	guint owner_id;
	GDBusConnection *connection;
	dleyna_connector_connected_cb_t connected_cb;
	dleyna_connector_disconnected_cb_t disconnected_cb;
	dleyna_connector_client_lost_cb_t client_lost_cb;
};

static dleyna_dbus_context_t g_context;

#define DLEYNA_SERVICE "com.intel.dleyna"

static const GDBusErrorEntry g_error_entries[] = {
	{ DLEYNA_ERROR_BAD_PATH, DLEYNA_SERVICE".BadPath" },
	{ DLEYNA_ERROR_OBJECT_NOT_FOUND, DLEYNA_SERVICE".ObjectNotFound" },
	{ DLEYNA_ERROR_BAD_QUERY, DLEYNA_SERVICE".BadQuery" },
	{ DLEYNA_ERROR_OPERATION_FAILED, DLEYNA_SERVICE".OperationFailed" },
	{ DLEYNA_ERROR_BAD_RESULT, DLEYNA_SERVICE".BadResult" },
	{ DLEYNA_ERROR_UNKNOWN_INTERFACE, DLEYNA_SERVICE".UnknownInterface" },
	{ DLEYNA_ERROR_UNKNOWN_PROPERTY, DLEYNA_SERVICE".UnknownProperty" },
	{ DLEYNA_ERROR_DEVICE_NOT_FOUND, DLEYNA_SERVICE".DeviceNotFound" },
	{ DLEYNA_ERROR_DIED, DLEYNA_SERVICE".Died" },
	{ DLEYNA_ERROR_CANCELLED, DLEYNA_SERVICE".Cancelled" },
	{ DLEYNA_ERROR_NOT_SUPPORTED, DLEYNA_SERVICE".NotSupported" },
	{ DLEYNA_ERROR_LOST_OBJECT, DLEYNA_SERVICE".LostObject" },
	{ DLEYNA_ERROR_BAD_MIME, DLEYNA_SERVICE".BadMime" },
	{ DLEYNA_ERROR_HOST_FAILED, DLEYNA_SERVICE".HostFailed" },
	{ DLEYNA_ERROR_IO, DLEYNA_SERVICE".IO" }
};

const dleyna_connector_t *dleyna_connector_get_interface(void);

static void prv_object_method_call(GDBusConnection *conn,
				   const gchar *sender,
				   const gchar *object,
				   const gchar *interface,
				   const gchar *method,
				   GVariant *parameters,
				   GDBusMethodInvocation *invocation,
				   gpointer user_data);

static const GDBusInterfaceVTable g_object_vtable = {
	prv_object_method_call,
	NULL,
	NULL
};

static gchar **prv_subtree_enumerate(GDBusConnection *connection,
				     const gchar *sender,
				     const gchar *object_path,
				     gpointer user_data);

static GDBusInterfaceInfo **prv_subtree_introspect(
	GDBusConnection *connection,
	const gchar *sender,
	const gchar *object_path,
	const gchar *node,
	gpointer user_data);

static const GDBusInterfaceVTable *prv_subtree_dispatch(
	GDBusConnection *connection,
	const gchar *sender,
	const gchar *object_path,
	const gchar *interface_name,
	const gchar *node,
	gpointer *out_user_data,
	gpointer user_data);


static const GDBusSubtreeVTable g_subtree_vtable = {
	prv_subtree_enumerate,
	prv_subtree_introspect,
	prv_subtree_dispatch
};

static void prv_subtree_method_call(GDBusConnection *conn,
				    const gchar *sender,
				    const gchar *object_path,
				    const gchar *interface,
				    const gchar *method,
				    GVariant *parameters,
				    GDBusMethodInvocation *invocation,
				    gpointer user_data);

static const GDBusInterfaceVTable g_subtree_interface_vtable = {
	prv_subtree_method_call,
	NULL,
	NULL
};

static void prv_connector_init_error_domain(GQuark error_quark)
{
	guint index = sizeof(g_error_entries) / sizeof(const GDBusErrorEntry);

	while (index) {
		index--;
		g_dbus_error_register_error(
				error_quark,
				g_error_entries[index].error_code,
				g_error_entries[index].dbus_error_name);
	}
}

static void prv_free_dbus_object(gpointer data)
{
	dleyna_dbus_object_t *object = data;

	g_free(object->root_path);

	g_free(object);
}

static gboolean prv_connector_initialize(const gchar *server_info,
					 const gchar *root_info,
					 GQuark error_quark,
					 gpointer user_data)
{
	gboolean success = TRUE;

	DLEYNA_LOG_DEBUG("Enter");

	memset(&g_context, 0, sizeof(g_context));

	g_context.objects = g_hash_table_new_full(g_direct_hash, g_direct_equal,
						  g_free, prv_free_dbus_object);
	g_context.clients = g_hash_table_new_full(g_str_hash, g_str_equal,
						  g_free, NULL);

	g_context.root_node_info = g_dbus_node_info_new_for_xml(root_info,
								NULL);
	if (!g_context.root_node_info) {
		success = FALSE;
		goto out;
	}

	g_context.server_node_info = g_dbus_node_info_new_for_xml(server_info,
								  NULL);
	if (!g_context.server_node_info) {
		success = FALSE;
		goto out;
	}

	prv_connector_init_error_domain(error_quark);

out:
	DLEYNA_LOG_DEBUG("Exit");

	return success;
}

static void prv_connector_disconnect(void)
{
	if (g_context.owner_id) {
		g_bus_unown_name(g_context.owner_id);
		g_context.owner_id = 0;
	}

}

static void prv_connector_shutdown(void)
{
	DLEYNA_LOG_DEBUG("Enter");

	if (g_context.objects)
		g_hash_table_unref(g_context.objects);

	if (g_context.clients)
		g_hash_table_unref(g_context.clients);

	prv_connector_disconnect();

	g_clear_object(&g_context.connection);

	if (g_context.server_node_info)
		g_dbus_node_info_unref(g_context.server_node_info);

	if (g_context.root_node_info)
		g_dbus_node_info_unref(g_context.root_node_info);

	DLEYNA_LOG_DEBUG("Exit");
}

static void prv_bus_acquired(GDBusConnection *connection, const gchar *name,
			     gpointer user_data)
{
	g_context.connection = g_object_ref (connection);
	g_context.connected_cb((dleyna_connector_id_t)connection);
}

static void prv_name_lost(GDBusConnection *connection, const gchar *name,
			  gpointer user_data)
{
	g_context.disconnected_cb((dleyna_connector_id_t)connection);
}

static void prv_connector_connect(
			const gchar *server_name,
			dleyna_connector_connected_cb_t connected_cb,
			dleyna_connector_disconnected_cb_t disconnected_cb)
{
	DLEYNA_LOG_DEBUG("Enter");

	g_context.connected_cb = connected_cb;
	g_context.disconnected_cb = disconnected_cb;

	g_context.owner_id = g_bus_own_name(G_BUS_TYPE_SESSION,
					    server_name,
					    G_BUS_NAME_OWNER_FLAGS_NONE,
					    prv_bus_acquired, NULL,
					    prv_name_lost, NULL, NULL);

	DLEYNA_LOG_DEBUG("Exit");
}

static void prv_connector_unwatch_client(const gchar *client_name)
{
	guint client_id;

	DLEYNA_LOG_DEBUG("Enter");

	client_id = GPOINTER_TO_UINT(g_hash_table_lookup(g_context.clients,
							 client_name));
	(void) g_hash_table_remove(g_context.clients, client_name);

	g_bus_unwatch_name(client_id);

	DLEYNA_LOG_DEBUG("Exit");
}

static void prv_lost_client(GDBusConnection *connection, const gchar *name,
			    gpointer user_data)
{
	g_context.client_lost_cb(name);
	prv_connector_unwatch_client(name);
}

static gboolean prv_connector_watch_client(const gchar *client_name)
{
	guint watch_id;
	gboolean added = TRUE;

	DLEYNA_LOG_DEBUG("Enter");

	if (g_hash_table_lookup(g_context.clients, client_name)) {
		added = FALSE;
		goto out;
	}

	watch_id = g_bus_watch_name(G_BUS_TYPE_SESSION, client_name,
				      G_BUS_NAME_WATCHER_FLAGS_NONE,
				      NULL, prv_lost_client, NULL,
				      NULL);
	g_hash_table_insert(g_context.clients, g_strdup(client_name),
			    GUINT_TO_POINTER(watch_id));

out:
	DLEYNA_LOG_DEBUG("Exit");

	return added;
}

static void prv_connector_set_client_lost_cb(
				dleyna_connector_client_lost_cb_t lost_cb)
{
	g_context.client_lost_cb = lost_cb;
}

static GDBusInterfaceInfo *prv_find_interface_info(gboolean root,
						   const gchar *interface_name)
{
	GDBusNodeInfo *node;
	GDBusInterfaceInfo **interface;

	node = (root) ? g_context.root_node_info : g_context.server_node_info;

	interface = node->interfaces;
	while (*interface) {
		if (!strcmp(interface_name, (*interface)->name))
			break;
		interface++;
	}

	return  *interface;
}

static void prv_object_method_call(GDBusConnection *conn,
				   const gchar *sender,
				   const gchar *object_path,
				   const gchar *interface,
				   const gchar *method,
				   GVariant *parameters,
				   GDBusMethodInvocation *invocation,
				   gpointer user_data)
{
	dleyna_dbus_object_t *object = user_data;

	object->dispatch_table[0]((dleyna_connector_id_t)conn,
				   sender,
				   object_path,
				   interface,
				   method,
				   parameters,
				   (dleyna_connector_msg_id_t)invocation);
}

static void prv_subtree_method_call(GDBusConnection *conn,
				   const gchar *sender,
				   const gchar *object_path,
				   const gchar *interface,
				   const gchar *method,
				   GVariant *parameters,
				   GDBusMethodInvocation *invocation,
				   gpointer user_data)
{
	dleyna_dbus_call_info_t *call_info = user_data;
	dleyna_connector_dispatch_cb_t callback =
		call_info->object->dispatch_table[call_info->interface_index];

	callback((dleyna_connector_id_t)conn, sender, object_path,
		 interface, method, parameters,
		 (dleyna_connector_msg_id_t)invocation);

	g_free(call_info);
}

static guint prv_connector_publish_object(
			dleyna_connector_id_t connection,
			const gchar *object_path,
			gboolean root,
			const gchar* interface_name,
			const dleyna_connector_dispatch_cb_t *cb_table_1)
{
	guint object_id;
	GDBusInterfaceInfo *info;
	dleyna_dbus_object_t *object;
	guint *object_key;

	DLEYNA_LOG_DEBUG("Enter, path = <%s>", object_path);

	object = g_new0(dleyna_dbus_object_t, 1);

	info = prv_find_interface_info(root, interface_name);
	object_id = g_dbus_connection_register_object(
						(GDBusConnection *)connection,
						object_path,
						info,
						&g_object_vtable,
						object, NULL, NULL);
	if (object_id) {
		object->id = object_id;
		object->dispatch_table = cb_table_1;
		object->dispatch_table_size = 1;

		object_key = g_new(guint, 1);
		*object_key = object_id;
		g_hash_table_insert(g_context.objects, object_key, object);
	} else {
		g_free(object);
	}

	DLEYNA_LOG_DEBUG("Exit, object_id = %u", object_id);

	return object_id;
}

static gchar **prv_subtree_enumerate(GDBusConnection *connection,
				     const gchar *sender,
				     const gchar *object_path,
				     gpointer user_data)
{
	return g_malloc0(sizeof(gchar *));
}

static GDBusInterfaceInfo **prv_subtree_introspect(
	GDBusConnection *connection,
	const gchar *sender,
	const gchar *object_path,
	const gchar *node,
	gpointer user_data)
{
	GDBusInterfaceInfo **retval;
	GDBusInterfaceInfo *info;
	unsigned int i;
	unsigned count = 0;
	const gchar *iface_name;
	dleyna_dbus_object_t *object = user_data;

	retval = g_new0(GDBusInterfaceInfo *, object->dispatch_table_size + 1);

	for (i = 0; i < object->dispatch_table_size; i++) {
		iface_name = g_context.server_node_info->interfaces[i]->name;
		if (object->filter_cb(object_path, node, iface_name)) {
			info = g_context.server_node_info->interfaces[i];
			retval[count++] =  g_dbus_interface_info_ref(info);
		}
	}

	return retval;
}

static const GDBusInterfaceVTable *prv_subtree_dispatch(
	GDBusConnection *connection,
	const gchar *sender,
	const gchar *object_path,
	const gchar *interface_name,
	const gchar *node,
	gpointer *out_user_data,
	gpointer user_data)
{
	const GDBusInterfaceVTable *retval = NULL;
	dleyna_dbus_object_t *object = user_data;
	unsigned int i;
	GDBusInterfaceInfo *info;
	dleyna_dbus_call_info_t *out_call_info;

	for (i = 0; i < object->dispatch_table_size; i++) {
		info = g_context.server_node_info->interfaces[i];
		if (!strcmp(interface_name, info->name))
			break;
	}

	out_call_info = g_new(dleyna_dbus_call_info_t, 1);
	out_call_info->object = object;
	out_call_info->interface_index = i;
	*out_user_data = out_call_info;

	retval = &g_subtree_interface_vtable;

	return retval;
}

static guint prv_connector_publish_subtree(
				dleyna_connector_id_t connection,
				const gchar *object_path,
				const dleyna_connector_dispatch_cb_t *cb_table,
				guint cb_table_size,
				dleyna_connector_interface_filter_cb_t cb)
{
	guint flags;
	guint object_id;
	dleyna_dbus_object_t *object;
	guint *object_key;

	DLEYNA_LOG_DEBUG("Enter, path = <%s>", object_path);

	object = g_new0(dleyna_dbus_object_t, 1);

	flags = G_DBUS_SUBTREE_FLAGS_DISPATCH_TO_UNENUMERATED_NODES;
	object_id = g_dbus_connection_register_subtree(
						(GDBusConnection *)connection,
						object_path,
						&g_subtree_vtable,
						flags,
						object,
						NULL, NULL);

	if (object_id) {
		object->id = object_id;
		object->root_path = g_strdup(object_path);
		object->dispatch_table = cb_table;
		object->dispatch_table_size = cb_table_size;
		object->filter_cb = cb;

		object_key = g_new(guint, 1);
		*object_key = object_id;
		g_hash_table_insert(g_context.objects, object_key, object);
	} else {
		g_free(object);
	}

	DLEYNA_LOG_DEBUG("Exit, object_id = %u", object_id);

	return object_id;
}

static void prv_connector_unpublish_object(dleyna_connector_id_t connection,
					   guint object_id)
{
	DLEYNA_LOG_DEBUG("Enter, object_id = %u", object_id);

	g_dbus_connection_unregister_object((GDBusConnection *)connection,
					    object_id);

	(void) g_hash_table_remove(g_context.objects, &object_id);

	DLEYNA_LOG_DEBUG("Exit");
}

static void prv_connector_unpublish_subtree(dleyna_connector_id_t connection,
					    guint object_id)
{
	DLEYNA_LOG_DEBUG("Enter, object_id = %u", object_id);

	g_dbus_connection_unregister_subtree((GDBusConnection *)connection,
					     object_id);

	(void) g_hash_table_remove(g_context.objects, &object_id);

	DLEYNA_LOG_DEBUG("Exit");
}

static void prv_connector_return_response(dleyna_connector_msg_id_t message_id,
					  GVariant *parameters)
{
	g_dbus_method_invocation_return_value(
					(GDBusMethodInvocation *)message_id,
					parameters);
}

static void prv_connector_return_error(dleyna_connector_msg_id_t message_id,
				       const GError *error)
{
	g_dbus_method_invocation_return_gerror(
					(GDBusMethodInvocation *)message_id,
					error);
}

static gboolean prv_connector_notify(dleyna_connector_id_t connection,
				     const gchar *object_path,
				     const gchar *interface_name,
				     const gchar *notification_name,
				     GVariant *parameters,
				     GError **error)
{
	return g_dbus_connection_emit_signal((GDBusConnection *)connection,
					     NULL,
					     object_path,
					     interface_name,
					     notification_name,
					     parameters,
					     NULL);
}

static const dleyna_connector_t g_dbus_connector = {
	prv_connector_initialize,
	prv_connector_shutdown,
	prv_connector_connect,
	prv_connector_disconnect,
	prv_connector_watch_client,
	prv_connector_unwatch_client,
	prv_connector_set_client_lost_cb,
	prv_connector_publish_object,
	prv_connector_publish_subtree,
	prv_connector_unpublish_object,
	prv_connector_unpublish_subtree,
	prv_connector_return_response,
	prv_connector_return_error,
	prv_connector_notify,
};

const dleyna_connector_t *dleyna_connector_get_interface(void)
{
	return &g_dbus_connector;
}
