// SPDX-License-Identifier: GPL-2.0-or-later
/*
 *
 *  BlueZ - Bluetooth protocol stack for Linux
 *
 *  Copyright (C) 2006-2007  Nokia Corporation
 *  Copyright (C) 2004-2009  Marcel Holtmann <marcel@holtmann.org>
 *  Copyright (C) 2012-2012  Intel Corporation
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#define _GNU_SOURCE
#include <stdlib.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdbool.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>

#include <glib.h>
#include <dbus/dbus.h>

#include "gdbus/gdbus.h"

#include "src/log.h"
#include "src/dbus-common.h"
#include "src/error.h"

#include "player.h"

#define MEDIA_PLAYER_INTERFACE "org.bluez.MediaPlayer1"
#define MEDIA_FOLDER_INTERFACE "org.bluez.MediaFolder1"
#define MEDIA_ITEM_INTERFACE "org.bluez.MediaItem1"

struct player_callback {
	const struct media_player_callback *cbs;
	void *user_data;
};

struct pending_req {
	GDBusPendingPropertySet id;
	const char *key;
	const char *value;
};

struct media_item {
	struct media_player	*player;
	char			*path;		/* Item object path */
	char			*name;		/* Item name */
	player_item_type_t	type;		/* Item type */
	player_folder_type_t	folder_type;	/* Folder type */
	bool			playable;	/* Item playable flag */
	uint64_t		uid;		/* Item uid */
	GHashTable		*metadata;	/* Item metadata */
};

struct media_folder {
	struct media_folder	*parent;
	struct media_item	*item;		/* Folder item */
	uint32_t		number_of_items;/* Number of items */
	GSList			*subfolders;
	GSList			*items;
	DBusMessage		*msg;
};

struct media_player {
	char			*device;	/* Device path */
	char			*name;		/* Player name */
	char			*type;		/* Player type */
	char			*subtype;	/* Player subtype */
	bool			browsable;	/* Player browsing feature */
	bool			searchable;	/* Player searching feature */
	struct media_folder	*scope;		/* Player current scope */
	struct media_folder	*folder;	/* Player current folder */
	struct media_folder	*search;	/* Player search folder */
	struct media_folder	*playlist;	/* Player current playlist */
	char			*path;		/* Player object path */
	GHashTable		*settings;	/* Player settings */
	GHashTable		*track;		/* Player current track */
	char			*status;
	uint32_t		position;
	GTimer			*progress;
	struct player_callback	*cb;
	GSList			*pending;
	GSList			*folders;
	uint16_t		obex_port;
};

static void append_track(void *key, void *value, void *user_data)
{
	DBusMessageIter *dict = user_data;
	const char *strkey = key;

	if (strcasecmp(strkey, "Duration") == 0 ||
			strcasecmp(strkey, "TrackNumber") == 0 ||
			strcasecmp(strkey, "NumberOfTracks") == 0)  {
		uint32_t num = atoi(value);
		dict_append_entry(dict, key, DBUS_TYPE_UINT32, &num);
	} else if (strcasecmp(strkey, "Item") == 0) {
		dict_append_entry(dict, key, DBUS_TYPE_OBJECT_PATH, &value);
	} else {
		dict_append_entry(dict, key, DBUS_TYPE_STRING, &value);
	}
}

static struct pending_req *find_pending(struct media_player *mp,
							const char *key)
{
	GSList *l;

	for (l = mp->pending; l; l = l->next) {
		struct pending_req *p = l->data;

		if (strcasecmp(key, p->key) == 0)
			return p;
	}

	return NULL;
}

static struct pending_req *pending_new(GDBusPendingPropertySet id,
					const char *key, const char *value)
{
	struct pending_req *p;

	p = g_new0(struct pending_req, 1);
	p->id = id;
	p->key = key;
	p->value = value;

	return p;
}

static uint32_t media_player_get_position(struct media_player *mp)
{
	double timedelta;
	uint32_t sec, msec;

	if (g_strcmp0(mp->status, "playing") != 0 ||
						mp->position == UINT32_MAX)
		return mp->position;

	timedelta = g_timer_elapsed(mp->progress, NULL);

	sec = (uint32_t) timedelta;
	msec = (uint32_t) ((timedelta - sec) * 1000);

	return mp->position + sec * 1000 + msec;
}

static gboolean get_position(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_player *mp = data;
	uint32_t position;

	position = media_player_get_position(mp);

	dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &position);

	return TRUE;
}

static gboolean status_exists(const GDBusPropertyTable *property, void *data)
{
	struct media_player *mp = data;

	return mp->status != NULL;
}

static gboolean get_status(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_player *mp = data;

	if (mp->status == NULL)
		return FALSE;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &mp->status);

	return TRUE;
}

static gboolean setting_exists(const GDBusPropertyTable *property, void *data)
{
	struct media_player *mp = data;
	const char *value;

	value = g_hash_table_lookup(mp->settings, property->name);

	return value ? TRUE : FALSE;
}

static gboolean get_setting(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_player *mp = data;
	const char *value;

	value = g_hash_table_lookup(mp->settings, property->name);
	if (value == NULL)
		return FALSE;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &value);

	return TRUE;
}

static void player_set_setting(struct media_player *mp,
					GDBusPendingPropertySet id,
					const char *key, const char *value)
{
	struct player_callback *cb = mp->cb;
	struct pending_req *p;

	if (cb == NULL || cb->cbs->set_setting == NULL) {
		g_dbus_pending_property_error(id,
					ERROR_INTERFACE ".NotSupported",
					"Operation is not supported");
		return;
	}

	p = find_pending(mp, key);
	if (p != NULL) {
		g_dbus_pending_property_error(id,
					ERROR_INTERFACE ".InProgress",
					"Operation already in progress");
		return;
	}

	if (!cb->cbs->set_setting(mp, key, value, cb->user_data)) {
		g_dbus_pending_property_error(id,
					ERROR_INTERFACE ".InvalidArguments",
					"Invalid arguments in method call");
		return;
	}

	p = pending_new(id, key, value);

	mp->pending = g_slist_append(mp->pending, p);
}

static void set_setting(const GDBusPropertyTable *property,
			DBusMessageIter *iter, GDBusPendingPropertySet id,
			void *data)
{
	struct media_player *mp = data;
	const char *value, *current;

	if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRING) {
		g_dbus_pending_property_error(id,
					ERROR_INTERFACE ".InvalidArguments",
					"Invalid arguments in method call");
		return;
	}

	dbus_message_iter_get_basic(iter, &value);

	current = g_hash_table_lookup(mp->settings, property->name);
	if (g_strcmp0(current, value) == 0) {
		g_dbus_pending_property_success(id);
		return;
	}

	player_set_setting(mp, id, property->name, value);
}

