/* Spa midi dbus */
/* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */
/* SPDX-License-Identifier: MIT */

#include "config.h"

#include <errno.h>
#include <stddef.h>

#include <spa/support/log.h>
#include <spa/support/loop.h>
#include <spa/support/plugin.h>
#include <spa/monitor/device.h>
#include <spa/monitor/utils.h>
#include <spa/utils/hook.h>
#include <spa/utils/type.h>
#include <spa/utils/keys.h>
#include <spa/utils/names.h>
#include <spa/utils/result.h>
#include <spa/utils/string.h>
#include <spa/utils/json.h>
#include <spa/node/node.h>
#include <spa/node/keys.h>

#include "midi.h"

#include "bluez5-interface-gen.h"
#include "dbus-monitor.h"

#define MIDI_OBJECT_PATH	"/midi"
#define MIDI_PROFILE_PATH	MIDI_OBJECT_PATH "/profile"

SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.midi");
#undef SPA_LOG_TOPIC_DEFAULT
#define SPA_LOG_TOPIC_DEFAULT &log_topic

struct impl
{
	struct spa_handle handle;
	struct spa_device device;

	struct spa_log *log;

	GDBusConnection *conn;
	struct dbus_monitor monitor;
	GDBusObjectManagerServer *manager;

	struct spa_hook_list hooks;

	uint32_t id;
};

struct _MidiEnumCharacteristicProxy
{
	Bluez5GattCharacteristic1Proxy parent_instance;

	struct impl *impl;

	gchar *description;
	uint32_t id;
	GCancellable *read_call;
	GCancellable *dsc_call;
	unsigned int node_emitted:1;
	unsigned int read_probed:1;
	unsigned int read_done:1;
	unsigned int dsc_probed:1;
	unsigned int dsc_done:1;
};

G_DECLARE_FINAL_TYPE(MidiEnumCharacteristicProxy, midi_enum_characteristic_proxy, MIDI_ENUM,
		CHARACTERISTIC_PROXY, Bluez5GattCharacteristic1Proxy)
G_DEFINE_TYPE(MidiEnumCharacteristicProxy, midi_enum_characteristic_proxy, BLUEZ5_TYPE_GATT_CHARACTERISTIC1_PROXY)
#define MIDI_ENUM_TYPE_CHARACTERISTIC_PROXY	(midi_enum_characteristic_proxy_get_type())

struct _MidiEnumManagerProxy
{
	Bluez5GattManager1Proxy parent_instance;

	GCancellable *register_call;
	unsigned int registered:1;
};

G_DECLARE_FINAL_TYPE(MidiEnumManagerProxy, midi_enum_manager_proxy, MIDI_ENUM,
		MANAGER_PROXY, Bluez5GattManager1Proxy)
G_DEFINE_TYPE(MidiEnumManagerProxy, midi_enum_manager_proxy, BLUEZ5_TYPE_GATT_MANAGER1_PROXY)
#define MIDI_ENUM_TYPE_MANAGER_PROXY	(midi_enum_manager_proxy_get_type())


static void emit_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr, Bluez5Device1 *device)
{
	struct spa_device_object_info info;
	char nick[512], class[16];
	struct spa_dict_item items[23];
	uint32_t n_items = 0;
	const char *path = g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr));
	const char *alias = bluez5_device1_get_alias(device);

	spa_log_debug(impl->log, "emit node for path=%s", path);

	info = SPA_DEVICE_OBJECT_INFO_INIT();
	info.type = SPA_TYPE_INTERFACE_Node;
	info.factory_name = SPA_NAME_API_BLUEZ5_MIDI_NODE;
	info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS |
		SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
	info.flags = 0;

	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "bluez5");
	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, "bluetooth");
	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Midi/Bridge");
	items[n_items++] = SPA_DICT_ITEM_INIT("node.description",
			alias ? alias : bluez5_device1_get_name(device));
	if (chr->description && chr->description[0] != '\0') {
		spa_scnprintf(nick, sizeof(nick), "%s (%s)", alias, chr->description);
		items[n_items++] = SPA_DICT_ITEM_INIT("node.nick", nick);
	}
	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ICON, bluez5_device1_get_icon(device));
	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_PATH, path);
	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ADDRESS, bluez5_device1_get_address(device));
	snprintf(class, sizeof(class), "0x%06x", bluez5_device1_get_class(device));
	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CLASS, class);
	items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ROLE, "client");

	info.props = &SPA_DICT_INIT(items, n_items);
	spa_device_emit_object_info(&impl->hooks, chr->id, &info);
}

