/*
 * dLeyna
 *
 * Copyright (C) 2012-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.
 *
 * Mark Ryan <mark.d.ryan@intel.com>
 *
 */

#include <string.h>

#include <libgssdp/gssdp-resource-browser.h>
#include <libgupnp/gupnp-control-point.h>
#include <libgupnp/gupnp-error.h>

#include <libdleyna/core/error.h>
#include <libdleyna/core/log.h>
#include <libdleyna/core/service-task.h>

#include "async.h"
#include "device.h"
#include "interface.h"
#include "path.h"
#include "search.h"
#include "sort.h"
#include "upnp.h"

#define DLS_DMS_DEVICE_TYPE "urn:schemas-upnp-org:device:MediaServer:"

struct dls_upnp_t_ {
	dleyna_connector_id_t connection;
	const dleyna_connector_dispatch_cb_t *interface_info;
	GHashTable *filter_map;
	GHashTable *property_map;
	dls_upnp_callback_t found_server;
	dls_upnp_callback_t lost_server;
	GUPnPContextManager *context_manager;
	void *user_data;
	GHashTable *device_udn_map;
	GHashTable *sleeping_device_udn_map;
	GHashTable *device_uc_map;
};

/* Private structure used in service task */
typedef struct prv_device_new_ct_t_ prv_device_new_ct_t;
struct prv_device_new_ct_t_ {
	dls_upnp_t *upnp;
	char *udn;
	gchar *ip_address;
	dls_device_t *device;
	const dleyna_task_queue_key_t *queue_id;
};

static void prv_device_new_free(prv_device_new_ct_t *priv_t)
{
	if (priv_t) {
		g_free(priv_t->udn);
		g_free(priv_t->ip_address);
		g_free(priv_t);
	}
}

static void prv_device_chain_end(gboolean cancelled, gpointer data)
{
	dls_device_t *device;
	prv_device_new_ct_t *priv_t = (prv_device_new_ct_t *)data;

	DLEYNA_LOG_DEBUG("Enter");

	device = priv_t->device;

	if (cancelled)
		goto on_clear;

	DLEYNA_LOG_DEBUG("Notify new server available: %s", device->path);
	g_hash_table_insert(priv_t->upnp->device_udn_map, g_strdup(priv_t->udn),
			    device);
	priv_t->upnp->found_server(device->path, priv_t->upnp->user_data);

on_clear:

	g_hash_table_remove(priv_t->upnp->device_uc_map, priv_t->udn);

	if (cancelled)
		dls_device_delete(device);

	prv_device_new_free(priv_t);

	DLEYNA_LOG_DEBUG_NL();
}

static void prv_device_context_switch_end(gboolean cancelled, gpointer data)
{
	prv_device_new_ct_t *priv_t = (prv_device_new_ct_t *)data;

	DLEYNA_LOG_DEBUG("Enter");

	g_hash_table_remove(priv_t->upnp->device_uc_map, priv_t->udn);
	prv_device_new_free(priv_t);

	DLEYNA_LOG_DEBUG("Exit");
}

static const dleyna_task_queue_key_t *prv_create_device_queue(
						prv_device_new_ct_t **priv_t)
{
	const dleyna_task_queue_key_t *queue_id;

	*priv_t = g_new0(prv_device_new_ct_t, 1);

	queue_id = dleyna_task_processor_add_queue(
			dls_server_get_task_processor(),
			dleyna_service_task_create_source(),
			DLS_SERVER_SINK,
			DLEYNA_TASK_QUEUE_FLAG_AUTO_REMOVE,
			dleyna_service_task_process_cb,
			dleyna_service_task_cancel_cb,
			dleyna_service_task_delete_cb);
	dleyna_task_queue_set_finally(queue_id, prv_device_chain_end);
	dleyna_task_queue_set_user_data(queue_id, *priv_t);


	return queue_id;
}

static void prv_update_device_context(prv_device_new_ct_t *priv_t,
				      dls_upnp_t *upnp, const char *udn,
				      dls_device_t *device,
				      const gchar *ip_address,
				      const dleyna_task_queue_key_t *queue_id)
{
	priv_t->upnp = upnp;
	priv_t->udn = g_strdup(udn);
	priv_t->ip_address = g_strdup(ip_address);
	priv_t->queue_id = queue_id;
	priv_t->device = device;

	g_hash_table_insert(upnp->device_uc_map, g_strdup(udn), priv_t);
}

static GUPnPDeviceInfo *prv_lookup_dms_child_device(GUPnPDeviceInfo *proxy)
{
	GList *child_devices;
	GList *next;
	const gchar *device_type;
	GUPnPDeviceInfo *info = NULL;
	GUPnPDeviceInfo *child_info = NULL;

	child_devices = gupnp_device_info_list_device_types(proxy);

	next = child_devices;
	while (next != NULL) {
		device_type = (gchar *)next->data;

		child_info = gupnp_device_info_get_device(proxy, device_type);

		if (g_str_has_prefix(device_type, DLS_DMS_DEVICE_TYPE)) {
			break;
		} else {
			info = prv_lookup_dms_child_device(child_info);

			g_object_unref(child_info);
			child_info = NULL;

			if (info != NULL) {
				child_info = info;

				break;
			}
		}

		next = g_list_next(next);
	}

	g_list_free_full(child_devices, (GDestroyNotify)g_free);

	return child_info;
}

