/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2011 Richard Hughes <richard@hughsie.com>
 * Copyright (C) 2011 Hans de Goede <hdegoede@redhat.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

/**
 * SECTION:gusb-context
 * @short_description: Per-thread instance integration for libusb
 *
 * This object is used to get a context that is thread safe.
 */

#include "config.h"

#include <libusb.h>

#include "gusb-context-private.h"
#include "gusb-device-private.h"
#include "gusb-util.h"

enum { PROP_0, PROP_LIBUSB_CONTEXT, PROP_DEBUG_LEVEL, N_PROPERTIES };

enum { DEVICE_ADDED_SIGNAL, DEVICE_REMOVED_SIGNAL, DEVICE_CHANGED_SIGNAL, LAST_SIGNAL };

#define G_USB_CONTEXT_HOTPLUG_POLL_INTERVAL_DEFAULT 1000 /* ms */

#define GET_PRIVATE(o) (g_usb_context_get_instance_private(o))

/**
 * GUsbContextPrivate:
 *
 * Private #GUsbContext data
 **/
typedef struct {
	GMainContext *main_ctx;
	GPtrArray *devices;
	GPtrArray *devices_removed;
	GHashTable *dict_usb_ids;
	GHashTable *dict_replug;
	GThread *thread_event;
	gboolean done_enumerate;
	volatile gint thread_event_run;
	guint hotplug_poll_id;
	guint hotplug_poll_interval;
	int debug_level;
	GUsbContextFlags flags;
	libusb_context *ctx;
	libusb_hotplug_callback_handle hotplug_id;
	GPtrArray *idle_events;
	GMutex idle_events_mutex;
	guint idle_events_id;
} GUsbContextPrivate;

/* not defined in FreeBSD */
#ifndef HAVE_LIBUSB_CAP_HAS_HOTPLUG
#define LIBUSB_CAP_HAS_HOTPLUG 0x0001
#endif

typedef struct {
	GMainLoop *loop;
	GUsbDevice *device;
	guint timeout_id;
} GUsbContextReplugHelper;

static guint signals[LAST_SIGNAL] = {0};
static GParamSpec *pspecs[N_PROPERTIES] = {
    NULL,
};

static void
g_usb_context_initable_iface_init(GInitableIface *iface);

G_DEFINE_TYPE_WITH_CODE(GUsbContext,
			g_usb_context,
			G_TYPE_OBJECT,
			G_ADD_PRIVATE(GUsbContext)
			    G_IMPLEMENT_INTERFACE(G_TYPE_INITABLE,
						  g_usb_context_initable_iface_init))

/* not defined in FreeBSD */
#ifndef HAVE_LIBUSB_HAS_CAPABILITY
static gboolean
libusb_has_capability(int cap)
{
	if (cap == LIBUSB_CAP_HAS_HOTPLUG)
		return TRUE;
	return FALSE;
}
#endif

static void
g_usb_context_replug_helper_free(GUsbContextReplugHelper *replug_helper)
{
	if (replug_helper->timeout_id != 0)
		g_source_remove(replug_helper->timeout_id);
	g_main_loop_unref(replug_helper->loop);
	g_object_unref(replug_helper->device);
	g_free(replug_helper);
}

/* clang-format off */
/**
 * g_usb_context_error_quark:
 *
 * Return value: Our personal error quark.
 *
 * Since: 0.1.0
 **/
G_DEFINE_QUARK (g-usb-context-error-quark, g_usb_context_error)
/* clang-format on */

static void
g_usb_context_dispose(GObject *object)
{
	GUsbContext *self = G_USB_CONTEXT(object);
	GUsbContextPrivate *priv = GET_PRIVATE(self);

	/* this is safe to call even when priv->hotplug_id is unset */
	if (g_atomic_int_dec_and_test(&priv->thread_event_run)) {
		libusb_hotplug_deregister_callback(priv->ctx, priv->hotplug_id);
		g_thread_join(priv->thread_event);
	}

	if (priv->hotplug_poll_id > 0) {
		g_source_remove(priv->hotplug_poll_id);
		priv->hotplug_poll_id = 0;
	}
	if (priv->idle_events_id > 0) {
		g_source_remove(priv->idle_events_id);
		priv->idle_events_id = 0;
	}

	g_clear_pointer(&priv->main_ctx, g_main_context_unref);
	g_clear_pointer(&priv->devices, g_ptr_array_unref);
	g_clear_pointer(&priv->devices_removed, g_ptr_array_unref);
	g_clear_pointer(&priv->dict_usb_ids, g_hash_table_unref);
	g_clear_pointer(&priv->dict_replug, g_hash_table_unref);
	g_clear_pointer(&priv->ctx, libusb_exit);
	g_clear_pointer(&priv->idle_events, g_ptr_array_unref);
	g_mutex_clear(&priv->idle_events_mutex);

	G_OBJECT_CLASS(g_usb_context_parent_class)->dispose(object);
}

static void
g_usb_context_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
	GUsbContext *self = G_USB_CONTEXT(object);
	GUsbContextPrivate *priv = GET_PRIVATE(self);

	switch (prop_id) {
	case PROP_LIBUSB_CONTEXT:
		g_value_set_pointer(value, priv->ctx);
		break;
	case PROP_DEBUG_LEVEL:
		g_value_set_int(value, priv->debug_level);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
		break;
	}
}