static void remove_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr)
{
	spa_log_debug(impl->log, "remove node for path=%s", g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)));

	spa_device_emit_object_info(&impl->hooks, chr->id, NULL);
}

static void check_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr);

static void read_probe_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
	MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(source_object);
	struct impl *impl = user_data;
	gchar *value = NULL;
	GError *err = NULL;

	bluez5_gatt_characteristic1_call_read_value_finish(
		BLUEZ5_GATT_CHARACTERISTIC1(source_object), &value, res, &err);

	if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
		/* Operation canceled: user_data may be invalid by now */
		g_error_free(err);
		goto done;
	}
	if (err) {
		spa_log_error(impl->log, "%s.ReadValue() failed: %s",
				BLUEZ_GATT_CHR_INTERFACE,
				err->message);
		g_error_free(err);
		goto done;
	}

	g_free(value);

	spa_log_debug(impl->log, "MIDI GATT read probe done for path=%s",
			g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)));

	chr->read_done = true;

	check_chr_node(impl, chr);

done:
	g_clear_object(&chr->read_call);
}

static int read_probe(struct impl *impl, MidiEnumCharacteristicProxy *chr)
{
	GVariantBuilder builder;
	GVariant *options;

	/*
	 * BLE MIDI-1.0 §5: The Central shall read the MIDI I/O characteristic
	 * of the Peripheral after establishing a connection with the accessory.
	 */

	if (chr->read_probed)
		return 0;
	if (chr->read_call)
		return -EBUSY;

	chr->read_probed = true;

	spa_log_debug(impl->log, "MIDI GATT read probe for path=%s",
			g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)));

	chr->read_call = g_cancellable_new();

	g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
	options = g_variant_builder_end(&builder);

	bluez5_gatt_characteristic1_call_read_value(BLUEZ5_GATT_CHARACTERISTIC1(chr),
			options,
			chr->read_call,
			read_probe_reply,
			impl);

	return 0;
}

static Bluez5GattDescriptor1 *find_dsc(struct impl *impl, MidiEnumCharacteristicProxy *chr)
{
	const char *path = g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr));
	Bluez5GattDescriptor1 *found = NULL;;
	GList *objects;

	objects = g_dbus_object_manager_get_objects(dbus_monitor_manager(&impl->monitor));

	for (GList *llo = g_list_first(objects); llo; llo = llo->next) {
		GList *interfaces = g_dbus_object_get_interfaces(G_DBUS_OBJECT(llo->data));

		for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) {
			Bluez5GattDescriptor1 *dsc;

			if (!BLUEZ5_IS_GATT_DESCRIPTOR1(lli->data))
				continue;

			dsc = BLUEZ5_GATT_DESCRIPTOR1(lli->data);

			if (!spa_streq(bluez5_gatt_descriptor1_get_uuid(dsc),
							BT_GATT_CHARACTERISTIC_USER_DESCRIPTION_UUID))
				continue;

			if (spa_streq(bluez5_gatt_descriptor1_get_characteristic(dsc), path)) {
				found = dsc;
				break;
			}
		}
		g_list_free_full(interfaces, g_object_unref);

		if (found)
			break;
	}
	g_list_free_full(objects, g_object_unref);

	return found;
}