static void prv_device_available_cb(GUPnPControlPoint *cp,
				    GUPnPDeviceProxy *proxy,
				    gpointer user_data)
{
	dls_upnp_t *upnp = user_data;
	const char *udn;
	dls_device_t *device;
	const gchar *ip_address;
	dls_device_context_t *context;
	const dleyna_task_queue_key_t *queue_id;
	unsigned int i;
	prv_device_new_ct_t *priv_t;
	GUPnPDeviceInfo *device_proxy = (GUPnPDeviceInfo *)proxy;
	GUPnPDeviceInfo *device_info = NULL;
	const gchar *device_type;
	gboolean subscribe = FALSE;
	gpointer key;
	gpointer val;

	udn = gupnp_device_info_get_udn(device_proxy);

	ip_address = gssdp_client_get_host_ip(
		GSSDP_CLIENT(gupnp_control_point_get_context(cp)));

	if (!udn || !ip_address)
		goto on_error;

	DLEYNA_LOG_DEBUG("UDN %s", udn);
	DLEYNA_LOG_DEBUG("IP Address %s", ip_address);

	device_type = gupnp_device_info_get_device_type(device_proxy);

	if (!g_str_has_prefix(device_type, DLS_DMS_DEVICE_TYPE)) {
		device_info = prv_lookup_dms_child_device(device_proxy);

		if (device_info == NULL)
			goto on_error;
	} else {
		device_info = device_proxy;
	}

	device = g_hash_table_lookup(upnp->device_udn_map, udn);

	if (!device) {
		device = g_hash_table_lookup(upnp->sleeping_device_udn_map,
					     udn);

		if (device != NULL) {
			if (g_hash_table_lookup_extended(
						  upnp->sleeping_device_udn_map,
						  udn,
						  &key,
						  &val)) {
				g_hash_table_steal(
						upnp->sleeping_device_udn_map,
						udn);

				g_free(key);
			}

			g_hash_table_insert(upnp->device_udn_map, g_strdup(udn),
					    device);

			if (device->wake_on_timeout_id) {
				DLEYNA_LOG_DEBUG("Stop WAKE-ON watcher...");

				(void) g_source_remove(
						 device->wake_on_timeout_id);
				device->wake_on_timeout_id = 0;
			}
			dls_device_delete_context(device->sleeping_context);
			device->sleeping_context = NULL;
			device->sleeping = FALSE;
			subscribe = TRUE;
		}
	}

	if (!device) {
		priv_t = g_hash_table_lookup(upnp->device_uc_map, udn);

		if (priv_t)
			device = priv_t->device;
	}

	if (!device) {
		DLEYNA_LOG_DEBUG("Device not found. Adding");
		DLEYNA_LOG_DEBUG_NL();

		queue_id = prv_create_device_queue(&priv_t);

		device = dls_device_new(upnp->connection,
					proxy,
					device_info,
					ip_address,
					upnp->interface_info,
					upnp->property_map, 
					udn,
					queue_id);

		prv_update_device_context(priv_t, upnp, udn, device, ip_address,
					  queue_id);
	} else {
		DLEYNA_LOG_DEBUG("Device Found");

		for (i = 0; i < device->contexts->len; ++i) {
			context = g_ptr_array_index(device->contexts, i);
			if (!strcmp(context->ip_address, ip_address))
				break;
		}

		if (i == device->contexts->len) {
			DLEYNA_LOG_DEBUG("Adding Context");
			(void) dls_device_append_new_context(device,
							     ip_address,
							     proxy,
							     device_info);
			if (subscribe)
				dls_device_subscribe_to_service_changes(device);
		}

		DLEYNA_LOG_DEBUG_NL();
	}

on_error:

	return;
}

static gboolean prv_subscribe_to_service_changes(gpointer user_data)
{
	dls_device_t *device = user_data;

	device->timeout_id = 0;
	dls_device_subscribe_to_service_changes(device);

	return FALSE;
}