static void
g_usb_context_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
	GUsbContext *self = G_USB_CONTEXT(object);
	GUsbContextPrivate *priv = GET_PRIVATE(self);

	switch (prop_id) {
	case PROP_DEBUG_LEVEL:
		priv->debug_level = g_value_get_int(value);
#ifdef HAVE_LIBUSB_SET_OPTION
		libusb_set_option(priv->ctx, LIBUSB_OPTION_LOG_LEVEL, priv->debug_level);
#else
		libusb_set_debug(priv->ctx, priv->debug_level);
#endif
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
		break;
	}
}

static void
g_usb_context_class_init(GUsbContextClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);

	object_class->dispose = g_usb_context_dispose;
	object_class->get_property = g_usb_context_get_property;
	object_class->set_property = g_usb_context_set_property;

	/**
	 * GUsbContext:libusb_context:
	 */
	pspecs[PROP_LIBUSB_CONTEXT] =
	    g_param_spec_pointer("libusb_context", NULL, NULL, G_PARAM_READABLE);

	/**
	 * GUsbContext:debug_level:
	 */
	pspecs[PROP_DEBUG_LEVEL] =
	    g_param_spec_int("debug_level", NULL, NULL, 0, 3, 0, G_PARAM_READWRITE);

	g_object_class_install_properties(object_class, N_PROPERTIES, pspecs);

	/**
	 * GUsbContext::device-added:
	 * @self: the #GUsbContext instance that emitted the signal
	 * @device: A #GUsbDevice
	 *
	 * This signal is emitted when a USB device is added.
	 **/
	signals[DEVICE_ADDED_SIGNAL] = g_signal_new("device-added",
						    G_TYPE_FROM_CLASS(klass),
						    G_SIGNAL_RUN_LAST,
						    G_STRUCT_OFFSET(GUsbContextClass, device_added),
						    NULL,
						    NULL,
						    g_cclosure_marshal_VOID__OBJECT,
						    G_TYPE_NONE,
						    1,
						    G_USB_TYPE_DEVICE);

	/**
	 * GUsbContext::device-removed:
	 * @self: the #GUsbContext instance that emitted the signal
	 * @device: A #GUsbDevice
	 *
	 * This signal is emitted when a USB device is removed.
	 **/
	signals[DEVICE_REMOVED_SIGNAL] =
	    g_signal_new("device-removed",
			 G_TYPE_FROM_CLASS(klass),
			 G_SIGNAL_RUN_LAST,
			 G_STRUCT_OFFSET(GUsbContextClass, device_removed),
			 NULL,
			 NULL,
			 g_cclosure_marshal_VOID__OBJECT,
			 G_TYPE_NONE,
			 1,
			 G_USB_TYPE_DEVICE);

	/**
	 * GUsbContext::device-changed:
	 * @self: the #GUsbContext instance that emitted the signal
	 * @device: A #GUsbDevice
	 *
	 * This signal is emitted when a USB device is changed.
	 **/
	signals[DEVICE_CHANGED_SIGNAL] =
	    g_signal_new("device-changed",
			 G_TYPE_FROM_CLASS(klass),
			 G_SIGNAL_RUN_LAST,
			 G_STRUCT_OFFSET(GUsbContextClass, device_changed),
			 NULL,
			 NULL,
			 g_cclosure_marshal_VOID__OBJECT,
			 G_TYPE_NONE,
			 1,
			 G_USB_TYPE_DEVICE);
}

static void
g_usb_context_emit_device_add(GUsbContext *self, GUsbDevice *device)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);

	/* emitted directly by g_usb_context_enumerate */
	if (!priv->done_enumerate)
		return;

	if (_g_usb_context_has_flag(self, G_USB_CONTEXT_FLAGS_DEBUG))
		g_debug("emitting ::device-added(%s)", g_usb_device_get_platform_id(device));
	g_signal_emit(self, signals[DEVICE_ADDED_SIGNAL], 0, device);
}

static void
g_usb_context_emit_device_remove(GUsbContext *self, GUsbDevice *device)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);

	/* should not happen, if it does we would not fire any signal */
	if (!priv->done_enumerate)
		return;

	if (_g_usb_context_has_flag(self, G_USB_CONTEXT_FLAGS_DEBUG))
		g_debug("emitting ::device-removed(%s)", g_usb_device_get_platform_id(device));
	g_signal_emit(self, signals[DEVICE_REMOVED_SIGNAL], 0, device);
}

static void
g_usb_context_emit_device_changed(GUsbContext *self, GUsbDevice *device)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);

	/* should not happen, if it does we would not fire any signal */
	if (!priv->done_enumerate)
		return;

	if (_g_usb_context_has_flag(self, G_USB_CONTEXT_FLAGS_DEBUG))
		g_debug("emitting ::device-changed(%s)", g_usb_device_get_platform_id(device));
	g_signal_emit(self, signals[DEVICE_CHANGED_SIGNAL], 0, device);
}

static void
g_usb_context_add_device(GUsbContext *self, struct libusb_device *dev)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	GUsbContextReplugHelper *replug_helper;
	const gchar *platform_id;
	guint8 bus;
	guint8 address;
	g_autoptr(GError) error = NULL;
	g_autoptr(GUsbDevice) device = NULL;

	/* does any existing device exist */
	bus = libusb_get_bus_number(dev);
	address = libusb_get_device_address(dev);

	if (priv->done_enumerate)
		device = g_usb_context_find_by_bus_address(self, bus, address, NULL);
	if (device != NULL)
		return;

	/* add the device */
	device = _g_usb_device_new(self, dev, &error);
	if (device == NULL) {
		g_debug("There was a problem creating the device: %s", error->message);
		return;
	}

	/* auto-open */
	if (priv->flags & G_USB_CONTEXT_FLAGS_AUTO_OPEN_DEVICES) {
		if (!_g_usb_device_open_internal(device, &error)) {
			g_warning("cannot open the device: %s", error->message);
			return;
		}
	}

	/* add to enumerated list */
	g_ptr_array_add(priv->devices, g_object_ref(device));

	/* if we're waiting for replug, suppress the signal */
	platform_id = g_usb_device_get_platform_id(device);
	replug_helper = g_hash_table_lookup(priv->dict_replug, platform_id);
	if (replug_helper != NULL) {
		g_debug("%s is in replug, ignoring add", platform_id);
		g_object_unref(replug_helper->device);
		replug_helper->device = g_object_ref(device);
		g_main_loop_quit(replug_helper->loop);
		return;
	}

	/* emit signal */
	g_usb_context_emit_device_add(self, device);
}