static void read_dsc_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
	MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(user_data);
	struct impl *impl = chr->impl;
	gchar *value = NULL;
	GError *err = NULL;

	chr->dsc_done = true;

	bluez5_gatt_descriptor1_call_read_value_finish(
		BLUEZ5_GATT_DESCRIPTOR1(source_object), &value, res, &err);

	if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
		/* Operation canceled: user_data may be invalid by now */
		g_error_free(err);
		goto done;
	}
	if (err) {
		spa_log_error(impl->log, "%s.ReadValue() failed: %s",
				BLUEZ_GATT_DSC_INTERFACE,
				err->message);
		g_error_free(err);
		goto done;
	}

	spa_log_debug(impl->log, "MIDI GATT read probe done for path=%s",
			g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)));

	g_free(chr->description);
	chr->description = value;

	spa_log_debug(impl->log, "MIDI GATT user descriptor value: '%s'",
			chr->description);

	check_chr_node(impl, chr);

done:
	g_clear_object(&chr->dsc_call);
}

static int read_dsc(struct impl *impl, MidiEnumCharacteristicProxy *chr)
{
	Bluez5GattDescriptor1 *dsc;
	GVariant *options;
	GVariantBuilder builder;

	if (chr->dsc_probed)
		return 0;
	if (chr->dsc_call)
		return -EBUSY;

	chr->dsc_probed = true;

	dsc = find_dsc(impl, chr);
	if (dsc == NULL) {
		chr->dsc_done = true;
		return -ENOENT;
	}

	spa_log_debug(impl->log, "MIDI GATT user descriptor read, path=%s",
			g_dbus_proxy_get_object_path(G_DBUS_PROXY(dsc)));

	chr->dsc_call = g_cancellable_new();

	g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
	options = g_variant_builder_end(&builder);

	bluez5_gatt_descriptor1_call_read_value(BLUEZ5_GATT_DESCRIPTOR1(dsc),
			options,
			chr->dsc_call,
			read_dsc_reply,
			chr);

	return 0;
}

static int read_probe_reset(struct impl *impl, MidiEnumCharacteristicProxy *chr)
{
	g_cancellable_cancel(chr->read_call);
	g_clear_object(&chr->read_call);

	g_cancellable_cancel(chr->dsc_call);
	g_clear_object(&chr->dsc_call);

	chr->read_probed = false;
	chr->read_done = false;
	chr->dsc_probed = false;
	chr->dsc_done = false;
	return 0;
}

static void lookup_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr,
		Bluez5GattService1 **service, Bluez5Device1 **device)
{
	GDBusObject *object;
	const char *service_path;
	const char *device_path;

	*service = NULL;
	*device = NULL;

	service_path = bluez5_gatt_characteristic1_get_service(BLUEZ5_GATT_CHARACTERISTIC1(chr));
	if (!service_path)
		return;

	object = g_dbus_object_manager_get_object(dbus_monitor_manager(&impl->monitor), service_path);
	if (object) {
		GDBusInterface *iface = g_dbus_object_get_interface(object, BLUEZ_GATT_SERVICE_INTERFACE);
		*service = BLUEZ5_GATT_SERVICE1(iface);
	}

	if (!*service)
		return;

	device_path = bluez5_gatt_service1_get_device(*service);
	if (!device_path)
		return;

	object = g_dbus_object_manager_get_object(dbus_monitor_manager(&impl->monitor), device_path);
	if (object) {
		GDBusInterface *iface = g_dbus_object_get_interface(object, BLUEZ_DEVICE_INTERFACE);
		*device = BLUEZ5_DEVICE1(iface);
	}
}