static void prv_device_unavailable_cb(GUPnPControlPoint *cp,
				      GUPnPDeviceProxy *proxy,
				      gpointer user_data)
{
	dls_upnp_t *upnp = user_data;
	const char *udn;
	dls_device_t *device;
	const gchar *ip_address;
	unsigned int i;
	dls_device_context_t *context;
	gboolean subscribed;
	gboolean construction_ctx = FALSE;
	gboolean under_construction = FALSE;
	prv_device_new_ct_t *priv_t;
	const dleyna_task_queue_key_t *queue_id;
	dls_device_context_t *lost_context;
	gpointer key;
	gpointer val;

	DLEYNA_LOG_DEBUG("Enter");

	udn = gupnp_device_info_get_udn((GUPnPDeviceInfo *)proxy);

	ip_address = gupnp_context_get_host_ip(
		gupnp_control_point_get_context(cp));

	if (!udn || !ip_address)
		goto on_error;

	DLEYNA_LOG_DEBUG("UDN %s", udn);
	DLEYNA_LOG_DEBUG("IP Address %s", ip_address);

	device = g_hash_table_lookup(upnp->device_udn_map, udn);

	if (!device) {
		priv_t = g_hash_table_lookup(upnp->device_uc_map, udn);

		if (priv_t) {
			device = priv_t->device;
			under_construction = TRUE;
		}
	}

	if (!device) {
		DLEYNA_LOG_WARNING("Device not found. Ignoring");
		goto on_error;
	}

	for (i = 0; i < device->contexts->len; ++i) {
		context = g_ptr_array_index(device->contexts, i);
		if (!strcmp(context->ip_address, ip_address))
			break;
	}

	if (i >= device->contexts->len)
		goto on_error;

	subscribed = (context->cds.subscribed || context->ems.subscribed);
	if (under_construction)
		construction_ctx = !strcmp(context->ip_address,
					   priv_t->ip_address);

	g_ptr_array_set_free_func(device->contexts, NULL);

	lost_context = g_ptr_array_remove_index(device->contexts, i);

	g_ptr_array_set_free_func(device->contexts,
				  (GDestroyNotify)dls_device_delete_context);

	if (device->contexts->len == 0) {
		if (!under_construction) {
			DLEYNA_LOG_DEBUG("Last Context lost.");

			if (!device->sleeping) {
				DLEYNA_LOG_DEBUG("Delete device.");

				upnp->lost_server(device->path,
						  upnp->user_data);

				g_hash_table_remove(upnp->device_udn_map, udn);
			} else {
				DLEYNA_LOG_DEBUG("Persist sleeping device.");

				dleyna_task_processor_remove_queues_for_sink(
						dls_server_get_task_processor(),
						device->path);

				g_hash_table_insert(
						upnp->sleeping_device_udn_map,
						g_strdup(udn),
						device);

				if (g_hash_table_lookup_extended(
							  upnp->device_udn_map,
							  udn,
							  &key,
							  &val)) {
					g_hash_table_steal(upnp->device_udn_map,
							   udn);

					g_free(key);
				}

				device->sleeping_context = lost_context;

				lost_context = NULL;
			}
		} else {
			DLEYNA_LOG_WARNING(
				"Device under construction. Cancelling");

			dleyna_task_processor_cancel_queue(priv_t->queue_id);
		}
	} else if (under_construction && construction_ctx) {
		DLEYNA_LOG_WARNING(
			"Device under construction. Switching context");

		/* Cancel previous contruction task chain */
		g_hash_table_remove(priv_t->upnp->device_uc_map, priv_t->udn);
		dleyna_task_queue_set_finally(priv_t->queue_id,
					      prv_device_context_switch_end);
		dleyna_task_processor_cancel_queue(priv_t->queue_id);

		/* Create a new construction task chain */
		context = dls_device_get_context(device, NULL);
		queue_id = prv_create_device_queue(&priv_t);
		prv_update_device_context(priv_t, upnp, udn, device,
					  context->ip_address, queue_id);

		/* Start tasks from current construction step */
		dls_device_construct(device,
				     context,
				     upnp->connection,
				     upnp->interface_info,
				     upnp->property_map,
				     queue_id);

	} else if (subscribed && !device->timeout_id) {
		DLEYNA_LOG_DEBUG("Subscribe on new context");

		device->timeout_id = g_timeout_add_seconds(1,
				prv_subscribe_to_service_changes,
				device);
	}

	if (lost_context != NULL)
		dls_device_delete_context(lost_context);

on_error:

	DLEYNA_LOG_DEBUG("Exit");
	DLEYNA_LOG_DEBUG_NL();

	return;
}

static void prv_on_context_available(GUPnPContextManager *context_manager,
				     GUPnPContext *context,
				     gpointer user_data)
{
	dls_upnp_t *upnp = user_data;
	GUPnPControlPoint *cp;

	cp = gupnp_control_point_new(
		context,
		"upnp:rootdevice");

	g_signal_connect(cp, "device-proxy-available",
			 G_CALLBACK(prv_device_available_cb), upnp);

	g_signal_connect(cp, "device-proxy-unavailable",
			 G_CALLBACK(prv_device_unavailable_cb), upnp);

	gssdp_resource_browser_set_active(GSSDP_RESOURCE_BROWSER(cp), TRUE);
	gupnp_context_manager_manage_control_point(upnp->context_manager, cp);
	g_object_unref(cp);
}

dls_upnp_t *dls_upnp_new(dleyna_connector_id_t connection,
			 guint port,
			 const dleyna_connector_dispatch_cb_t *dispatch_table,
			 dls_upnp_callback_t found_server,
			 dls_upnp_callback_t lost_server,
			 void *user_data)
{
	dls_upnp_t *upnp = g_new0(dls_upnp_t, 1);

	upnp->connection = connection;
	upnp->interface_info = dispatch_table;
	upnp->user_data = user_data;
	upnp->found_server = found_server;
	upnp->lost_server = lost_server;

	upnp->device_udn_map = g_hash_table_new_full(g_str_hash, g_str_equal,
						g_free,
						dls_device_delete);

	upnp->sleeping_device_udn_map = g_hash_table_new_full(g_str_hash,
						g_str_equal,
						g_free,
						dls_device_delete);

	upnp->device_uc_map = g_hash_table_new_full(g_str_hash, g_str_equal,
						    g_free, NULL);

	dls_prop_maps_new(&upnp->property_map, &upnp->filter_map);

	upnp->context_manager = gupnp_context_manager_create(port);

	g_signal_connect(upnp->context_manager, "context-available",
			 G_CALLBACK(prv_on_context_available),
			 upnp);

	return upnp;
}

void dls_upnp_delete(dls_upnp_t *upnp)
{
	if (upnp) {
		g_signal_handlers_disconnect_by_func (G_OBJECT (upnp->context_manager),
						      G_CALLBACK(prv_on_context_available),
						      upnp);
		g_object_unref(upnp->context_manager);
		g_hash_table_unref(upnp->property_map);
		g_hash_table_unref(upnp->filter_map);
		g_hash_table_unref(upnp->device_udn_map);
		g_hash_table_unref(upnp->sleeping_device_udn_map);
		g_hash_table_unref(upnp->device_uc_map);
		g_free(upnp);
	}
}