static void
g_usb_context_remove_device(GUsbContext *self, struct libusb_device *dev)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	GUsbContextReplugHelper *replug_helper;
	const gchar *platform_id;
	guint8 bus;
	guint8 address;
	g_autoptr(GUsbDevice) device = NULL;

	/* does any existing device exist */
	bus = libusb_get_bus_number(dev);
	address = libusb_get_device_address(dev);
	device = g_usb_context_find_by_bus_address(self, bus, address, NULL);
	if (device == NULL) {
		g_debug("%i:%i does not exist", bus, address);
		return;
	}

	/* save this to a lookaside */
	if (priv->flags & G_USB_CONTEXT_FLAGS_SAVE_EVENTS) {
		g_ptr_array_add(priv->devices_removed, g_object_ref(device));
	}

	/* remove from enumerated list */
	g_ptr_array_remove(priv->devices, device);

	/* if we're waiting for replug, suppress the signal */
	platform_id = g_usb_device_get_platform_id(device);
	replug_helper = g_hash_table_lookup(priv->dict_replug, platform_id);
	if (replug_helper != NULL) {
		g_debug("%s is in replug, ignoring remove", platform_id);
		return;
	}

	/* emit signal */
	g_usb_context_emit_device_remove(self, device);
}

/**
 * g_usb_context_load:
 * @self: a #GUsbContext
 * @json_object: a #JsonObject
 * @error: a #GError, or %NULL
 *
 * Loads the context from a JSON object.
 *
 * Return value: %TRUE on success
 *
 * Since: 0.4.0
 **/
gboolean
g_usb_context_load(GUsbContext *self, JsonObject *json_object, GError **error)
{
	return g_usb_context_load_with_tag(self, json_object, NULL, error);
}

/**
 * g_usb_context_load_with_tag:
 * @self: a #GUsbContext
 * @json_object: a #JsonObject
 * @tag: a string tag, e.g. `runtime-reload`, or %NULL
 * @error: a #GError, or %NULL
 *
 * Loads any devices with a specified tag into the context from a JSON object.
 *
 * Return value: %TRUE on success
 *
 * Since: 0.4.1
 **/
gboolean
g_usb_context_load_with_tag(GUsbContext *self,
			    JsonObject *json_object,
			    const gchar *tag,
			    GError **error)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	JsonArray *json_array;
	g_autoptr(GPtrArray) devices_added =
	    g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
	g_autoptr(GPtrArray) devices_remove =
	    g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);

	g_return_val_if_fail(G_USB_IS_CONTEXT(self), FALSE);
	g_return_val_if_fail(json_object != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* if not already set */
	priv->done_enumerate = TRUE;

	/* sanity check */
	if (!json_object_has_member(json_object, "UsbDevices")) {
		g_set_error_literal(error,
				    G_IO_ERROR,
				    G_IO_ERROR_INVALID_DATA,
				    "no UsbDevices array");
		return FALSE;
	}

	/* four steps:
	 *
	 * 1. store all the existing devices matching the tag in devices_remove
	 * 2. read the devices in the array:
	 *    - if the platform-id exists: replace the event data & remove from devices_remove
	 *    - otherwise add to devices_added
	 * 3. emit devices in devices_remove
	 * 4. emit devices in devices_added
	 */
	for (guint i = 0; i < priv->devices->len; i++) {
		GUsbDevice *device = g_ptr_array_index(priv->devices, i);
		if (tag == NULL || g_usb_device_has_tag(device, tag))
			g_ptr_array_add(devices_remove, g_object_ref(device));
	}
	json_array = json_object_get_array_member(json_object, "UsbDevices");
	for (guint i = 0; i < json_array_get_length(json_array); i++) {
		JsonNode *node_tmp = json_array_get_element(json_array, i);
		JsonObject *obj_tmp = json_node_get_object(node_tmp);
		g_autoptr(GUsbDevice) device_old = NULL;
		g_autoptr(GUsbDevice) device_tmp =
		    g_object_new(G_USB_TYPE_DEVICE, "context", self, NULL);
		if (!_g_usb_device_load(device_tmp, obj_tmp, error))
			return FALSE;
		if (tag != NULL && !g_usb_device_has_tag(device_tmp, tag))
			continue;

		/* does a device with this platform ID [and the same created date] already exist */
		device_old =
		    g_usb_context_find_by_platform_id(self,
						      g_usb_device_get_platform_id(device_tmp),
						      NULL);
		if (device_old != NULL && g_date_time_equal(g_usb_device_get_created(device_old),
							    g_usb_device_get_created(device_tmp))) {
			g_autoptr(GPtrArray) events = g_usb_device_get_events(device_tmp);
			g_usb_device_clear_events(device_old);
			for (guint j = 0; j < events->len; j++) {
				GUsbDeviceEvent *event = g_ptr_array_index(events, j);
				_g_usb_device_add_event(device_old, event);
			}
			g_usb_context_emit_device_changed(self, device_old);
			g_ptr_array_remove(devices_remove, device_old);
			continue;
		}

		/* new to us! */
		g_ptr_array_add(devices_added, g_object_ref(device_tmp));
	}

	/* emit removes then adds */
	for (guint i = 0; i < devices_remove->len; i++) {
		GUsbDevice *device = g_ptr_array_index(devices_remove, i);
		g_usb_context_emit_device_remove(self, device);
		g_ptr_array_remove(priv->devices, device);
	}
	for (guint i = 0; i < devices_added->len; i++) {
		GUsbDevice *device = g_ptr_array_index(devices_added, i);
		g_ptr_array_add(priv->devices, g_object_ref(device));
		g_usb_context_emit_device_add(self, device);
	}

	/* success */
	return TRUE;
}