static void check_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr)
{
	Bluez5GattService1 *service;
	Bluez5Device1 *device;
	bool available;

	lookup_chr_node(impl, chr, &service, &device);

	if (!device || !bluez5_device1_get_connected(device)) {
		/* Retry read probe on each connection */
		read_probe_reset(impl, chr);
	}

	spa_log_debug(impl->log,
			"At %s, connected:%d resolved:%d",
			g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)),
			bluez5_device1_get_connected(device),
			bluez5_device1_get_services_resolved(device));

	available = service && device &&
			bluez5_device1_get_connected(device) &&
			bluez5_device1_get_services_resolved(device) &&
			spa_streq(bluez5_gatt_service1_get_uuid(service), BT_MIDI_SERVICE_UUID) &&
			spa_streq(bluez5_gatt_characteristic1_get_uuid(BLUEZ5_GATT_CHARACTERISTIC1(chr)),
					BT_MIDI_CHR_UUID);

	if (available && !chr->read_done) {
		read_probe(impl, chr);
		available = false;
	}

	if (available && !chr->dsc_done) {
		read_dsc(impl, chr);
		available = chr->dsc_done;
	}

	if (chr->node_emitted && !available) {
		remove_chr_node(impl, chr);
		chr->node_emitted = false;
	} else if (!chr->node_emitted && available) {
		emit_chr_node(impl, chr, device);
		chr->node_emitted = true;
	}
}

static GList *get_all_valid_chr(struct impl *impl)
{
	GList *lst = NULL;
	GList *objects;

	if (!dbus_monitor_manager(&impl->monitor)) {
		/* Still initializing (or it failed) */
		return NULL;
	}

	objects = g_dbus_object_manager_get_objects(dbus_monitor_manager(&impl->monitor));
	for (GList *p = g_list_first(objects); p; p = p->next) {
		GList *interfaces = g_dbus_object_get_interfaces(G_DBUS_OBJECT(p->data));

		for (GList *p2 = g_list_first(interfaces); p2; p2 = p2->next) {
			MidiEnumCharacteristicProxy *chr;

			if (!MIDI_ENUM_IS_CHARACTERISTIC_PROXY(p2->data))
				continue;

			chr = MIDI_ENUM_CHARACTERISTIC_PROXY(p2->data);
			if (chr->impl == NULL)
				continue;

			lst = g_list_append(lst, g_object_ref(chr));
		}
		g_list_free_full(interfaces, g_object_unref);
	}
	g_list_free_full(objects, g_object_unref);

	return lst;
}

static void check_all_nodes(struct impl *impl)
{
	/*
	 * Check if the nodes we have emitted are in sync with connected devices.
	 */

	GList *chrs = get_all_valid_chr(impl);

	for (GList *p = chrs; p; p = p->next)
		check_chr_node(impl, MIDI_ENUM_CHARACTERISTIC_PROXY(p->data));

	g_list_free_full(chrs, g_object_unref);
}

static void manager_register_application_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
	MidiEnumManagerProxy *manager = MIDI_ENUM_MANAGER_PROXY(source_object);
	struct impl *impl = user_data;
	GError *err = NULL;

	bluez5_gatt_manager1_call_register_application_finish(
		BLUEZ5_GATT_MANAGER1(source_object), res, &err);

	if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
		/* Operation canceled: user_data may be invalid by now */
		g_error_free(err);
		goto done;
	}
	if (err) {
		spa_log_error(impl->log, "%s.RegisterApplication() failed: %s",
				BLUEZ_GATT_MANAGER_INTERFACE,
				err->message);
		g_error_free(err);
		goto done;
	}

	manager->registered = true;

done:
	g_clear_object(&manager->register_call);
}