static gboolean track_exists(const GDBusPropertyTable *property, void *data)
{
	struct media_player *mp = data;

	return g_hash_table_size(mp->track) != 0;
}

static gboolean get_track(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_player *mp = data;
	DBusMessageIter dict;

	dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY,
					DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
					DBUS_TYPE_STRING_AS_STRING
					DBUS_TYPE_VARIANT_AS_STRING
					DBUS_DICT_ENTRY_END_CHAR_AS_STRING,
					&dict);

	g_hash_table_foreach(mp->track, append_track, &dict);

	dbus_message_iter_close_container(iter, &dict);

	return TRUE;
}

static gboolean get_device(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_player *mp = data;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH,
								&mp->device);

	return TRUE;
}

static gboolean name_exists(const GDBusPropertyTable *property, void *data)
{
	struct media_player *mp = data;

	return mp->name != NULL;
}

static gboolean get_name(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_player *mp = data;

	if (mp->name == NULL)
		return FALSE;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &mp->name);

	return TRUE;
}

static gboolean type_exists(const GDBusPropertyTable *property, void *data)
{
	struct media_player *mp = data;

	return mp->type != NULL;
}

static gboolean get_type(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_player *mp = data;

	if (mp->type == NULL)
		return FALSE;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &mp->type);

	return TRUE;
}

static gboolean subtype_exists(const GDBusPropertyTable *property, void *data)
{
	struct media_player *mp = data;

	return mp->subtype != NULL;
}

static gboolean get_subtype(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_player *mp = data;

	if (mp->subtype == NULL)
		return FALSE;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &mp->subtype);

	return TRUE;
}

static gboolean browsable_exists(const GDBusPropertyTable *property, void *data)
{
	struct media_player *mp = data;

	return mp->folder != NULL;
}

static gboolean get_browsable(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_player *mp = data;
	dbus_bool_t value;

	if (mp->folder == NULL)
		return FALSE;

	value = mp->browsable;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &value);

	return TRUE;
}

static gboolean searchable_exists(const GDBusPropertyTable *property,
								void *data)
{
	struct media_player *mp = data;

	return mp->folder != NULL;
}

static gboolean get_searchable(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_player *mp = data;
	dbus_bool_t value;

	if (mp->folder == NULL)
		return FALSE;

	value = mp->searchable;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &value);

	return TRUE;
}

static gboolean playlist_exists(const GDBusPropertyTable *property,
								void *data)
{
	struct media_player *mp = data;

	return mp->playlist != NULL;
}

static gboolean get_playlist(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_player *mp = data;
	struct media_folder *playlist = mp->playlist;

	if (playlist == NULL)
		return FALSE;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH,
							&playlist->item->path);

	return TRUE;
}

static gboolean obexport_exists(const GDBusPropertyTable *property,
								void *data)
{
	struct media_player *mp = data;

	return mp->obex_port != 0;
}

static gboolean get_obexport(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_player *mp = data;

	if (mp->obex_port == 0)
		return FALSE;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT16,
							&mp->obex_port);

	return TRUE;
}