GVariant *dls_upnp_get_device_ids(dls_upnp_t *upnp)
{
	GVariantBuilder vb;
	GHashTableIter iter;
	gpointer value;
	dls_device_t *device;
	GVariant *retval;

	DLEYNA_LOG_DEBUG("Enter");

	g_variant_builder_init(&vb, G_VARIANT_TYPE("ao"));

	g_hash_table_iter_init(&iter, upnp->device_udn_map);
	while (g_hash_table_iter_next(&iter, NULL, &value)) {
		device = value;
		DLEYNA_LOG_DEBUG("Have device %s", device->path);
		g_variant_builder_add(&vb, "o", device->path);
	}

	g_hash_table_iter_init(&iter, upnp->sleeping_device_udn_map);
	while (g_hash_table_iter_next(&iter, NULL, &value)) {
		device = value;
		DLEYNA_LOG_DEBUG("Have sleeping device %s", device->path);
		g_variant_builder_add(&vb, "o", device->path);
	}

	retval = g_variant_ref_sink(g_variant_builder_end(&vb));

	DLEYNA_LOG_DEBUG("Exit");

	return retval;
}

GHashTable *dls_upnp_get_device_udn_map(dls_upnp_t *upnp)
{
	return upnp->device_udn_map;
}

GHashTable *dls_upnp_get_sleeping_device_udn_map(dls_upnp_t *upnp)
{
	return upnp->sleeping_device_udn_map;
}

void dls_upnp_delete_sleeping_device(dls_upnp_t *upnp, dls_device_t *device)
{
	const char *udn;
	dls_device_context_t *ctx = device->sleeping_context;

	udn = gupnp_device_info_get_udn((GUPnPDeviceInfo *)ctx->device_proxy);

	upnp->lost_server(device->path, upnp->user_data);

	g_hash_table_remove(upnp->sleeping_device_udn_map, udn);
}

void dls_upnp_get_children(dls_upnp_t *upnp, dls_client_t *client,
			   dls_task_t *task,
			   dls_upnp_task_complete_t cb)
{
	dls_async_task_t *cb_data = (dls_async_task_t *)task;
	dls_async_bas_t *cb_task_data;
	gchar *upnp_filter = NULL;
	gchar *sort_by = NULL;

	DLEYNA_LOG_DEBUG("Enter");

	DLEYNA_LOG_DEBUG("Path: %s", task->target.path);
	DLEYNA_LOG_DEBUG("Start: %u", task->ut.get_children.start);
	DLEYNA_LOG_DEBUG("Count: %u", task->ut.get_children.count);

	cb_data->cb = cb;
	cb_task_data = &cb_data->ut.bas;

	cb_task_data->filter_mask =
		dls_props_parse_filter(upnp->filter_map,
				       task->ut.get_children.filter,
				       &upnp_filter);

	DLEYNA_LOG_DEBUG("Filter Mask 0x%"G_GUINT64_FORMAT"x",
			 cb_task_data->filter_mask);

	sort_by = dls_sort_translate_sort_string(upnp->filter_map,
						 task->ut.get_children.sort_by);
	if (!sort_by) {
		DLEYNA_LOG_WARNING("Invalid Sort Criteria");

		cb_data->error = g_error_new(DLEYNA_SERVER_ERROR,
					     DLEYNA_ERROR_BAD_QUERY,
					     "Sort Criteria are not valid");
		goto on_error;
	}

	DLEYNA_LOG_DEBUG("Sort By %s", sort_by);

	cb_task_data->protocol_info = client->protocol_info;

	dls_device_get_children(client, task, upnp_filter, sort_by);

on_error:

	if (!cb_data->action)
		(void) g_idle_add(dls_async_task_complete, cb_data);

	g_free(sort_by);
	g_free(upnp_filter);

	DLEYNA_LOG_DEBUG("Exit with %s", !cb_data->action ? "FAIL" : "SUCCESS");
}

void dls_upnp_get_all_props(dls_upnp_t *upnp, dls_client_t *client,
			    dls_task_t *task,
			    dls_upnp_task_complete_t cb)
{
	gboolean root_object;
	dls_async_task_t *cb_data = (dls_async_task_t *)task;
	dls_async_get_all_t *cb_task_data;

	DLEYNA_LOG_DEBUG("Enter");

	DLEYNA_LOG_DEBUG("Path: %s", task->target.path);
	DLEYNA_LOG_DEBUG("Interface %s", task->ut.get_prop.interface_name);

	cb_data->cb = cb;
	cb_task_data = &cb_data->ut.get_all;

	root_object = task->target.id[0] == '0' && task->target.id[1] == 0;

	DLEYNA_LOG_DEBUG("Root Object = %d", root_object);

	cb_task_data->protocol_info = client->protocol_info;
	cb_task_data->filter_mask = DLS_UPNP_MASK_ALL_PROPS;
	dls_device_get_all_props(client, task, root_object);

	DLEYNA_LOG_DEBUG("Exit with SUCCESS");
}