static int manager_register_application(struct impl *impl, MidiEnumManagerProxy *manager)
{
	GVariantBuilder builder;
	GVariant *options;

	if (manager->registered)
		return 0;
	if (manager->register_call)
		return -EBUSY;

	spa_log_debug(impl->log, "%s.RegisterApplication(%s) on %s",
			BLUEZ_GATT_MANAGER_INTERFACE,
			g_dbus_object_manager_get_object_path(G_DBUS_OBJECT_MANAGER(impl->manager)),
			g_dbus_proxy_get_object_path(G_DBUS_PROXY(manager)));

	manager->register_call = g_cancellable_new();

	g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
	options = g_variant_builder_end(&builder);

	bluez5_gatt_manager1_call_register_application(BLUEZ5_GATT_MANAGER1(manager),
			g_dbus_object_manager_get_object_path(G_DBUS_OBJECT_MANAGER(impl->manager)),
			options,
			manager->register_call,
			manager_register_application_reply,
			impl);

	return 0;
}

/*
 * DBus monitoring (Glib)
 */

static void midi_enum_characteristic_proxy_init(MidiEnumCharacteristicProxy *chr)
{
}

static void midi_enum_characteristic_proxy_finalize(GObject *object)
{
	MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(object);

	g_cancellable_cancel(chr->read_call);
	g_clear_object(&chr->read_call);

	g_cancellable_cancel(chr->dsc_call);
	g_clear_object(&chr->dsc_call);

	if (chr->impl && chr->node_emitted)
		remove_chr_node(chr->impl, chr);

	chr->impl = NULL;

	g_free(chr->description);
	chr->description = NULL;
}

static void midi_enum_characteristic_proxy_class_init(MidiEnumCharacteristicProxyClass *klass)
{
	GObjectClass *object_class = (GObjectClass *) klass;

	object_class->finalize = midi_enum_characteristic_proxy_finalize;
}

static void midi_enum_manager_proxy_init(MidiEnumManagerProxy *manager)
{
}

static void midi_enum_manager_proxy_finalize(GObject *object)
{
	MidiEnumManagerProxy *manager = MIDI_ENUM_MANAGER_PROXY(object);

	g_cancellable_cancel(manager->register_call);
	g_clear_object(&manager->register_call);
}

static void midi_enum_manager_proxy_class_init(MidiEnumManagerProxyClass *klass)
{
	GObjectClass *object_class = (GObjectClass *) klass;

	object_class->finalize = midi_enum_manager_proxy_finalize;
}

static void manager_update(struct dbus_monitor *monitor, GDBusInterface *iface)
{
	struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);

	manager_register_application(impl, MIDI_ENUM_MANAGER_PROXY(iface));
}

static void manager_clear(struct dbus_monitor *monitor, GDBusInterface *iface)
{
	midi_enum_manager_proxy_finalize(G_OBJECT(iface));
}

static void device_update(struct dbus_monitor *monitor, GDBusInterface *iface)
{
	struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);

	check_all_nodes(impl);
}

static void service_update(struct dbus_monitor *monitor, GDBusInterface *iface)
{
	struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
	Bluez5GattService1 *service = BLUEZ5_GATT_SERVICE1(iface);

	if (!spa_streq(bluez5_gatt_service1_get_uuid(service), BT_MIDI_SERVICE_UUID))
		return;

	check_all_nodes(impl);
}

static void chr_update(struct dbus_monitor *monitor, GDBusInterface *iface)
{
	struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
	MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(iface);

	if (!spa_streq(bluez5_gatt_characteristic1_get_uuid(BLUEZ5_GATT_CHARACTERISTIC1(chr)),
					BT_MIDI_CHR_UUID))
		return;

	if (chr->impl == NULL) {
		chr->impl = impl;
		chr->id = ++impl->id;
	}

	check_chr_node(impl, chr);
}

static void chr_clear(struct dbus_monitor *monitor, GDBusInterface *iface)
{
	midi_enum_characteristic_proxy_finalize(G_OBJECT(iface));
}