/**
 * g_usb_context_save:
 * @self: a #GUsbContext
 * @json_builder: a #JsonBuilder
 * @error: a #GError, or %NULL
 *
 * Saves the context to an existing JSON builder.
 *
 * Return value: %TRUE on success
 *
 * Since: 0.4.0
 **/
gboolean
g_usb_context_save(GUsbContext *self, JsonBuilder *json_builder, GError **error)
{
	return g_usb_context_save_with_tag(self, json_builder, NULL, error);
}

/**
 * g_usb_context_save_with_tag:
 * @self: a #GUsbContext
 * @json_builder: a #JsonBuilder
 * @tag: a string tag, e.g. `runtime-reload`, or %NULL
 * @error: a #GError, or %NULL
 *
 * Saves any devices with a specified tag into an existing JSON builder.
 *
 * Return value: %TRUE on success
 *
 * Since: 0.4.1
 **/
gboolean
g_usb_context_save_with_tag(GUsbContext *self,
			    JsonBuilder *json_builder,
			    const gchar *tag,
			    GError **error)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);

	g_return_val_if_fail(G_USB_IS_CONTEXT(self), FALSE);
	g_return_val_if_fail(json_builder != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* start */
	g_usb_context_enumerate(self);
	json_builder_begin_object(json_builder);

	/* array of devices */
	json_builder_set_member_name(json_builder, "UsbDevices");
	json_builder_begin_array(json_builder);
	if (priv->flags & G_USB_CONTEXT_FLAGS_SAVE_REMOVED_DEVICES) {
		for (guint i = 0; i < priv->devices_removed->len; i++) {
			GUsbDevice *device = g_ptr_array_index(priv->devices_removed, i);
			if (!_g_usb_device_save(device, json_builder, error))
				return FALSE;
		}
	}
	for (guint i = 0; i < priv->devices->len; i++) {
		GUsbDevice *device = g_ptr_array_index(priv->devices, i);
		if (tag != NULL && !g_usb_device_has_tag(device, tag))
			continue;
		if (!_g_usb_device_save(device, json_builder, error))
			return FALSE;
	}
	json_builder_end_array(json_builder);

	/* success */
	json_builder_end_object(json_builder);
	return TRUE;
}

typedef struct {
	GUsbContext *self;
	libusb_device *dev;
	libusb_hotplug_event event;
} GUsbContextIdleHelper;

static void
g_usb_context_idle_helper_free(GUsbContextIdleHelper *helper)
{
	g_object_unref(helper->self);
	libusb_unref_device(helper->dev);
	g_free(helper);
}

static gpointer
g_usb_context_idle_helper_copy(gconstpointer src, gpointer user_data)
{
	GUsbContextIdleHelper *helper_src = (GUsbContextIdleHelper *)src;
	GUsbContextIdleHelper *helper_dst = g_new0(GUsbContextIdleHelper, 1);
	helper_dst->self = g_object_ref(helper_src->self);
	helper_dst->dev = libusb_ref_device(helper_src->dev);
	helper_dst->event = helper_src->event;
	return helper_dst;
}

/* always in the main thread */
static gboolean
g_usb_context_idle_hotplug_cb(gpointer user_data)
{
	GUsbContext *self = G_USB_CONTEXT(user_data);
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	g_autoptr(GPtrArray) idle_events = NULL;

	/* drain the idle events with the lock held */
	g_mutex_lock(&priv->idle_events_mutex);
	idle_events = g_ptr_array_copy(priv->idle_events, g_usb_context_idle_helper_copy, NULL);
	g_ptr_array_set_size(priv->idle_events, 0);
	priv->idle_events_id = 0;
	g_mutex_unlock(&priv->idle_events_mutex);

	/* run the callbacks when not locked */
	for (guint i = 0; i < idle_events->len; i++) {
		GUsbContextIdleHelper *helper = g_ptr_array_index(idle_events, i);
		switch (helper->event) {
		case LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED:
			g_usb_context_add_device(helper->self, helper->dev);
			break;
		case LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT:
			g_usb_context_remove_device(helper->self, helper->dev);
			break;
		default:
			break;
		}
	}

	/* all done */
	return FALSE;
}

/* this is run in the libusb thread */
static int LIBUSB_CALL
g_usb_context_hotplug_cb(struct libusb_context *ctx,
			 struct libusb_device *dev,
			 libusb_hotplug_event event,
			 void *user_data)
{
	GUsbContext *self = G_USB_CONTEXT(user_data);
	GUsbContextIdleHelper *helper;
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&priv->idle_events_mutex);

	g_assert(locker != NULL);

	/* libusb is returning devices but LIBUSB_HOTPLUG_ENUMERATE is not set! */
	if (!priv->done_enumerate)
		return 0;

	helper = g_new0(GUsbContextIdleHelper, 1);
	helper->self = g_object_ref(self);
	helper->dev = libusb_ref_device(dev);
	helper->event = event;

	g_ptr_array_add(priv->idle_events, helper);
	if (priv->idle_events_id == 0)
		priv->idle_events_id = g_idle_add(g_usb_context_idle_hotplug_cb, self);

	return 0;
}