void dls_upnp_get_prop(dls_upnp_t *upnp, dls_client_t *client,
		       dls_task_t *task,
		       dls_upnp_task_complete_t cb)
{
	gboolean root_object;
	dls_async_task_t *cb_data = (dls_async_task_t *)task;
	dls_async_get_prop_t *cb_task_data;
	dls_prop_map_t *prop_map;
	dls_task_get_prop_t *task_data;

	DLEYNA_LOG_DEBUG("Enter");

	DLEYNA_LOG_DEBUG("Path: %s", task->target.path);
	DLEYNA_LOG_DEBUG("Interface %s", task->ut.get_prop.interface_name);
	DLEYNA_LOG_DEBUG("Prop.%s", task->ut.get_prop.prop_name);

	task_data = &task->ut.get_prop;
	cb_data->cb = cb;
	cb_task_data = &cb_data->ut.get_prop;

	root_object = task->target.id[0] == '0' && task->target.id[1] == 0;

	DLEYNA_LOG_DEBUG("Root Object = %d", root_object);

	cb_task_data->protocol_info = client->protocol_info;
	prop_map = g_hash_table_lookup(upnp->filter_map, task_data->prop_name);

	dls_device_get_prop(client, task, prop_map, root_object);

	DLEYNA_LOG_DEBUG("Exit with SUCCESS");
}

void dls_upnp_search(dls_upnp_t *upnp, dls_client_t *client,
		     dls_task_t *task,
		     dls_upnp_task_complete_t cb)
{
	gchar *upnp_filter = NULL;
	gchar *upnp_query = NULL;
	gchar *sort_by = NULL;
	dls_async_task_t *cb_data = (dls_async_task_t *)task;
	dls_async_bas_t *cb_task_data;

	DLEYNA_LOG_DEBUG("Enter");

	DLEYNA_LOG_DEBUG("Path: %s", task->target.path);
	DLEYNA_LOG_DEBUG("Query: %s", task->ut.search.query);
	DLEYNA_LOG_DEBUG("Start: %u", task->ut.search.start);
	DLEYNA_LOG_DEBUG("Count: %u", task->ut.search.count);

	cb_data->cb = cb;
	cb_task_data = &cb_data->ut.bas;

	cb_task_data->filter_mask =
		dls_props_parse_filter(upnp->filter_map,
				       task->ut.search.filter, &upnp_filter);

	DLEYNA_LOG_DEBUG("Filter Mask 0x%"G_GUINT64_FORMAT"x",
			 cb_task_data->filter_mask);

	upnp_query = dls_search_translate_search_string(upnp->filter_map,
							task->ut.search.query);
	if (!upnp_query) {
		DLEYNA_LOG_WARNING("Query string is not valid:%s",
				   task->ut.search.query);

		cb_data->error = g_error_new(DLEYNA_SERVER_ERROR,
					     DLEYNA_ERROR_BAD_QUERY,
					     "Query string is not valid.");
		goto on_error;
	}

	DLEYNA_LOG_DEBUG("UPnP Query %s", upnp_query);

	sort_by = dls_sort_translate_sort_string(upnp->filter_map,
						 task->ut.search.sort_by);
	if (!sort_by) {
		DLEYNA_LOG_WARNING("Invalid Sort Criteria");

		cb_data->error = g_error_new(DLEYNA_SERVER_ERROR,
					     DLEYNA_ERROR_BAD_QUERY,
					     "Sort Criteria are not valid");
		goto on_error;
	}

	DLEYNA_LOG_DEBUG("Sort By %s", sort_by);

	cb_task_data->protocol_info = client->protocol_info;

	dls_device_search(client, task, upnp_filter, upnp_query, sort_by);
on_error:

	if (!cb_data->action)
		(void) g_idle_add(dls_async_task_complete, cb_data);

	g_free(sort_by);
	g_free(upnp_query);
	g_free(upnp_filter);

	DLEYNA_LOG_DEBUG("Exit with %s", !cb_data->action ? "FAIL" : "SUCCESS");
}

void dls_upnp_browse_objects(dls_upnp_t *upnp, dls_client_t *client,
			     dls_task_t *task,
			     dls_upnp_task_complete_t cb)
{
	dls_async_task_t *cb_data = (dls_async_task_t *)task;
	dls_async_browse_objects_t *cb_task_data;

	DLEYNA_LOG_DEBUG("Enter");

	cb_data->cb = cb;
	cb_task_data = &cb_data->ut.browse_objects;
	cb_task_data->get_all.protocol_info = client->protocol_info;

	cb_task_data->get_all.filter_mask =
		dls_props_parse_filter(upnp->filter_map,
				       task->ut.browse_objects.filter,
				       &cb_task_data->upnp_filter);

	DLEYNA_LOG_DEBUG("Filter Mask 0x%"G_GUINT64_FORMAT"x",
			 cb_task_data->get_all.filter_mask);

	dls_device_browse_objects(client, task);

	DLEYNA_LOG_DEBUG("Exit");
}

void dls_upnp_get_resource(dls_upnp_t *upnp, dls_client_t *client,
			   dls_task_t *task,
			   dls_upnp_task_complete_t cb)
{
	dls_async_task_t *cb_data = (dls_async_task_t *)task;
	dls_async_get_all_t *cb_task_data;
	gchar *upnp_filter = NULL;

	DLEYNA_LOG_DEBUG("Enter");

	DLEYNA_LOG_DEBUG("Protocol Info: %s ", task->ut.resource.protocol_info);

	cb_data->cb = cb;
	cb_task_data = &cb_data->ut.get_all;

	DLEYNA_LOG_DEBUG("Root Path %s Id %s", task->target.root_path,
			 task->target.id);

	cb_task_data->filter_mask =
		dls_props_parse_filter(upnp->filter_map,
				       task->ut.resource.filter, &upnp_filter);

	DLEYNA_LOG_DEBUG("Filter Mask 0x%"G_GUINT64_FORMAT"x",
			 cb_task_data->filter_mask);

	dls_device_get_resource(client, task, upnp_filter);

	g_free(upnp_filter);

	DLEYNA_LOG_DEBUG("Exit");
}