static void monitor_start(struct impl *impl)
{
	struct dbus_monitor_proxy_type proxy_types[] = {
		{ BLUEZ_DEVICE_INTERFACE, BLUEZ5_TYPE_DEVICE1_PROXY, device_update, NULL },
		{ BLUEZ_GATT_MANAGER_INTERFACE, MIDI_ENUM_TYPE_MANAGER_PROXY, manager_update, manager_clear },
		{ BLUEZ_GATT_SERVICE_INTERFACE, BLUEZ5_TYPE_GATT_SERVICE1_PROXY, service_update, NULL },
		{ BLUEZ_GATT_CHR_INTERFACE, MIDI_ENUM_TYPE_CHARACTERISTIC_PROXY, chr_update, chr_clear },
		{ BLUEZ_GATT_DSC_INTERFACE, BLUEZ5_TYPE_GATT_DESCRIPTOR1_PROXY, NULL, NULL },
		{ NULL, BLUEZ5_TYPE_OBJECT_PROXY, NULL, NULL },
		{ NULL, G_TYPE_INVALID, NULL, NULL }
	};

	SPA_STATIC_ASSERT(SPA_N_ELEMENTS(proxy_types) <= DBUS_MONITOR_MAX_TYPES);

	dbus_monitor_init(&impl->monitor, BLUEZ5_TYPE_OBJECT_MANAGER_CLIENT,
			impl->log, impl->conn, BLUEZ_SERVICE, "/", proxy_types, NULL);
}

/*
 * DBus GATT profile, to enable BlueZ autoconnect
 */

static gboolean profile_handle_release(Bluez5GattProfile1 *iface, GDBusMethodInvocation *invocation)
{
	bluez5_gatt_profile1_complete_release(iface, invocation);
	return TRUE;
}

static int export_profile(struct impl *impl)
{
	static const char *uuids[] = { BT_MIDI_SERVICE_UUID, NULL };
	GDBusObjectSkeleton *skeleton = NULL;
	Bluez5GattProfile1 *iface = NULL;
	int res = -ENOMEM;

	iface = bluez5_gatt_profile1_skeleton_new();
	if (!iface)
		goto done;

	skeleton = g_dbus_object_skeleton_new(MIDI_PROFILE_PATH);
	if (!skeleton)
		goto done;
	g_dbus_object_skeleton_add_interface(skeleton, G_DBUS_INTERFACE_SKELETON(iface));

	bluez5_gatt_profile1_set_uuids(iface, uuids);
	g_signal_connect(iface, "handle-release", G_CALLBACK(profile_handle_release), NULL);

	g_dbus_object_manager_server_export(impl->manager, skeleton);

	spa_log_debug(impl->log, "MIDI GATT Profile exported, path=%s",
			g_dbus_object_get_object_path(G_DBUS_OBJECT(skeleton)));

	res = 0;

done:
	g_clear_object(&iface);
	g_clear_object(&skeleton);
	return res;
}

/*
 * Monitor impl
 */

static int impl_device_add_listener(void *object, struct spa_hook *listener,
		const struct spa_device_events *events, void *data)
{
	struct impl *this = object;
	struct spa_hook_list save;
	GList *chrs;

	spa_return_val_if_fail(this != NULL, -EINVAL);
	spa_return_val_if_fail(events != NULL, -EINVAL);

	chrs = get_all_valid_chr(this);

	spa_hook_list_isolate(&this->hooks, &save, listener, events, data);

	for (GList *p = g_list_first(chrs); p; p = p->next) {
		MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(p->data);
		Bluez5Device1 *device;
		Bluez5GattService1 *service;

		if (!chr->node_emitted)
			continue;

		lookup_chr_node(this, chr, &service, &device);
		if (device)
			emit_chr_node(this, chr, device);
	}
	g_list_free_full(chrs, g_object_unref);

	spa_hook_list_join(&this->hooks, &save);

	return 0;
}

static const struct spa_device_methods impl_device = {
	SPA_VERSION_DEVICE_METHODS,
	.add_listener = impl_device_add_listener,
};