static void
g_usb_context_rescan(GUsbContext *self)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	libusb_device **dev_list = NULL;
	g_autoptr(GList) existing_devices = NULL;

	/* copy to a context so we can remove from the array */
	for (guint i = 0; i < priv->devices->len; i++) {
		GUsbDevice *device = g_ptr_array_index(priv->devices, i);
		existing_devices = g_list_prepend(existing_devices, device);
	}

	/* look for any removed devices */
	libusb_get_device_list(priv->ctx, &dev_list);
	for (GList *l = existing_devices; l != NULL; l = l->next) {
		GUsbDevice *device = G_USB_DEVICE(l->data);
		gboolean found = FALSE;
		for (guint i = 0; dev_list != NULL && dev_list[i] != NULL; i++) {
			if (libusb_get_bus_number(dev_list[i]) == g_usb_device_get_bus(device) &&
			    libusb_get_device_address(dev_list[i]) ==
				g_usb_device_get_address(device)) {
				found = TRUE;
				break;
			}
		}
		if (!found) {
			g_usb_context_emit_device_remove(self, device);
			g_ptr_array_remove(priv->devices, device);
		}
	}

	/* add any devices not yet added (duplicates will be filtered */
	for (guint i = 0; dev_list != NULL && dev_list[i] != NULL; i++)
		g_usb_context_add_device(self, dev_list[i]);

	libusb_free_device_list(dev_list, 1);
}

static gboolean
g_usb_context_rescan_cb(gpointer user_data)
{
	GUsbContext *self = G_USB_CONTEXT(user_data);
	g_usb_context_rescan(self);
	return TRUE;
}

/**
 * g_usb_context_get_main_context:
 * @self: a #GUsbContext
 *
 * Gets the internal GMainContext to use for synchronous methods.
 * By default the value is set to the value of g_main_context_default()
 *
 * Return value: (transfer none): the #GMainContext
 *
 * Since: 0.2.5
 **/
GMainContext *
g_usb_context_get_main_context(GUsbContext *self)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(G_USB_IS_CONTEXT(self), NULL);
	return priv->main_ctx;
}

/**
 * g_usb_context_set_main_context:
 * @self: a #GUsbContext
 *
 * Sets the internal GMainContext to use for synchronous methods.
 *
 * Since: 0.2.5
 **/
void
g_usb_context_set_main_context(GUsbContext *self, GMainContext *main_ctx)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);

	g_return_if_fail(G_USB_IS_CONTEXT(self));

	if (main_ctx != priv->main_ctx) {
		g_main_context_unref(priv->main_ctx);
		priv->main_ctx = g_main_context_ref(main_ctx);
	}
}

static void
g_usb_context_ensure_rescan_timeout(GUsbContext *self)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);

	if (priv->hotplug_poll_id > 0) {
		g_source_remove(priv->hotplug_poll_id);
		priv->hotplug_poll_id = 0;
	}
	if (priv->hotplug_poll_interval > 0) {
		priv->hotplug_poll_id =
		    g_timeout_add(priv->hotplug_poll_interval, g_usb_context_rescan_cb, self);
	}
}

/**
 * g_usb_context_get_hotplug_poll_interval:
 * @self: a #GUsbContext
 *
 * Gets the poll interval for platforms like Windows that do not support `LIBUSB_CAP_HAS_HOTPLUG`.
 *
 * Return value: interval in ms
 *
 * Since: 0.3.10
 **/
guint
g_usb_context_get_hotplug_poll_interval(GUsbContext *self)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(G_USB_IS_CONTEXT(self), G_MAXUINT);
	return priv->hotplug_poll_id;
}

/**
 * g_usb_context_set_hotplug_poll_interval:
 * @self: a #GUsbContext
 * @hotplug_poll_interval: the interval in ms
 *
 * Sets the poll interval for platforms like Windows that do not support `LIBUSB_CAP_HAS_HOTPLUG`.
 * This defaults to 1000ms and can be changed before or after g_usb_context_enumerate() has been
 * called.
 *
 * Since: 0.3.10
 **/
void
g_usb_context_set_hotplug_poll_interval(GUsbContext *self, guint hotplug_poll_interval)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);

	g_return_if_fail(G_USB_IS_CONTEXT(self));

	/* same */
	if (priv->hotplug_poll_interval == hotplug_poll_interval)
		return;

	priv->hotplug_poll_interval = hotplug_poll_interval;

	/* if already running then change the existing timeout */
	if (priv->hotplug_poll_id > 0)
		g_usb_context_ensure_rescan_timeout(self);
}

/**
 * g_usb_context_enumerate:
 * @self: a #GUsbContext
 *
 * Enumerates all the USB devices and adds them to the context.
 *
 * You only need to call this function once, and any subsequent calls
 * are silently ignored.
 *
 * Since: 0.2.2
 **/
void
g_usb_context_enumerate(GUsbContext *self)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);

	/* only ever initially scan once, then rely on hotplug / poll */
	if (priv->done_enumerate)
		return;

	g_usb_context_rescan(self);
	if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) {
		g_debug("platform does not do hotplug, using polling");
		g_usb_context_ensure_rescan_timeout(self);
	}
	priv->done_enumerate = TRUE;

	/* emit device-added signals before returning */
	for (guint i = 0; i < priv->devices->len; i++) {
		g_signal_emit(self,
			      signals[DEVICE_ADDED_SIGNAL],
			      0,
			      g_ptr_array_index(priv->devices, i));
	}

	/* any queued up hotplug events are queued as idle handlers */
}