static DBusMessage *media_player_play(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	struct media_player *mp = data;
	struct player_callback *cb = mp->cb;
	int err;

	if (cb->cbs->play == NULL)
		return btd_error_not_supported(msg);

	err = cb->cbs->play(mp, cb->user_data);
	if (err < 0)
		return btd_error_failed(msg, strerror(-err));

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static DBusMessage *media_player_pause(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	struct media_player *mp = data;
	struct player_callback *cb = mp->cb;
	int err;

	if (cb->cbs->pause == NULL)
		return btd_error_not_supported(msg);

	err = cb->cbs->pause(mp, cb->user_data);
	if (err < 0)
		return btd_error_failed(msg, strerror(-err));

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static DBusMessage *media_player_stop(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	struct media_player *mp = data;
	struct player_callback *cb = mp->cb;
	int err;

	if (cb->cbs->stop == NULL)
		return btd_error_not_supported(msg);

	err = cb->cbs->stop(mp, cb->user_data);
	if (err < 0)
		return btd_error_failed(msg, strerror(-err));

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static DBusMessage *media_player_next(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	struct media_player *mp = data;
	struct player_callback *cb = mp->cb;
	int err;

	if (cb->cbs->next == NULL)
		return btd_error_not_supported(msg);

	err = cb->cbs->next(mp, cb->user_data);
	if (err < 0)
		return btd_error_failed(msg, strerror(-err));

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static DBusMessage *media_player_previous(DBusConnection *conn,
						DBusMessage *msg, void *data)
{
	struct media_player *mp = data;
	struct player_callback *cb = mp->cb;
	int err;

	if (cb->cbs->previous == NULL)
		return btd_error_not_supported(msg);

	err = cb->cbs->previous(mp, cb->user_data);
	if (err < 0)
		return btd_error_failed(msg, strerror(-err));

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static DBusMessage *media_player_fast_forward(DBusConnection *conn,
						DBusMessage *msg, void *data)
{
	struct media_player *mp = data;
	struct player_callback *cb = mp->cb;
	int err;

	if (cb->cbs->fast_forward == NULL)
		return btd_error_not_supported(msg);

	err = cb->cbs->fast_forward(mp, cb->user_data);
	if (err < 0)
		return btd_error_failed(msg, strerror(-err));

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static DBusMessage *media_player_rewind(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	struct media_player *mp = data;
	struct player_callback *cb = mp->cb;
	int err;

	if (cb->cbs->rewind == NULL)
		return btd_error_not_supported(msg);

	err = cb->cbs->rewind(mp, cb->user_data);
	if (err < 0)
		return btd_error_failed(msg, strerror(-err));

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static DBusMessage *media_player_press(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	struct media_player *mp = data;
	struct player_callback *cb = mp->cb;
	int err;
	uint8_t avc_key;

	if (cb->cbs->press == NULL)
		return btd_error_not_supported(msg);

	if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_BYTE, &avc_key,
							DBUS_TYPE_INVALID))
		return btd_error_invalid_args(msg);

	err = cb->cbs->press(mp, avc_key, cb->user_data);
	if (err < 0)
		return btd_error_failed(msg, strerror(-err));

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static DBusMessage *media_player_hold(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	struct media_player *mp = data;
	struct player_callback *cb = mp->cb;
	int err;
	uint8_t avc_key;

	if (cb->cbs->hold == NULL)
		return btd_error_not_supported(msg);

	if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_BYTE, &avc_key,
							DBUS_TYPE_INVALID))
		return btd_error_invalid_args(msg);

	err = cb->cbs->hold(mp, avc_key, cb->user_data);
	if (err < 0)
		return btd_error_failed(msg, strerror(-err));

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static DBusMessage *media_player_release(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	struct media_player *mp = data;
	struct player_callback *cb = mp->cb;
	int err;

	if (cb->cbs->release == NULL)
		return btd_error_not_supported(msg);

	err = cb->cbs->release(mp, cb->user_data);
	if (err < 0)
		return btd_error_failed(msg, strerror(-err));

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static void parse_folder_list(gpointer data, gpointer user_data)
{
	struct media_item *item = data;
	DBusMessageIter *array = user_data;
	DBusMessageIter entry;

	dbus_message_iter_open_container(array, DBUS_TYPE_DICT_ENTRY, NULL,
								&entry);

	dbus_message_iter_append_basic(&entry, DBUS_TYPE_OBJECT_PATH,
								&item->path);

	g_dbus_get_properties(btd_get_dbus_connection(), item->path,
						MEDIA_ITEM_INTERFACE, &entry);

	dbus_message_iter_close_container(array, &entry);
}

void media_player_list_complete(struct media_player *mp, GSList *items,
								int err)
{
	struct media_folder *folder = mp->scope;
	DBusMessage *reply;
	DBusMessageIter iter, array;

	if (folder == NULL || folder->msg == NULL)
		return;

	if (err < 0) {
		reply = btd_error_failed(folder->msg, strerror(-err));
		goto done;
	}

	reply = dbus_message_new_method_return(folder->msg);

	dbus_message_iter_init_append(reply, &iter);

	dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY,
					DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
					DBUS_TYPE_OBJECT_PATH_AS_STRING
					DBUS_TYPE_ARRAY_AS_STRING
					DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
					DBUS_TYPE_STRING_AS_STRING
					DBUS_TYPE_VARIANT_AS_STRING
					DBUS_DICT_ENTRY_END_CHAR_AS_STRING
					DBUS_DICT_ENTRY_END_CHAR_AS_STRING,
					&array);

	g_slist_foreach(items, parse_folder_list, &array);
	dbus_message_iter_close_container(&iter, &array);

done:
	g_dbus_send_message(btd_get_dbus_connection(), reply);
	dbus_message_unref(folder->msg);
	folder->msg = NULL;
}

static struct media_item *
media_player_create_subfolder(struct media_player *mp, const char *name,
								uint64_t uid)
{
	struct media_folder *folder = mp->scope;
	struct media_item *item;
	char *path;

	path = g_strdup_printf("%s/%s", folder->item->name, name);

	DBG("%s", path);

	item = media_player_create_item(mp, path, PLAYER_ITEM_TYPE_FOLDER,
									uid);
	g_free(path);

	return item;
}

void media_player_search_complete(struct media_player *mp, int ret)
{
	struct media_folder *folder = mp->scope;
	struct media_folder *search = mp->search;
	DBusMessage *reply;

	if (folder == NULL || folder->msg == NULL)
		return;

	if (ret < 0) {
		reply = btd_error_failed(folder->msg, strerror(-ret));
		goto done;
	}

	if (search == NULL) {
		search = g_new0(struct media_folder, 1);
		search->item = media_player_create_subfolder(mp, "search", 0);
		mp->search = search;
		mp->folders = g_slist_prepend(mp->folders, search);
	}

	search->number_of_items = ret;

	reply = g_dbus_create_reply(folder->msg,
				DBUS_TYPE_OBJECT_PATH, &search->item->path,
				DBUS_TYPE_INVALID);

done:
	g_dbus_send_message(btd_get_dbus_connection(), reply);
	dbus_message_unref(folder->msg);
	folder->msg = NULL;
}

void media_player_total_items_complete(struct media_player *mp,
							uint32_t num_of_items)
{
	struct media_folder *folder = mp->scope;

	if (folder == NULL || folder->msg == NULL)
		return;

	if (folder->number_of_items != num_of_items) {
		folder->number_of_items = num_of_items;

		g_dbus_emit_property_changed(btd_get_dbus_connection(),
				mp->path, MEDIA_FOLDER_INTERFACE,
				"NumberOfItems");
	}
}

static const GDBusMethodTable media_player_methods[] = {
	{ GDBUS_METHOD("Play", NULL, NULL, media_player_play) },
	{ GDBUS_METHOD("Pause", NULL, NULL, media_player_pause) },
	{ GDBUS_METHOD("Stop", NULL, NULL, media_player_stop) },
	{ GDBUS_METHOD("Next", NULL, NULL, media_player_next) },
	{ GDBUS_METHOD("Previous", NULL, NULL, media_player_previous) },
	{ GDBUS_METHOD("FastForward", NULL, NULL, media_player_fast_forward) },
	{ GDBUS_METHOD("Rewind", NULL, NULL, media_player_rewind) },
	{ GDBUS_METHOD("Press", GDBUS_ARGS({"avc_key", "y"}), NULL,
							media_player_press) },
	{ GDBUS_METHOD("Hold", GDBUS_ARGS({"avc_key", "y"}), NULL,
							media_player_hold) },
	{ GDBUS_METHOD("Release", NULL, NULL, media_player_release) },
	{ }
};

static const GDBusSignalTable media_player_signals[] = {
	{ }
};

static const GDBusPropertyTable media_player_properties[] = {
	{ "Name", "s", get_name, NULL, name_exists },
	{ "Type", "s", get_type, NULL, type_exists },
	{ "Subtype", "s", get_subtype, NULL, subtype_exists },
	{ "Position", "u", get_position, NULL, NULL },
	{ "Status", "s", get_status, NULL, status_exists },
	{ "Equalizer", "s", get_setting, set_setting, setting_exists },
	{ "Repeat", "s", get_setting, set_setting, setting_exists },
	{ "Shuffle", "s", get_setting, set_setting, setting_exists },
	{ "Scan", "s", get_setting, set_setting, setting_exists },
	{ "Track", "a{sv}", get_track, NULL, track_exists },
	{ "Device", "o", get_device, NULL, NULL },
	{ "Browsable", "b", get_browsable, NULL, browsable_exists },
	{ "Searchable", "b", get_searchable, NULL, searchable_exists },
	{ "Playlist", "o", get_playlist, NULL, playlist_exists },
	{ "ObexPort", "q", get_obexport, NULL, obexport_exists,
			G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
	{ }
};

static DBusMessage *media_folder_search(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	struct media_player *mp = data;
	struct media_folder *folder = mp->scope;
	struct player_callback *cb = mp->cb;
	DBusMessageIter iter;
	const char *string;
	int err;

	dbus_message_iter_init(msg, &iter);

	if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING)
		return btd_error_invalid_args(msg);

	dbus_message_iter_get_basic(&iter, &string);

	if (!mp->searchable || folder != mp->folder || !cb->cbs->search)
		return btd_error_not_supported(msg);

	if (folder->msg != NULL)
		return btd_error_failed(msg, strerror(EINVAL));

	err = cb->cbs->search(mp, string, cb->user_data);
	if (err < 0)
		return btd_error_failed(msg, strerror(-err));

	folder->msg = dbus_message_ref(msg);

	return NULL;
}

static int parse_filters(struct media_player *player, DBusMessageIter *iter,
						uint32_t *start, uint32_t *end)
{
	struct media_folder *folder = player->scope;
	DBusMessageIter dict;
	int ctype;

	*start = 0;
	*end = folder->number_of_items ? folder->number_of_items - 1 :
								UINT32_MAX;

	ctype = dbus_message_iter_get_arg_type(iter);
	if (ctype != DBUS_TYPE_ARRAY)
		return FALSE;

	dbus_message_iter_recurse(iter, &dict);

	while ((ctype = dbus_message_iter_get_arg_type(&dict)) !=
							DBUS_TYPE_INVALID) {
		DBusMessageIter entry, var;
		const char *key;

		if (ctype != DBUS_TYPE_DICT_ENTRY)
			return -EINVAL;

		dbus_message_iter_recurse(&dict, &entry);
		if (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_STRING)
			return -EINVAL;

		dbus_message_iter_get_basic(&entry, &key);
		dbus_message_iter_next(&entry);

		if (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_VARIANT)
			return -EINVAL;

		dbus_message_iter_recurse(&entry, &var);

		if (dbus_message_iter_get_arg_type(&var) != DBUS_TYPE_UINT32)
			return -EINVAL;

		if (strcasecmp(key, "Start") == 0)
			dbus_message_iter_get_basic(&var, start);
		else if (strcasecmp(key, "End") == 0)
			dbus_message_iter_get_basic(&var, end);

		dbus_message_iter_next(&dict);
	}

	if (folder->number_of_items > 0 && *end > folder->number_of_items)
		*end = folder->number_of_items;

	return 0;
}

static DBusMessage *media_folder_list_items(DBusConnection *conn,
						DBusMessage *msg, void *data)
{
	struct media_player *mp = data;
	struct media_folder *folder = mp->scope;
	struct player_callback *cb = mp->cb;
	DBusMessageIter iter;
	uint32_t start, end;
	int err;

	dbus_message_iter_init(msg, &iter);

	if (parse_filters(mp, &iter, &start, &end) < 0)
		return btd_error_invalid_args(msg);

	if (cb->cbs->list_items == NULL)
		return btd_error_not_supported(msg);

	if (folder->msg != NULL)
		return btd_error_failed(msg, strerror(EBUSY));

	err = cb->cbs->list_items(mp, folder->item->name, start, end,
							cb->user_data);
	if (err < 0)
		return btd_error_failed(msg, strerror(-err));

	folder->msg = dbus_message_ref(msg);

	return NULL;
}

static void media_item_free(struct media_item *item)
{
	if (item->metadata != NULL)
		g_hash_table_unref(item->metadata);

	g_free(item->path);
	g_free(item->name);
	g_free(item);
}

static void media_item_destroy(void *data)
{
	struct media_item *item = data;

	DBG("%s", item->path);

	g_dbus_unregister_interface(btd_get_dbus_connection(), item->path,
						MEDIA_ITEM_INTERFACE);

	media_item_free(item);
}

static void media_folder_destroy(void *data)
{
	struct media_folder *folder = data;

	g_slist_free_full(folder->subfolders, media_folder_destroy);
	g_slist_free_full(folder->items, media_item_destroy);

	if (folder->msg != NULL)
		dbus_message_unref(folder->msg);

	media_item_destroy(folder->item);
	g_free(folder);
}

static void media_player_change_scope(struct media_player *mp,
						struct media_folder *folder)
{
	struct player_callback *cb = mp->cb;
	int err;

	if (mp->scope == folder)
		return;

	DBG("%s", folder->item->name);

	/* Skip setting current folder if folder is current playlist/search */
	if (folder == mp->playlist || folder == mp->search)
		goto cleanup;

	mp->folder = folder;

	/* Skip item cleanup if scope is the current playlist */
	if (mp->scope == mp->playlist)
		goto done;

cleanup:
	g_slist_free_full(mp->scope->items, media_item_destroy);
	mp->scope->items = NULL;

	/* Destroy search folder if it exists and is not being set as scope */
	if (mp->search != NULL && folder != mp->search) {
		mp->folders = g_slist_remove(mp->folders, mp->search);
		media_folder_destroy(mp->search);
		mp->search = NULL;
	}

done:
	mp->scope = folder;

	if (cb->cbs->total_items) {
		err = cb->cbs->total_items(mp, folder->item->name,
							cb->user_data);
		if (err < 0)
			DBG("Failed to get total num of items");
	} else {
		g_dbus_emit_property_changed(btd_get_dbus_connection(),
				mp->path, MEDIA_FOLDER_INTERFACE,
				"NumberOfItems");
	}

	g_dbus_emit_property_changed(btd_get_dbus_connection(), mp->path,
				MEDIA_FOLDER_INTERFACE, "Name");
}

static struct media_folder *find_folder(GSList *folders, const char *pattern)
{
	GSList *l;

	for (l = folders; l; l = l->next) {
		struct media_folder *folder = l->data;

		if (g_str_equal(folder->item->name, pattern))
			return folder;

		if (g_str_equal(folder->item->path, pattern))
			return folder;

		folder = find_folder(folder->subfolders, pattern);
		if (folder != NULL)
			return folder;
	}

	return NULL;
}

static struct media_folder *media_player_find_folder(struct media_player *mp,
							const char *pattern)
{
	return find_folder(mp->folders, pattern);
}

static DBusMessage *media_folder_change_folder(DBusConnection *conn,
						DBusMessage *msg, void *data)
{
	struct media_player *mp = data;
	struct media_folder *folder = mp->scope;
	struct player_callback *cb = mp->cb;
	const char *path;
	int err;

	if (!dbus_message_get_args(msg, NULL,
					DBUS_TYPE_OBJECT_PATH, &path,
					DBUS_TYPE_INVALID))
		return btd_error_invalid_args(msg);

	if (folder->msg != NULL)
		return btd_error_failed(msg, strerror(EBUSY));

	folder = media_player_find_folder(mp, path);
	if (folder == NULL)
		return btd_error_invalid_args(msg);

	if (mp->scope == folder)
		return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);

	if (folder == mp->playlist || folder == mp->folder ||
						folder == mp->search) {
		media_player_change_scope(mp, folder);
		return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
	}

	/*
	 * ChangePath can only navigate one level up/down so check if folder
	 * is direct child or parent of the current folder otherwise fail.
	 */
	if (!g_slist_find(mp->folder->subfolders, folder) &&
				!g_slist_find(folder->subfolders, mp->folder))
		return btd_error_invalid_args(msg);

	if (cb->cbs->change_folder == NULL)
		return btd_error_not_supported(msg);

	err = cb->cbs->change_folder(mp, folder->item->name, folder->item->uid,
								cb->user_data);
	if (err < 0)
		return btd_error_failed(msg, strerror(-err));

	mp->scope->msg = dbus_message_ref(msg);

	return NULL;
}

static gboolean folder_name_exists(const GDBusPropertyTable *property,
								void *data)
{
	struct media_player *mp = data;
	struct media_folder *folder = mp->scope;

	if (folder == NULL || folder->item == NULL)
		return FALSE;

	return folder->item->name != NULL;
}

static gboolean get_folder_name(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_player *mp = data;
	struct media_folder *folder = mp->scope;

	if (folder == NULL || folder->item == NULL)
		return FALSE;

	DBG("%s", folder->item->name);

	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING,
							&folder->item->name);

	return TRUE;
}

static gboolean items_exists(const GDBusPropertyTable *property, void *data)
{
	struct media_player *mp = data;

	return mp->scope != NULL;
}

static gboolean get_items(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_player *mp = data;
	struct media_folder *folder = mp->scope;

	if (folder == NULL)
		return FALSE;

	DBG("%u", folder->number_of_items);

	dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32,
						&folder->number_of_items);

	return TRUE;
}

static const GDBusMethodTable media_folder_methods[] = {
	{ GDBUS_ASYNC_METHOD("Search",
			GDBUS_ARGS({ "string", "s" }, { "filter", "a{sv}" }),
			GDBUS_ARGS({ "folder", "o" }),
			media_folder_search) },
	{ GDBUS_ASYNC_METHOD("ListItems",
			GDBUS_ARGS({ "filter", "a{sv}" }),
			GDBUS_ARGS({ "items", "a{oa{sv}}" }),
			media_folder_list_items) },
	{ GDBUS_ASYNC_METHOD("ChangeFolder",
			GDBUS_ARGS({ "folder", "o" }), NULL,
			media_folder_change_folder) },
	{ }
};

static const GDBusPropertyTable media_folder_properties[] = {
	{ "Name", "s", get_folder_name, NULL, folder_name_exists },
	{ "NumberOfItems", "u", get_items, NULL, items_exists },
	{ }
};

static void media_player_set_scope(struct media_player *mp,
						struct media_folder *folder)
{
	if (mp->scope == NULL) {
		if (!g_dbus_register_interface(btd_get_dbus_connection(),
					mp->path, MEDIA_FOLDER_INTERFACE,
					media_folder_methods,
					NULL,
					media_folder_properties, mp, NULL)) {
			error("D-Bus failed to register %s on %s path",
					MEDIA_FOLDER_INTERFACE, mp->path);
			return;
		}
		mp->scope = folder;
		return;
	}

	return media_player_change_scope(mp, folder);
}

static struct media_folder *
media_player_find_folder_by_uid(struct media_player *mp, uint64_t uid)
{
	struct media_folder *folder = mp->scope;
	struct media_folder *parent = folder->parent;
	GSList *l;

	if (parent && parent->item->uid == uid)
		return parent;

	for (l = folder->subfolders; l; l = l->next) {
		struct media_folder *folder = l->data;

		if (folder->item->uid == uid)
			return folder;
	}

	return NULL;
}

static void media_player_set_folder_by_uid(struct media_player *mp,
					uint64_t uid, uint32_t number_of_items)
{
	struct media_folder *folder;

	DBG("uid %" PRIu64 " number of items %u", uid, number_of_items);

	folder = media_player_find_folder_by_uid(mp, uid);
	if (folder == NULL) {
		error("Unknown folder: %" PRIu64, uid);
		return;
	}

	folder->number_of_items = number_of_items;

	media_player_set_scope(mp, folder);
}

void media_player_change_folder_complete(struct media_player *mp,
						const char *path, uint64_t uid,
						int ret)
{
	struct media_folder *folder = mp->scope;
	DBusMessage *reply;

	if (folder == NULL || folder->msg == NULL)
		return;

	if (ret < 0) {
		reply = btd_error_failed(folder->msg, strerror(-ret));
		goto done;
	}

	media_player_set_folder_by_uid(mp, uid, ret);

	reply = g_dbus_create_reply(folder->msg, DBUS_TYPE_INVALID);

done:
	g_dbus_send_message(btd_get_dbus_connection(), reply);
	dbus_message_unref(folder->msg);
	folder->msg = NULL;
}

void media_player_destroy(struct media_player *mp)
{
	DBG("%s", mp->path);

	g_dbus_unregister_interface(btd_get_dbus_connection(), mp->path,
						MEDIA_PLAYER_INTERFACE);

	if (mp->track)
		g_hash_table_unref(mp->track);

	if (mp->settings)
		g_hash_table_unref(mp->settings);

	if (mp->scope)
		g_dbus_unregister_interface(btd_get_dbus_connection(),
						mp->path,
						MEDIA_FOLDER_INTERFACE);

	g_slist_free_full(mp->pending, g_free);
	g_slist_free_full(mp->folders, media_folder_destroy);

	g_timer_destroy(mp->progress);
	g_free(mp->cb);
	g_free(mp->status);
	g_free(mp->path);
	g_free(mp->device);
	g_free(mp->subtype);
	g_free(mp->type);
	g_free(mp->name);
	g_free(mp);
}

struct media_player *media_player_controller_create(const char *path,
							const char *type,
							uint16_t id)
{
	struct media_player *mp;

	mp = g_new0(struct media_player, 1);
	mp->device = g_strdup(path);
	mp->path = g_strdup_printf("%s/%s/player%u", path, type, id);
	mp->settings = g_hash_table_new_full(g_str_hash, g_str_equal,
							g_free, g_free);
	mp->track = g_hash_table_new_full(g_str_hash, g_str_equal,
							g_free, g_free);
	mp->progress = g_timer_new();

	if (!g_dbus_register_interface(btd_get_dbus_connection(),
					mp->path, MEDIA_PLAYER_INTERFACE,
					media_player_methods,
					media_player_signals,
					media_player_properties, mp, NULL)) {
		error("D-Bus failed to register %s path", mp->path);
		media_player_destroy(mp);
		return NULL;
	}

	DBG("%s", mp->path);

	return mp;
}

const char *media_player_get_path(struct media_player *mp)
{
	return mp->path;
}

void media_player_set_duration(struct media_player *mp, uint32_t duration)
{
	char *value, *curval;

	DBG("%u", duration);

	/* Only update duration if track exists */
	if (g_hash_table_size(mp->track) == 0)
		return;

	/* Ignore if duration is already set to value > 0 */
	curval = g_hash_table_lookup(mp->track, "Duration");
	if (curval != NULL && atoi(curval) != 0)
		return;

	value = g_strdup_printf("%u", duration);

	g_hash_table_replace(mp->track, g_strdup("Duration"), value);

	g_dbus_emit_property_changed_full(btd_get_dbus_connection(),
					mp->path, MEDIA_PLAYER_INTERFACE,
					"Track",
					G_DBUS_PROPERTY_CHANGED_FLAG_FLUSH);
}

void media_player_set_position(struct media_player *mp, uint32_t position)
{
	DBG("%u", position);

	/* Only update duration if track exists */
	if (g_hash_table_size(mp->track) == 0)
		return;

	mp->position = position;
	g_timer_start(mp->progress);

	g_dbus_emit_property_changed(btd_get_dbus_connection(), mp->path,
					MEDIA_PLAYER_INTERFACE, "Position");
}

void media_player_set_setting(struct media_player *mp, const char *key,
							const char *value)
{
	char *curval;
	struct pending_req *p;

	DBG("%s: %s", key, value);

	if (strcasecmp(key, "Error") == 0) {
		p = g_slist_nth_data(mp->pending, 0);
		if (p == NULL)
			return;

		g_dbus_pending_property_error(p->id, ERROR_INTERFACE ".Failed",
									value);
		goto send;
	}

	curval = g_hash_table_lookup(mp->settings, key);
	if (g_strcmp0(curval, value) == 0)
		goto done;

	g_hash_table_replace(mp->settings, g_strdup(key), g_strdup(value));
	g_dbus_emit_property_changed(btd_get_dbus_connection(), mp->path,
					MEDIA_PLAYER_INTERFACE, key);

done:
	p = find_pending(mp, key);
	if (p == NULL)
		return;

	if (strcasecmp(value, p->value) == 0)
		g_dbus_pending_property_success(p->id);
	else
		g_dbus_pending_property_error(p->id,
					ERROR_INTERFACE ".NotSupported",
					"Operation is not supported");

send:
	mp->pending = g_slist_remove(mp->pending, p);
	g_free(p);

	return;
}

const char *media_player_get_status(struct media_player *mp)
{
	return mp->status;
}

void media_player_set_status(struct media_player *mp, const char *status)
{
	DBG("%s", status);

	if (g_strcmp0(mp->status, status) == 0)
		return;

	g_free(mp->status);
	mp->status = g_strdup(status);

	g_dbus_emit_property_changed(btd_get_dbus_connection(), mp->path,
					MEDIA_PLAYER_INTERFACE, "Status");

	mp->position = media_player_get_position(mp);
	g_timer_start(mp->progress);
}

static gboolean remove_metadata(void *key, void *value, void *user_data)
{
	if (!strcmp(key, "Duration"))
		return FALSE;

	return strcmp(key, "Item") ? TRUE : FALSE;
}

void media_player_clear_metadata(struct media_player *mp)
{
	if (!mp)
		return;

	g_hash_table_foreach_remove(mp->track, remove_metadata, NULL);
}

void media_player_set_metadata(struct media_player *mp,
				struct media_item *item, const char *key,
				void *data, size_t len)
{
	char *value, *curval;
	GHashTable *metadata = item ? item->metadata : mp->track;

	value = g_strndup(data, len);

	DBG("%s: %s", key, value);

	curval = g_hash_table_lookup(metadata, key);
	if (g_strcmp0(curval, value) == 0) {
		g_free(value);
		return;
	}

	g_hash_table_replace(metadata, g_strdup(key), value);
}

void media_player_metadata_changed(struct media_player *mp)
{
	char *item;

	if (!mp)
		return;

	g_dbus_emit_property_changed_full(btd_get_dbus_connection(),
					mp->path, MEDIA_PLAYER_INTERFACE,
					"Track",
					G_DBUS_PROPERTY_CHANGED_FLAG_FLUSH);

	item = g_hash_table_lookup(mp->track, "Item");
	if (item == NULL)
		return;

	g_dbus_emit_property_changed_full(btd_get_dbus_connection(),
					item, MEDIA_ITEM_INTERFACE,
					"Metadata",
					G_DBUS_PROPERTY_CHANGED_FLAG_FLUSH);
}

void media_player_set_type(struct media_player *mp, const char *type)
{
	if (g_strcmp0(mp->type, type) == 0)
		return;

	DBG("%s", type);

	mp->type = g_strdup(type);

	g_dbus_emit_property_changed(btd_get_dbus_connection(),
					mp->path, MEDIA_PLAYER_INTERFACE,
					"Type");
}

void media_player_set_subtype(struct media_player *mp, const char *subtype)
{
	if (g_strcmp0(mp->subtype, subtype) == 0)
		return;

	DBG("%s", subtype);

	mp->subtype = g_strdup(subtype);

	g_dbus_emit_property_changed(btd_get_dbus_connection(),
					mp->path, MEDIA_PLAYER_INTERFACE,
					"Subtype");
}

void media_player_set_name(struct media_player *mp, const char *name)
{
	if (g_strcmp0(mp->name, name) == 0)
		return;

	DBG("%s", name);

	mp->name = g_strdup(name);

	g_dbus_emit_property_changed(btd_get_dbus_connection(),
					mp->path, MEDIA_PLAYER_INTERFACE,
					"Name");
}

void media_player_set_browsable(struct media_player *mp, bool enabled)
{
	if (mp->browsable == enabled)
		return;

	DBG("%s", enabled ? "true" : "false");

	mp->browsable = enabled;

	g_dbus_emit_property_changed(btd_get_dbus_connection(),
					mp->path, MEDIA_PLAYER_INTERFACE,
					"Browsable");
}

bool media_player_get_browsable(struct media_player *mp)
{
	return mp->browsable;
}

void media_player_set_searchable(struct media_player *mp, bool enabled)
{
	if (mp->searchable == enabled)
		return;

	DBG("%s", enabled ? "true" : "false");

	mp->searchable = enabled;

	g_dbus_emit_property_changed(btd_get_dbus_connection(),
					mp->path, MEDIA_PLAYER_INTERFACE,
					"Searchable");
}

void media_player_set_folder(struct media_player *mp, const char *name,
						uint32_t number_of_items)
{
	struct media_folder *folder;

	DBG("%s number of items %u", name, number_of_items);

	folder = media_player_find_folder(mp, name);
	if (folder == NULL) {
		error("Unknown folder: %s", name);
		return;
	}

	folder->number_of_items = number_of_items;

	media_player_set_scope(mp, folder);
}

void media_player_set_playlist(struct media_player *mp, const char *name)
{
	struct media_folder *folder;

	DBG("%s", name);

	folder = media_player_find_folder(mp, name);
	if (folder == NULL) {
		error("Unknown folder: %s", name);
		return;
	}

	mp->playlist = folder;

	g_dbus_emit_property_changed(btd_get_dbus_connection(), mp->path,
					MEDIA_PLAYER_INTERFACE, "Playlist");
}

static struct media_item *media_folder_find_item(struct media_folder *folder,
								uint64_t uid)
{
	GSList *l;

	if (uid == 0)
		return NULL;

	for (l = folder->items; l; l = l->next) {
		struct media_item *item = l->data;

		if (item->uid == uid)
			return item;
	}

	return NULL;
}

static DBusMessage *media_item_play(DBusConnection *conn, DBusMessage *msg,
								void *data)
{
	struct media_item *item = data;
	struct media_player *mp = item->player;
	struct media_folder *folder = mp->scope;
	struct player_callback *cb = mp->cb;
	const char *path;
	int err;

	if (!item->playable || !cb->cbs->play_item)
		return btd_error_not_supported(msg);

	if (folder->msg)
		return btd_error_failed(msg, strerror(EBUSY));

	path = mp->search && folder == mp->search ? "/Search" : item->path;

	err = cb->cbs->play_item(mp, path, item->uid, cb->user_data);
	if (err < 0)
		return btd_error_failed(msg, strerror(-err));

	folder->msg = dbus_message_ref(msg);

	return NULL;
}

static DBusMessage *media_item_add_to_nowplaying(DBusConnection *conn,
						DBusMessage *msg, void *data)
{
	struct media_item *item = data;
	struct media_player *mp = item->player;
	struct player_callback *cb = mp->cb;
	int err;

	if (!item->playable || !cb->cbs->play_item)
		return btd_error_not_supported(msg);

	err = cb->cbs->add_to_nowplaying(mp, item->path, item->uid,
							cb->user_data);
	if (err < 0)
		return btd_error_failed(msg, strerror(-err));

	return g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
}

static gboolean get_player(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_item *item = data;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH,
							&item->player->path);

	return TRUE;
}

static gboolean item_name_exists(const GDBusPropertyTable *property,
								void *data)
{
	struct media_item *item = data;

	return item->name != NULL;
}

static gboolean get_item_name(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_item *item = data;

	if (item->name == NULL)
		return FALSE;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &item->name);

	return TRUE;
}