static gboolean prv_compute_mime_and_class(dls_task_t *task,
					   dls_async_upload_t *cb_task_data,
					   GError **error)
{
	gchar *content_type = NULL;

	if (!g_file_test(task->ut.upload.file_path,
			 G_FILE_TEST_IS_REGULAR | G_FILE_TEST_EXISTS)) {
		DLEYNA_LOG_WARNING(
			"File %s does not exist or is not a regular file",
			task->ut.upload.file_path);

		*error = g_error_new(DLEYNA_SERVER_ERROR,
				     DLEYNA_ERROR_OBJECT_NOT_FOUND,
				     "File %s does not exist or is not a regular file",
				     task->ut.upload.file_path);
		goto on_error;
	}

	content_type = g_content_type_guess(task->ut.upload.file_path, NULL, 0,
					    NULL);

	if (!content_type) {
		DLEYNA_LOG_WARNING("Unable to determine Content Type for %s",
				   task->ut.upload.file_path);

		*error = g_error_new(DLEYNA_SERVER_ERROR, DLEYNA_ERROR_BAD_MIME,
				     "Unable to determine Content Type for %s",
				     task->ut.upload.file_path);
		goto on_error;
	}

	cb_task_data->mime_type = g_content_type_get_mime_type(content_type);
	g_free(content_type);

	if (!cb_task_data->mime_type) {
		DLEYNA_LOG_WARNING("Unable to determine MIME Type for %s",
				   task->ut.upload.file_path);

		*error = g_error_new(DLEYNA_SERVER_ERROR, DLEYNA_ERROR_BAD_MIME,
				     "Unable to determine MIME Type for %s",
				     task->ut.upload.file_path);
		goto on_error;
	}

	if (g_content_type_is_a(cb_task_data->mime_type, "image/*")) {
		cb_task_data->object_class = "object.item.imageItem";
	} else if (g_content_type_is_a(cb_task_data->mime_type, "audio/*")) {
		cb_task_data->object_class = "object.item.audioItem";
	} else if (g_content_type_is_a(cb_task_data->mime_type, "video/*")) {
		cb_task_data->object_class = "object.item.videoItem";
	} else {
		DLEYNA_LOG_WARNING("Unsupported MIME Type %s",
				   cb_task_data->mime_type);

		*error = g_error_new(DLEYNA_SERVER_ERROR, DLEYNA_ERROR_BAD_MIME,
				     "Unsupported MIME Type %s",
				     cb_task_data->mime_type);
		goto on_error;
	}

	return TRUE;

on_error:

	return FALSE;
}

void dls_upnp_upload_to_any(dls_upnp_t *upnp, dls_client_t *client,
			    dls_task_t *task,
			    dls_upnp_task_complete_t cb)
{
	dls_async_task_t *cb_data = (dls_async_task_t *)task;
	dls_async_upload_t *cb_task_data;

	DLEYNA_LOG_DEBUG("Enter");

	cb_data->cb = cb;
	cb_task_data = &cb_data->ut.upload;

	DLEYNA_LOG_DEBUG("Root Path %s Id %s", task->target.root_path,
			 task->target.id);

	if (strcmp(task->target.id, "0")) {
		DLEYNA_LOG_WARNING("Bad path %s", task->target.path);

		cb_data->error =
			g_error_new(DLEYNA_SERVER_ERROR, DLEYNA_ERROR_BAD_PATH,
				    "UploadToAnyContainer must be executed on a root path");
		goto on_error;
	}

	if (!prv_compute_mime_and_class(task, cb_task_data, &cb_data->error))
		goto on_error;

	DLEYNA_LOG_DEBUG("MIME Type %s", cb_task_data->mime_type);
	DLEYNA_LOG_DEBUG("Object class %s", cb_task_data->object_class);

	dls_device_upload(client, task, "DLNA.ORG_AnyContainer");

on_error:

	if (!cb_data->action)
		(void) g_idle_add(dls_async_task_complete, cb_data);

	DLEYNA_LOG_DEBUG("Exit");
}

void dls_upnp_upload(dls_upnp_t *upnp, dls_client_t *client, dls_task_t *task,
		     dls_upnp_task_complete_t cb)
{
	dls_async_task_t *cb_data = (dls_async_task_t *)task;
	dls_async_upload_t *cb_task_data;

	DLEYNA_LOG_DEBUG("Enter");

	cb_data->cb = cb;
	cb_task_data = &cb_data->ut.upload;

	if (!prv_compute_mime_and_class(task, cb_task_data, &cb_data->error))
		goto on_error;

	DLEYNA_LOG_DEBUG("MIME Type %s", cb_task_data->mime_type);
	DLEYNA_LOG_DEBUG("Object class %s", cb_task_data->object_class);

	dls_device_upload(client, task, task->target.id);

on_error:

	if (!cb_data->action)
		(void) g_idle_add(dls_async_task_complete, cb_data);

	DLEYNA_LOG_DEBUG("Exit");
}