/**
 * g_usb_context_set_flags:
 * @self: a #GUsbContext
 * @flags: some #GUsbContextFlags, e.g. %G_USB_CONTEXT_FLAGS_AUTO_OPEN_DEVICES
 *
 * Sets the flags to use for the context. These should be set before
 * g_usb_context_enumerate() is called.
 *
 * Since: 0.2.11
 **/
void
g_usb_context_set_flags(GUsbContext *self, GUsbContextFlags flags)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	priv->flags = flags;
}

/**
 * g_usb_context_get_flags:
 * @self: a #GUsbContext
 *
 * Sets the flags to use for the context.
 *
 * Return value: the #GUsbContextFlags, e.g. %G_USB_CONTEXT_FLAGS_AUTO_OPEN_DEVICES
 *
 * Since: 0.2.11
 **/
GUsbContextFlags
g_usb_context_get_flags(GUsbContext *self)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	return priv->flags;
}

gboolean
_g_usb_context_has_flag(GUsbContext *self, GUsbContextFlags flag)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	return (priv->flags & flag) > 0;
}

static gpointer
g_usb_context_event_thread_cb(gpointer data)
{
	GUsbContext *self = G_USB_CONTEXT(data);
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	struct timeval tv = {
	    .tv_usec = 0,
	    .tv_sec = 2,
	};

	while (g_atomic_int_get(&priv->thread_event_run) > 0)
		libusb_handle_events_timeout_completed(priv->ctx, &tv, NULL);

	return NULL;
}

static void
g_usb_context_init(GUsbContext *self)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);

	priv->flags = G_USB_CONTEXT_FLAGS_NONE;
	priv->hotplug_poll_interval = G_USB_CONTEXT_HOTPLUG_POLL_INTERVAL_DEFAULT;
	priv->devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
	priv->devices_removed = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
	priv->dict_usb_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
	priv->dict_replug = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);

	/* to escape the thread into the mainloop */
	g_mutex_init(&priv->idle_events_mutex);
	priv->idle_events =
	    g_ptr_array_new_with_free_func((GDestroyNotify)g_usb_context_idle_helper_free);
}

static gboolean
g_usb_context_initable_init(GInitable *initable, GCancellable *cancellable, GError **error)
{
	GUsbContext *self = G_USB_CONTEXT(initable);
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	gint rc;
	libusb_context *ctx;

	rc = libusb_init(&ctx);
	if (rc < 0) {
		g_set_error(error,
			    G_USB_CONTEXT_ERROR,
			    G_USB_CONTEXT_ERROR_INTERNAL,
			    "failed to init libusb: %s [%i]",
			    g_usb_strerror(rc),
			    rc);
		return FALSE;
	}

	priv->main_ctx = g_main_context_ref(g_main_context_default());
	priv->ctx = ctx;
	priv->thread_event_run = 1;
	priv->thread_event = g_thread_new("GUsbEventThread", g_usb_context_event_thread_cb, self);

	/* watch for add/remove */
	if (libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) {
		rc = libusb_hotplug_register_callback(priv->ctx,
						      LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED |
							  LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT,
						      0,
						      LIBUSB_HOTPLUG_MATCH_ANY,
						      LIBUSB_HOTPLUG_MATCH_ANY,
						      LIBUSB_HOTPLUG_MATCH_ANY,
						      g_usb_context_hotplug_cb,
						      self,
						      &priv->hotplug_id);
		if (rc != LIBUSB_SUCCESS) {
			g_warning("Error creating a hotplug callback: %s", g_usb_strerror(rc));
		}
	}

	return TRUE;
}

static void
g_usb_context_initable_iface_init(GInitableIface *iface)
{
	iface->init = g_usb_context_initable_init;
}

/**
 * _g_usb_context_get_context:
 * @self: a #GUsbContext
 *
 * Gets the internal libusb_context.
 *
 * Return value: (transfer none): the libusb_context
 *
 * Since: 0.1.0
 **/
libusb_context *
_g_usb_context_get_context(GUsbContext *self)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	return priv->ctx;
}

/**
 * g_usb_context_get_source:
 * @self: a #GUsbContext
 * @main_ctx: a #GMainContext, or %NULL
 *
 * This function does nothing.
 *
 * Return value: (transfer none): the #GUsbSource.
 *
 * Since: 0.1.0
 **/
GUsbSource *
g_usb_context_get_source(GUsbContext *self, GMainContext *main_ctx)
{
	return NULL;
}

/**
 * g_usb_context_set_debug:
 * @self: a #GUsbContext
 * @flags: a GLogLevelFlags such as %G_LOG_LEVEL_ERROR | %G_LOG_LEVEL_INFO, or 0
 *
 * Sets the debug flags which control what is logged to the console.
 *
 * Using %G_LOG_LEVEL_INFO will output to standard out, and everything
 * else logs to standard error.
 *
 * Since: 0.1.0
 **/