static const char *type_to_string(uint8_t type)
{
	switch (type) {
	case PLAYER_ITEM_TYPE_AUDIO:
		return "audio";
	case PLAYER_ITEM_TYPE_VIDEO:
		return "video";
	case PLAYER_ITEM_TYPE_FOLDER:
		return "folder";
	}

	return NULL;
}

static gboolean get_item_type(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_item *item = data;
	const char *string;

	string = type_to_string(item->type);

	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &string);

	return TRUE;
}

static gboolean get_playable(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_item *item = data;
	dbus_bool_t value;

	value = item->playable;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &value);

	return TRUE;
}

static const char *folder_type_to_string(uint8_t type)
{
	switch (type) {
	case PLAYER_FOLDER_TYPE_MIXED:
		return "mixed";
	case PLAYER_FOLDER_TYPE_TITLES:
		return "titles";
	case PLAYER_FOLDER_TYPE_ALBUMS:
		return "albums";
	case PLAYER_FOLDER_TYPE_ARTISTS:
		return "artists";
	case PLAYER_FOLDER_TYPE_GENRES:
		return "genres";
	case PLAYER_FOLDER_TYPE_PLAYLISTS:
		return "playlists";
	case PLAYER_FOLDER_TYPE_YEARS:
		return "years";
	}

	return NULL;
}