static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
{
	struct impl *this;

	spa_return_val_if_fail(handle != NULL, -EINVAL);
	spa_return_val_if_fail(interface != NULL, -EINVAL);

	this = (struct impl *) handle;

	if (spa_streq(type, SPA_TYPE_INTERFACE_Device))
		*interface = &this->device;
	else
		return -ENOENT;

	return 0;
}

static int impl_clear(struct spa_handle *handle)
{
	struct impl *this;

	this = (struct impl *) handle;

	dbus_monitor_clear(&this->monitor);
	g_clear_object(&this->manager);
	g_clear_object(&this->conn);

	spa_zero(*this);

	return 0;
}

static size_t
impl_get_size(const struct spa_handle_factory *factory,
		const struct spa_dict *params)
{
	return sizeof(struct impl);
}

static int
impl_init(const struct spa_handle_factory *factory,
		struct spa_handle *handle,
		const struct spa_dict *info,
		const struct spa_support *support,
		uint32_t n_support)
{
	struct impl *this;
	GError *error = NULL;
	int res = 0;

	spa_return_val_if_fail(factory != NULL, -EINVAL);
	spa_return_val_if_fail(handle != NULL, -EINVAL);

	handle->get_interface = impl_get_interface;
	handle->clear = impl_clear;

	this = (struct impl *) handle;

	this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);

	if (this->log == NULL)
		return -EINVAL;

	spa_log_topic_init(this->log, &log_topic);

	if (!(info && spa_atob(spa_dict_lookup(info, SPA_KEY_API_GLIB_MAINLOOP)))) {
		spa_log_error(this->log, "Glib mainloop is not usable: %s not set",
				SPA_KEY_API_GLIB_MAINLOOP);
		return -EINVAL;
	}

	spa_hook_list_init(&this->hooks);

	this->device.iface = SPA_INTERFACE_INIT(
		SPA_TYPE_INTERFACE_Device,
				SPA_VERSION_DEVICE,
				&impl_device, this);

	this->conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error);
	if (!this->conn) {
		spa_log_error(this->log, "Creating GDBus connection failed: %s",
				error->message);
		g_error_free(error);
		goto fail;
	}

	g_dbus_connection_set_exit_on_close(this->conn, FALSE);

	this->manager = g_dbus_object_manager_server_new(MIDI_OBJECT_PATH);
	if (!this->manager){
		spa_log_error(this->log, "Creating GDBus object manager failed");
		goto fail;
	}

	if ((res = export_profile(this)) < 0)
		goto fail;

	g_dbus_object_manager_server_set_connection(this->manager, this->conn);

	monitor_start(this);

	return 0;

fail:
	res = (res < 0) ? res : ((errno > 0) ? -errno : -EIO);
	impl_clear(handle);
	return res;
}

static const struct spa_interface_info impl_interfaces[] = {
	{SPA_TYPE_INTERFACE_Device,},
};

static int
impl_enum_interface_info(const struct spa_handle_factory *factory,
		const struct spa_interface_info **info,
		uint32_t *index)
{
	spa_return_val_if_fail(factory != NULL, -EINVAL);
	spa_return_val_if_fail(info != NULL, -EINVAL);
	spa_return_val_if_fail(index != NULL, -EINVAL);

	if (*index >= SPA_N_ELEMENTS(impl_interfaces))
		return 0;

	*info = &impl_interfaces[(*index)++];

	return 1;
}

static const struct spa_dict_item info_items[] = {
	{ SPA_KEY_FACTORY_AUTHOR, "Pauli Virtanen <pav@iki.fi>" },
	{ SPA_KEY_FACTORY_DESCRIPTION, "Bluez5 MIDI connection" },
};

static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);

const struct spa_handle_factory spa_bluez5_midi_enum_factory = {
	SPA_VERSION_HANDLE_FACTORY,
	SPA_NAME_API_BLUEZ5_MIDI_ENUM,
	&info,
	impl_get_size,
	impl_init,
	impl_enum_interface_info,
};