void
g_usb_context_set_debug(GUsbContext *self, GLogLevelFlags flags)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	int debug_level;

	g_return_if_fail(G_USB_IS_CONTEXT(self));

	if (flags & (G_LOG_LEVEL_DEBUG | G_LOG_LEVEL_INFO))
		debug_level = 3;
	else if (flags & (G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_WARNING))
		debug_level = 2;
	else if (flags & (G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_ERROR))
		debug_level = 1;
	else
		debug_level = 0;

	if (debug_level != priv->debug_level) {
		priv->debug_level = debug_level;
#ifdef HAVE_LIBUSB_SET_OPTION
		libusb_set_option(priv->ctx, LIBUSB_OPTION_LOG_LEVEL, debug_level);
#else
		libusb_set_debug(priv->ctx, debug_level);
#endif

		g_object_notify_by_pspec(G_OBJECT(self), pspecs[PROP_DEBUG_LEVEL]);
	}
}

/**
 * g_usb_context_find_by_bus_address:
 * @self: a #GUsbContext
 * @bus: a bus number
 * @address: a bus address
 * @error: A #GError or %NULL
 *
 * Finds a device based on its bus and address values.
 *
 * Return value: (transfer full): a new #GUsbDevice, or %NULL if not found.
 *
 * Since: 0.2.2
 **/
GUsbDevice *
g_usb_context_find_by_bus_address(GUsbContext *self, guint8 bus, guint8 address, GError **error)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);

	g_return_val_if_fail(G_USB_IS_CONTEXT(self), NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	g_usb_context_enumerate(self);
	for (guint i = 0; i < priv->devices->len; i++) {
		GUsbDevice *device = g_ptr_array_index(priv->devices, i);
		if (g_usb_device_get_bus(device) == bus &&
		    g_usb_device_get_address(device) == address) {
			return g_object_ref(device);
		}
	}
	g_set_error(error,
		    G_USB_DEVICE_ERROR,
		    G_USB_DEVICE_ERROR_NO_DEVICE,
		    "Failed to find device %04x:%04x",
		    bus,
		    address);
	return NULL;
}

/**
 * g_usb_context_find_by_platform_id:
 * @self: a #GUsbContext
 * @platform_id: a platform id, e.g. "usb:00:03:03:02"
 * @error: A #GError or %NULL
 *
 * Finds a device based on its platform id value.
 *
 * Return value: (transfer full): a new #GUsbDevice, or %NULL if not found.
 *
 * Since: 0.2.4
 **/
GUsbDevice *
g_usb_context_find_by_platform_id(GUsbContext *self, const gchar *platform_id, GError **error)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);

	g_return_val_if_fail(G_USB_IS_CONTEXT(self), NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	g_usb_context_enumerate(self);
	for (guint i = 0; i < priv->devices->len; i++) {
		GUsbDevice *device = g_ptr_array_index(priv->devices, i);
		if (g_strcmp0(g_usb_device_get_platform_id(device), platform_id) == 0)
			return g_object_ref(device);
	}
	g_set_error(error,
		    G_USB_DEVICE_ERROR,
		    G_USB_DEVICE_ERROR_NO_DEVICE,
		    "Failed to find device %s",
		    platform_id);
	return NULL;
}

/**
 * g_usb_context_find_by_vid_pid:
 * @self: a #GUsbContext
 * @vid: a vendor ID
 * @pid: a product ID
 * @error: A #GError or %NULL
 *
 * Finds a device based on its bus and address values.
 *
 * Return value: (transfer full): a new #GUsbDevice, or %NULL if not found.
 *
 * Since: 0.2.2
 **/
GUsbDevice *
g_usb_context_find_by_vid_pid(GUsbContext *self, guint16 vid, guint16 pid, GError **error)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);

	g_return_val_if_fail(G_USB_IS_CONTEXT(self), NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	g_usb_context_enumerate(self);
	for (guint i = 0; i < priv->devices->len; i++) {
		GUsbDevice *device = g_ptr_array_index(priv->devices, i);
		if (g_usb_device_get_vid(device) == vid && g_usb_device_get_pid(device) == pid) {
			return g_object_ref(device);
		}
	}
	g_set_error(error,
		    G_USB_DEVICE_ERROR,
		    G_USB_DEVICE_ERROR_NO_DEVICE,
		    "Failed to find device %04x:%04x",
		    vid,
		    pid);
	return NULL;
}

static gboolean
g_usb_context_load_usb_ids(GUsbContext *self, GError **error)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	guint16 pid;
	guint16 vid = 0x0000;
	g_autofree gchar *data = NULL;
	g_auto(GStrv) lines = NULL;

	/* already loaded */
	if (g_hash_table_size(priv->dict_usb_ids) > 0)
		return TRUE;

	/* parse */
	if (!g_file_get_contents(USB_IDS, &data, NULL, error))
		return FALSE;

	lines = g_strsplit(data, "\n", -1);
	for (guint i = 0; lines[i] != NULL; i++) {
		if (lines[i][0] == '#')
			continue;

		if (lines[i][0] == '\0')
			continue;

		/* the list of known device classes, subclasses and protocols appeared */
		if (g_str_has_prefix(lines[i], "C 00"))
			break;

		if (lines[i][0] != '\t') {
			if (strlen(lines[i]) < 7)
				continue;
			lines[i][4] = '\0';
			vid = g_ascii_strtoull(lines[i], NULL, 16);
			if (vid == 0)
				break;

			g_hash_table_insert(priv->dict_usb_ids,
					    g_strdup(lines[i]),
					    g_strdup(lines[i] + 6));
		} else {
			if (vid == 0x0000)
				break;

			if (strlen(lines[i]) < 8)
				continue;
			lines[i][5] = '\0';
			pid = g_ascii_strtoull(lines[i] + 1, NULL, 16);
			g_hash_table_insert(priv->dict_usb_ids,
					    g_strdup_printf("%04x:%04x", vid, pid),
					    g_strdup(lines[i] + 7));
		}
	}

	return TRUE;
}