static gboolean folder_type_exists(const GDBusPropertyTable *property,
								void *data)
{
	struct media_item *item = data;

	return folder_type_to_string(item->folder_type) != NULL;
}

static gboolean get_folder_type(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_item *item = data;
	const char *string;

	string = folder_type_to_string(item->folder_type);
	if (string == NULL)
		return FALSE;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &string);

	return TRUE;
}

static gboolean metadata_exists(const GDBusPropertyTable *property, void *data)
{
	struct media_item *item = data;

	return item->metadata != NULL;
}

static void append_metadata(void *key, void *value, void *user_data)
{
	DBusMessageIter *dict = user_data;
	const char *strkey = key;

	if (strcasecmp(strkey, "Item") == 0)
		return;

	if (strcasecmp(strkey, "Duration") == 0 ||
			strcasecmp(strkey, "TrackNumber") == 0 ||
			strcasecmp(strkey, "NumberOfTracks") == 0)  {
		uint32_t num = atoi(value);
		dict_append_entry(dict, key, DBUS_TYPE_UINT32, &num);
	} else {
		dict_append_entry(dict, key, DBUS_TYPE_STRING, &value);
	}
}

static gboolean get_metadata(const GDBusPropertyTable *property,
					DBusMessageIter *iter, void *data)
{
	struct media_item *item = data;
	DBusMessageIter dict;

	dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY,
					DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
					DBUS_TYPE_STRING_AS_STRING
					DBUS_TYPE_VARIANT_AS_STRING
					DBUS_DICT_ENTRY_END_CHAR_AS_STRING,
					&dict);

	if (g_hash_table_size(item->metadata) > 0)
		g_hash_table_foreach(item->metadata, append_metadata, &dict);
	else if (item->name != NULL)
		dict_append_entry(&dict, "Title", DBUS_TYPE_STRING,
								&item->name);

	dbus_message_iter_close_container(iter, &dict);

	return TRUE;
}