void dls_upnp_get_upload_status(dls_upnp_t *upnp, dls_task_t *task)
{
	GError *error = NULL;

	DLEYNA_LOG_DEBUG("Enter");

	DLEYNA_LOG_DEBUG("Root Path %s Id %s", task->target.root_path,
			 task->target.id);

	if (strcmp(task->target.id, "0")) {
		DLEYNA_LOG_WARNING("Bad path %s", task->target.path);

		error = g_error_new(DLEYNA_SERVER_ERROR, DLEYNA_ERROR_BAD_PATH,
				    "GetUploadStatus must be executed on a root path");
		goto on_error;
	}

	(void) dls_device_get_upload_status(task, &error);

on_error:

	if (error) {
		dls_task_fail(task, error);
		g_error_free(error);
	} else {
		dls_task_complete(task);
	}

	DLEYNA_LOG_DEBUG("Exit");
}

void dls_upnp_get_upload_ids(dls_upnp_t *upnp, dls_task_t *task)
{
	GError *error = NULL;

	DLEYNA_LOG_DEBUG("Enter");

	DLEYNA_LOG_DEBUG("Root Path %s Id %s", task->target.root_path,
			 task->target.id);

	if (strcmp(task->target.id, "0")) {
		DLEYNA_LOG_WARNING("Bad path %s", task->target.path);

		error = g_error_new(DLEYNA_SERVER_ERROR, DLEYNA_ERROR_BAD_PATH,
				    "GetUploadIDs must be executed on a root path");
		goto on_error;
	}

	 dls_device_get_upload_ids(task);

on_error:

	if (error) {
		dls_task_fail(task, error);
		g_error_free(error);
	} else {
		dls_task_complete(task);
	}

	DLEYNA_LOG_DEBUG("Exit");
}

void dls_upnp_cancel_upload(dls_upnp_t *upnp, dls_task_t *task)
{
	GError *error = NULL;

	DLEYNA_LOG_DEBUG("Enter");

	DLEYNA_LOG_DEBUG("Root Path %s Id %s", task->target.root_path,
			 task->target.id);

	if (strcmp(task->target.id, "0")) {
		DLEYNA_LOG_WARNING("Bad path %s", task->target.path);

		error = g_error_new(DLEYNA_SERVER_ERROR, DLEYNA_ERROR_BAD_PATH,
				    "CancelUpload must be executed on a root path");
		goto on_error;
	}

	(void) dls_device_cancel_upload(task, &error);

on_error:

	if (error) {
		dls_task_fail(task, error);
		g_error_free(error);
	} else {
		dls_task_complete(task);
	}

	DLEYNA_LOG_DEBUG("Exit");
}

void dls_upnp_delete_object(dls_upnp_t *upnp, dls_client_t *client,
			    dls_task_t *task,
			    dls_upnp_task_complete_t cb)
{
	dls_async_task_t *cb_data = (dls_async_task_t *)task;

	DLEYNA_LOG_DEBUG("Enter");

	cb_data->cb = cb;

	DLEYNA_LOG_DEBUG("Root Path %s Id %s", task->target.root_path,
			 task->target.id);

	dls_device_delete_object(client, task);

	DLEYNA_LOG_DEBUG("Exit");
}

void dls_upnp_create_container(dls_upnp_t *upnp, dls_client_t *client,
			       dls_task_t *task,
			       dls_upnp_task_complete_t cb)
{
	dls_async_task_t *cb_data = (dls_async_task_t *)task;

	DLEYNA_LOG_DEBUG("Enter");

	cb_data->cb = cb;

	DLEYNA_LOG_DEBUG("Root Path %s Id %s", task->target.root_path,
			 task->target.id);

	dls_device_create_container(client, task, task->target.id);

	if (!cb_data->action)
		(void) g_idle_add(dls_async_task_complete, cb_data);

	DLEYNA_LOG_DEBUG("Exit");
}

void dls_upnp_create_container_in_any(dls_upnp_t *upnp, dls_client_t *client,
				      dls_task_t *task,
				      dls_upnp_task_complete_t cb)
{
	dls_async_task_t *cb_data = (dls_async_task_t *)task;

	DLEYNA_LOG_DEBUG("Enter");

	cb_data->cb = cb;

	DLEYNA_LOG_DEBUG("Root Path %s Id %s", task->target.root_path,
			 task->target.id);

	if (strcmp(task->target.id, "0")) {
		DLEYNA_LOG_WARNING("Bad path %s", task->target.path);

		cb_data->error =
			g_error_new(DLEYNA_SERVER_ERROR, DLEYNA_ERROR_BAD_PATH,
				    "CreateContainerInAnyContainer must be executed on a root path");
		goto on_error;
	}

	dls_device_create_container(client, task, "DLNA.ORG_AnyContainer");

on_error:

	if (!cb_data->action)
		(void) g_idle_add(dls_async_task_complete, cb_data);

	DLEYNA_LOG_DEBUG("Exit");
}