/**
 * _g_usb_context_lookup_vendor:
 * @self: a #GUsbContext
 * @vid: a USB vendor ID
 * @error: a #GError, or %NULL
 *
 * Returns the vendor name using usb.ids.
 *
 * Return value: the description, or %NULL
 *
 * Since: 0.1.5
 **/
const gchar *
_g_usb_context_lookup_vendor(GUsbContext *self, guint16 vid, GError **error)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	const gchar *tmp;
	g_autofree gchar *key = NULL;

	g_return_val_if_fail(G_USB_IS_CONTEXT(self), NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	/* load */
	if (!g_usb_context_load_usb_ids(self, error))
		return NULL;

	/* find */
	key = g_strdup_printf("%04x", vid);
	tmp = g_hash_table_lookup(priv->dict_usb_ids, key);
	if (tmp == NULL) {
		g_set_error(error,
			    G_USB_CONTEXT_ERROR,
			    G_USB_CONTEXT_ERROR_INTERNAL,
			    "failed to find vid %s",
			    key);
		return NULL;
	}

	return tmp;
}

/**
 * _g_usb_context_lookup_product:
 * @self: a #GUsbContext
 * @vid: a USB vendor ID
 * @pid: a USB product ID
 * @error: a #GError, or %NULL
 *
 * Returns the product name using usb.ids.
 *
 * Return value: the description, or %NULL
 *
 * Since: 0.1.5
 **/
const gchar *
_g_usb_context_lookup_product(GUsbContext *self, guint16 vid, guint16 pid, GError **error)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	const gchar *tmp;
	g_autofree gchar *key = NULL;

	g_return_val_if_fail(G_USB_IS_CONTEXT(self), NULL);
	g_return_val_if_fail(error == NULL || *error == NULL, NULL);

	/* load */
	if (!g_usb_context_load_usb_ids(self, error))
		return NULL;

	/* find */
	key = g_strdup_printf("%04x:%04x", vid, pid);
	tmp = g_hash_table_lookup(priv->dict_usb_ids, key);
	if (tmp == NULL) {
		g_set_error(error,
			    G_USB_CONTEXT_ERROR,
			    G_USB_CONTEXT_ERROR_INTERNAL,
			    "failed to find vid %s",
			    key);
		return NULL;
	}

	return tmp;
}

/**
 * g_usb_context_get_devices:
 * @self: a #GUsbContext
 *
 * Return value: (transfer full) (element-type GUsbDevice): a new #GPtrArray of #GUsbDevice's.
 *
 * Since: 0.2.2
 **/
GPtrArray *
g_usb_context_get_devices(GUsbContext *self)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);

	g_return_val_if_fail(G_USB_IS_CONTEXT(self), NULL);

	g_usb_context_enumerate(self);

	return g_ptr_array_ref(priv->devices);
}

static gboolean
g_usb_context_replug_timeout_cb(gpointer user_data)
{
	GUsbContextReplugHelper *replug_helper = (GUsbContextReplugHelper *)user_data;
	replug_helper->timeout_id = 0;
	g_main_loop_quit(replug_helper->loop);
	return FALSE;
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC(GUsbContextReplugHelper, g_usb_context_replug_helper_free)

/**
 * g_usb_context_wait_for_replug:
 * @self: a #GUsbContext
 * @device: a #GUsbDevice
 * @timeout_ms: timeout to wait
 * @error: A #GError or %NULL
 *
 * Waits for the device to be replugged.
 * It may come back with a different VID:PID.
 *
 * Warning: This is synchronous and blocks until the device comes
 * back or the timeout triggers.
 *
 * Return value: (transfer full): a new #GUsbDevice, or %NULL for invalid
 *
 * Since: 0.2.9
 **/
GUsbDevice *
g_usb_context_wait_for_replug(GUsbContext *self,
			      GUsbDevice *device,
			      guint timeout_ms,
			      GError **error)
{
	GUsbContextPrivate *priv = GET_PRIVATE(self);
	const gchar *platform_id;
	g_autoptr(GUsbContextReplugHelper) replug_helper = NULL;

	g_return_val_if_fail(G_USB_IS_CONTEXT(self), NULL);

	/* create a helper */
	replug_helper = g_new0(GUsbContextReplugHelper, 1);
	replug_helper->device = g_object_ref(device);
	replug_helper->loop = g_main_loop_new(priv->main_ctx, FALSE);
	replug_helper->timeout_id =
	    g_timeout_add(timeout_ms, g_usb_context_replug_timeout_cb, replug_helper);

	/* register */
	platform_id = g_usb_device_get_platform_id(device);
	g_hash_table_insert(priv->dict_replug, g_strdup(platform_id), replug_helper);

	/* wait for timeout, or replug */
	g_main_loop_run(replug_helper->loop);

	/* unregister */
	g_hash_table_remove(priv->dict_replug, platform_id);

	/* so we timed out; emit the removal now */
	if (replug_helper->timeout_id == 0) {
		g_usb_context_emit_device_remove(self, replug_helper->device);
		g_set_error_literal(error,
				    G_USB_CONTEXT_ERROR,
				    G_USB_CONTEXT_ERROR_INTERNAL,
				    "request timed out");
		return NULL;
	}
	return g_object_ref(replug_helper->device);
}

/**
 * g_usb_context_new:
 * @error: a #GError, or %NULL
 *
 * Creates a new context for accessing USB devices.
 *
 * Return value: a new %GUsbContext object or %NULL on error.
 *
 * Since: 0.1.0
 **/
GUsbContext *
g_usb_context_new(GError **error)
{
	return g_initable_new(G_USB_TYPE_CONTEXT, NULL, error, NULL);
}