static const GDBusMethodTable media_item_methods[] = {
	{ GDBUS_ASYNC_METHOD("Play", NULL, NULL, media_item_play) },
	{ GDBUS_METHOD("AddtoNowPlaying", NULL, NULL,
					media_item_add_to_nowplaying) },
	{ }
};

static const GDBusPropertyTable media_item_properties[] = {
	{ "Player", "o", get_player, NULL, NULL },
	{ "Name", "s", get_item_name, NULL, item_name_exists },
	{ "Type", "s", get_item_type, NULL, NULL },
	{ "FolderType", "s", get_folder_type, NULL, folder_type_exists },
	{ "Playable", "b", get_playable, NULL, NULL },
	{ "Metadata", "a{sv}", get_metadata, NULL, metadata_exists },
	{ }
};

void media_player_play_item_complete(struct media_player *mp, int err)
{
	struct media_folder *folder = mp->scope;
	DBusMessage *reply;

	if (folder == NULL || folder->msg == NULL)
		return;

	if (err < 0) {
		reply = btd_error_failed(folder->msg, strerror(-err));
		goto done;
	}

	reply = g_dbus_create_reply(folder->msg, DBUS_TYPE_INVALID);

done:
	g_dbus_send_message(btd_get_dbus_connection(), reply);
	dbus_message_unref(folder->msg);
	folder->msg = NULL;
}