void dls_upnp_update_object(dls_upnp_t *upnp, dls_client_t *client,
			    dls_task_t *task,
			    dls_upnp_task_complete_t cb)
{
	dls_async_task_t *cb_data = (dls_async_task_t *)task;
	dls_async_update_t *cb_task_data;
	dls_upnp_prop_mask mask;
	gchar *upnp_filter = NULL;
	dls_task_update_t *task_data;

	DLEYNA_LOG_DEBUG("Enter");

	cb_data->cb = cb;
	cb_task_data = &cb_data->ut.update;
	task_data = &task->ut.update;

	DLEYNA_LOG_DEBUG("Root Path %s Id %s", task->target.root_path,
			 task->target.id);

	if (!dls_props_parse_update_filter(upnp->filter_map,
					   task_data->to_add_update,
					   task_data->to_delete,
					   &mask, &upnp_filter)) {
		DLEYNA_LOG_WARNING("Invalid Parameter");

		cb_data->error = g_error_new(DLEYNA_SERVER_ERROR,
					     DLEYNA_ERROR_OPERATION_FAILED,
					     "Invalid Parameter");
		goto on_error;
	}

	cb_task_data->map = upnp->filter_map;

	DLEYNA_LOG_DEBUG("Filter = %s", upnp_filter);
	DLEYNA_LOG_DEBUG("Mask = 0x%"G_GUINT64_FORMAT"x", mask);

	if (mask == 0) {
		DLEYNA_LOG_WARNING("Empty Parameters");

		cb_data->error = g_error_new(DLEYNA_SERVER_ERROR,
					     DLEYNA_ERROR_OPERATION_FAILED,
					     "Empty Parameters");

		goto on_error;
	}

	dls_device_update_object(client, task, upnp_filter);

on_error:

	g_free(upnp_filter);

	if (!cb_data->action)
		(void) g_idle_add(dls_async_task_complete, cb_data);

	DLEYNA_LOG_DEBUG("Exit");
}

void dls_upnp_get_object_metadata(dls_upnp_t *upnp, dls_client_t *client,
				  dls_task_t *task, dls_upnp_task_complete_t cb)
{
	dls_async_task_t *cb_data = (dls_async_task_t *)task;

	DLEYNA_LOG_DEBUG("Enter");

	cb_data->cb = cb;

	DLEYNA_LOG_DEBUG("Root Path %s Id %s", task->target.root_path,
			 task->target.id);

	dls_device_get_object_metadata(client, task, task->target.id);

	DLEYNA_LOG_DEBUG("Exit");
}

void dls_upnp_create_reference(dls_upnp_t *upnp, dls_client_t *client,
			       dls_task_t *task,
			       dls_upnp_task_complete_t cb)
{
	dls_async_task_t *cb_data = (dls_async_task_t *)task;

	DLEYNA_LOG_DEBUG("Enter");

	cb_data->cb = cb;

	DLEYNA_LOG_DEBUG("Root Path: %s - Id: %s", task->target.root_path,
			 task->target.id);

	dls_device_create_reference(client, task);

	DLEYNA_LOG_DEBUG("Exit");

	return;
}

void dls_upnp_get_icon(dls_upnp_t *upnp, dls_client_t *client,
		       dls_task_t *task,
		       dls_upnp_task_complete_t cb)
{
	dls_async_task_t *cb_data = (dls_async_task_t *)task;

	DLEYNA_LOG_DEBUG("Enter");

	cb_data->cb = cb;

	dls_device_get_icon(client, task);

	DLEYNA_LOG_DEBUG("Exit");
}

void dls_upnp_wake(dls_upnp_t *upnp, dls_client_t *client,
		   dls_task_t *task,
		   dls_upnp_task_complete_t cb)
{
	dls_async_task_t *cb_data = (dls_async_task_t *)task;

	DLEYNA_LOG_DEBUG("Enter");

	cb_data->cb = cb;

	dls_device_wake(client, task);

	DLEYNA_LOG_DEBUG("Exit");
}

void dls_upnp_unsubscribe(dls_upnp_t *upnp)
{
	GHashTableIter iter;
	gpointer value;
	dls_device_t *device;

	DLEYNA_LOG_DEBUG("Enter");

	g_hash_table_iter_init(&iter, upnp->device_udn_map);
	while (g_hash_table_iter_next(&iter, NULL, &value)) {
		device = value;
		dls_device_unsubscribe(device);
	}

	DLEYNA_LOG_DEBUG("Exit");
}

static gboolean prv_device_uc_find(gpointer key, gpointer value,
				   gpointer user_data)
{
	prv_device_new_ct_t *priv_t = (prv_device_new_ct_t *)value;

	return (priv_t->device == user_data) ? TRUE : FALSE;
}

static gboolean prv_device_find(gpointer key, gpointer value,
				gpointer user_data)
{
	return (value == user_data) ? TRUE : FALSE;
}

gboolean dls_upnp_device_context_exist(dls_device_t *device,
				       dls_device_context_t *context)
{
	gpointer result;
	guint i;
	gboolean found = FALSE;
	dls_upnp_t *upnp = dls_server_get_upnp();

	if (upnp == NULL)
		goto on_exit;

	/* Check if the device still exist */
	result = g_hash_table_find(upnp->device_udn_map, prv_device_find,
				   device);

	if (result == NULL)
		if (g_hash_table_find(upnp->device_uc_map, prv_device_uc_find,
				      device) == NULL)
			goto on_exit;

	/* Search if the context still exist in the device */
	for (i = 0; i < device->contexts->len; ++i) {
		if (g_ptr_array_index(device->contexts, i) == context) {
			found = TRUE;
			break;
		}
	}

on_exit:
	return found;
}

void dls_upnp_rescan(dls_upnp_t *upnp)
{
	DLEYNA_LOG_DEBUG("re-scanning control points");

	gupnp_context_manager_rescan_control_points(upnp->context_manager);
}

GUPnPContextManager *dls_upnp_get_context_manager(dls_upnp_t *upnp)
{
	return upnp->context_manager;
}