void media_item_set_playable(struct media_item *item, bool value)
{
	if (item->playable == value)
		return;

	item->playable = value;

	g_dbus_emit_property_changed(btd_get_dbus_connection(), item->path,
					MEDIA_ITEM_INTERFACE, "Playable");
}

static struct media_item *media_folder_create_item(struct media_player *mp,
						struct media_folder *folder,
						const char *name,
						player_item_type_t type,
						uint64_t uid)
{
	struct media_item *item;
	const char *strtype;

	item = media_folder_find_item(folder, uid);
	if (item != NULL)
		return item;

	strtype = type_to_string(type);
	if (strtype == NULL)
		return NULL;

	DBG("%s type %s uid %" PRIu64 "", name, strtype, uid);

	item = g_new0(struct media_item, 1);
	item->player = mp;
	item->uid = uid;

	if (!uid && name[0] == '/')
		item->path = g_strdup_printf("%s%s", mp->path, name);
	else
		item->path = g_strdup_printf("%s/item%" PRIu64 "",
						folder->item->path, uid);

	item->name = g_strdup(name);
	item->type = type;
	item->folder_type = PLAYER_FOLDER_TYPE_INVALID;

	if (!g_dbus_register_interface(btd_get_dbus_connection(),
					item->path, MEDIA_ITEM_INTERFACE,
					media_item_methods,
					NULL,
					media_item_properties, item, NULL)) {
		error("D-Bus failed to register %s on %s path",
					MEDIA_ITEM_INTERFACE, item->path);
		media_item_free(item);
		return NULL;
	}

	if (type != PLAYER_ITEM_TYPE_FOLDER) {
		folder->items = g_slist_prepend(folder->items, item);
		item->metadata = g_hash_table_new_full(g_str_hash, g_str_equal,
							g_free, g_free);
	}

	DBG("%s", item->path);

	return item;
}

struct media_item *media_player_create_item(struct media_player *mp,
						const char *name,
						player_item_type_t type,
						uint64_t uid)
{
	return media_folder_create_item(mp, mp->scope, name, type, uid);
}

struct media_item *media_player_create_folder(struct media_player *mp,
						const char *name,
						player_folder_type_t type,
						uint64_t uid)
{
	struct media_folder *folder;
	struct media_item *item;

	if (uid > 0)
		folder = media_player_find_folder_by_uid(mp, uid);
	else
		folder = media_player_find_folder(mp, name);

	if (folder != NULL)
		return folder->item;

	if (uid > 0)
		item = media_player_create_subfolder(mp, name, uid);
	else
		item = media_player_create_item(mp, name,
						PLAYER_ITEM_TYPE_FOLDER, uid);

	if (item == NULL)
		return NULL;

	folder = g_new0(struct media_folder, 1);
	folder->item = item;

	item->folder_type = type;

	if (mp->folder != NULL)
		goto done;

	mp->folder = folder;

done:
	if (uid > 0) {
		folder->parent = mp->folder;
		mp->folder->subfolders = g_slist_prepend(
							mp->folder->subfolders,
							folder);
	} else
		mp->folders = g_slist_prepend(mp->folders, folder);

	return item;
}

void media_player_set_callbacks(struct media_player *mp,
				const struct media_player_callback *cbs,
				void *user_data)
{
	struct player_callback *cb;

	if (mp->cb)
		g_free(mp->cb);

	cb = g_new0(struct player_callback, 1);
	cb->cbs = cbs;
	cb->user_data = user_data;

	mp->cb = cb;
}

struct media_item *media_player_set_playlist_item(struct media_player *mp,
								uint64_t uid)
{
	struct media_folder *folder = mp->playlist;
	struct media_item *item;
	char *path;

	DBG("%" PRIu64 "", uid);

	if (folder == NULL || uid == 0)
		return NULL;

	item = media_folder_create_item(mp, folder, NULL,
						PLAYER_ITEM_TYPE_AUDIO, uid);
	if (item == NULL)
		return NULL;

	media_item_set_playable(item, true);

	if (mp->track != item->metadata) {
		g_hash_table_unref(mp->track);
		mp->track = g_hash_table_ref(item->metadata);
	}

	path = g_hash_table_lookup(mp->track, "Item");
	if (path && !strcmp(path, item->path))
		return item;

	g_hash_table_replace(mp->track, g_strdup("Item"), g_strdup(item->path));

	return item;
}

void media_player_clear_playlist(struct media_player *mp)
{
	if (mp->playlist) {
		g_slist_free_full(mp->playlist->items, media_item_destroy);
		mp->playlist->items = NULL;
	}

	g_dbus_emit_property_changed(btd_get_dbus_connection(), mp->path,
					MEDIA_PLAYER_INTERFACE, "Playlist");
}

void media_player_set_obex_port(struct media_player *mp, uint16_t port)
{
	mp->obex_port = port;
}
