/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2010-2011 Richard Hughes <richard@hughsie.com>
 * Copyright (C) 2011 Hans de Goede <hdegoede@redhat.com>
 * Copyright (C) 2011 Debarshi Ray <debarshir@src.gnome.org>
 *
 * Licensed under the GNU Lesser General Public License Version 2.1
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 */

/**
 * SECTION:gusb-device
 * @short_description: GLib device integration for libusb
 *
 * This object is a thin glib wrapper around a libusb_device
 */

#include "config.h"

#include <string.h>

#include <libusb-1.0/libusb.h>

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

/**
 * GUsbDevicePrivate:
 *
 * Private #GUsbDevice data
 **/
struct _GUsbDevicePrivate
{
	gchar			*platform_id;
	GUsbContext		*context;
	libusb_device		*device;
	libusb_device_handle	*handle;
	struct libusb_device_descriptor desc;
};

enum {
	PROP_0,
	PROP_LIBUSB_DEVICE,
	PROP_CONTEXT,
	PROP_PLATFORM_ID,
	N_PROPERTIES
};

static GParamSpec *pspecs[N_PROPERTIES] = { NULL, };

static void g_usb_device_initable_iface_init (GInitableIface *iface);

G_DEFINE_TYPE_WITH_CODE (GUsbDevice, g_usb_device, G_TYPE_OBJECT,
                         G_ADD_PRIVATE (GUsbDevice)
                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE,
                                                g_usb_device_initable_iface_init))

/**
 * g_usb_device_error_quark:
 *
 * Return value: Our personal error quark.
 *
 * Since: 0.1.0
 **/
G_DEFINE_QUARK (g-usb-device-error-quark, g_usb_device_error)

static void
g_usb_device_finalize (GObject *object)
{
	GUsbDevice *device = G_USB_DEVICE (object);
	GUsbDevicePrivate *priv = device->priv;

	g_free (priv->platform_id);

	G_OBJECT_CLASS (g_usb_device_parent_class)->finalize (object);
}

static void
g_usb_device_dispose (GObject *object)
{
	GUsbDevice *device = G_USB_DEVICE (object);
	GUsbDevicePrivate *priv = device->priv;

	g_clear_pointer (&priv->device, libusb_unref_device);
	g_clear_object (&priv->context);

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

static void
g_usb_device_get_property (GObject    *object,
                           guint       prop_id,
                           GValue     *value,
                           GParamSpec *pspec)
{
	GUsbDevice *device = G_USB_DEVICE (object);
	GUsbDevicePrivate *priv = device->priv;

	switch (prop_id) {
	case PROP_LIBUSB_DEVICE:
		g_value_set_pointer (value, priv->device);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
g_usb_device_set_property (GObject      *object,
                           guint         prop_id,
                           const GValue *value,
                           GParamSpec   *pspec)
{
	GUsbDevice *device = G_USB_DEVICE (object);
	GUsbDevicePrivate *priv = device->priv;

	switch (prop_id) {
	case PROP_LIBUSB_DEVICE:
		priv->device = g_value_get_pointer (value);
		break;
	case PROP_CONTEXT:
		priv->context = g_value_dup_object (value);
		break;
	case PROP_PLATFORM_ID:
		priv->platform_id = g_value_dup_string (value);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
g_usb_device_constructed (GObject *object)
{
	GUsbDevice *device = G_USB_DEVICE (object);
	GUsbDevicePrivate *priv;
	gint rc;

	priv = device->priv;

	if (!priv->device)
		g_error("constructed without a libusb_device");

	libusb_ref_device(priv->device);

	rc = libusb_get_device_descriptor (priv->device, &priv->desc);
	if (rc != LIBUSB_SUCCESS)
		g_warning ("Failed to get USB descriptor for device: %s",
			   g_usb_strerror (rc));

	G_OBJECT_CLASS (g_usb_device_parent_class)->constructed (object);
}

static void
g_usb_device_class_init (GUsbDeviceClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->finalize = g_usb_device_finalize;
	object_class->dispose = g_usb_device_dispose;
	object_class->get_property = g_usb_device_get_property;
	object_class->set_property = g_usb_device_set_property;
	object_class->constructed = g_usb_device_constructed;

	/**
	 * GUsbDevice:libusb_device:
	 */
	pspecs[PROP_LIBUSB_DEVICE] =
		g_param_spec_pointer ("libusb-device", NULL, NULL,
		                      G_PARAM_CONSTRUCT_ONLY|
		                      G_PARAM_READWRITE);

	/**
	 * GUsbDevice:context:
	 */
	pspecs[PROP_CONTEXT] =
		g_param_spec_object ("context", NULL, NULL,
		                     G_USB_TYPE_CONTEXT,
		                     G_PARAM_CONSTRUCT_ONLY|
		                     G_PARAM_WRITABLE);

	/**
	 * GUsbDevice:platform-id:
	 */
	pspecs[PROP_PLATFORM_ID] =
		g_param_spec_string ("platform-id", NULL, NULL,
		                     NULL,
		                     G_PARAM_CONSTRUCT_ONLY|
		                     G_PARAM_WRITABLE);

	g_object_class_install_properties (object_class, N_PROPERTIES, pspecs);
}

static void
g_usb_device_init (GUsbDevice *device)
{
	device->priv = g_usb_device_get_instance_private (device);
}

static void
g_usb_device_build_platform_id_cb (GString *str, libusb_device *dev)
{
	libusb_device *parent;
	parent = libusb_get_parent (dev);
	if (parent != NULL)
		g_usb_device_build_platform_id_cb (str, parent);
	if (str->len == 0) {
		g_string_append_printf (str, "%02x:",
					libusb_get_bus_number (dev));
	}
	g_string_append_printf (str, "%02x:",
				libusb_get_port_number (dev));
}

static gchar *
g_usb_device_build_platform_id (struct libusb_device *dev)
{
	GString *platform_id;

	/* build a topology of the device */
	platform_id = g_string_new ("usb:");
	g_usb_device_build_platform_id_cb (platform_id, dev);
	g_string_truncate (platform_id, platform_id->len - 1);
	return g_string_free (platform_id, FALSE);
}

static gboolean
g_usb_device_initable_init (GInitable     *initable,
                            GCancellable  *cancellable,
                            GError       **error)
{
	GUsbDevice *device = G_USB_DEVICE (initable);
	GUsbDevicePrivate *priv;
	gint rc;

	priv = device->priv;

	if (!priv->device) {
		g_set_error_literal (error, G_USB_DEVICE_ERROR, G_USB_DEVICE_ERROR_INTERNAL,
		                     "Constructed without a libusb_device");
		return FALSE;
	}

	libusb_ref_device (priv->device);

	rc = libusb_get_device_descriptor (priv->device, &priv->desc);
	if (rc != LIBUSB_SUCCESS) {
		g_set_error (error, G_USB_DEVICE_ERROR, G_USB_DEVICE_ERROR_INTERNAL,
		             "Failed to get USB descriptor for device: %s",
		             g_usb_strerror (rc));
		return FALSE;
	}

	/* this does not change on plug->unplug->plug */
	priv->platform_id = g_usb_device_build_platform_id (priv->device);

	return TRUE;
}

static void
g_usb_device_initable_iface_init (GInitableIface *iface)
{
	iface->init = g_usb_device_initable_init;
}

/**
 * _g_usb_device_new:
 *
 * Return value: a new #GUsbDevice object.
 *
 * Since: 0.1.0
 **/
GUsbDevice *
_g_usb_device_new (GUsbContext    *context,
                   libusb_device  *device,
                   GError        **error)
{
	return g_initable_new (G_USB_TYPE_DEVICE,
	                       NULL, error,
	                       "context", context,
	                       "libusb-device", device,
	                       NULL);
}

/**
 * _g_usb_device_get_device:
 * @device: a #GUsbDevice instance
 *
 * Gets the low-level libusb_device
 *
 * Return value: The #libusb_device or %NULL. Do not unref this value.
 **/
libusb_device *
_g_usb_device_get_device (GUsbDevice *device)
{
	return device->priv->device;
}

static gboolean
g_usb_device_libusb_error_to_gerror (GUsbDevice  *device,
                                     gint         rc,
                                     GError     **error)
{
	gint error_code = G_USB_DEVICE_ERROR_INTERNAL;
	/* Put the rc in libusb's error enum so that gcc warns us if we're
	   missing an error code */
	enum libusb_error result = rc;

	switch (result) {
	case LIBUSB_SUCCESS:
		return TRUE;
	case LIBUSB_ERROR_INVALID_PARAM:
	case LIBUSB_ERROR_NOT_FOUND:
	case LIBUSB_ERROR_NO_MEM:
	case LIBUSB_ERROR_OTHER:
	case LIBUSB_ERROR_INTERRUPTED:
		error_code = G_USB_DEVICE_ERROR_INTERNAL; break;
	case LIBUSB_ERROR_IO:
	case LIBUSB_ERROR_OVERFLOW:
	case LIBUSB_ERROR_PIPE:
		error_code = G_USB_DEVICE_ERROR_IO; break;
	case LIBUSB_ERROR_TIMEOUT:
		error_code = G_USB_DEVICE_ERROR_TIMED_OUT; break;
	case LIBUSB_ERROR_NOT_SUPPORTED:
		error_code = G_USB_DEVICE_ERROR_NOT_SUPPORTED; break;
	case LIBUSB_ERROR_NO_DEVICE:
	case LIBUSB_ERROR_ACCESS:
	case LIBUSB_ERROR_BUSY:
		error_code = G_USB_DEVICE_ERROR_NO_DEVICE; break;
	}

	g_set_error (error, G_USB_DEVICE_ERROR, error_code,
		     "USB error on device %04x:%04x : %s [%i]",
		     g_usb_device_get_vid (device),
		     g_usb_device_get_pid (device),
		     g_usb_strerror (rc), rc);

	return FALSE;
}

static gboolean
g_usb_device_not_open_error (GUsbDevice  *device,
                             GError     **error)
{
	g_set_error (error,
		     G_USB_DEVICE_ERROR,
		     G_USB_DEVICE_ERROR_NOT_OPEN,
		     "Device %04x:%04x has not been opened",
		     g_usb_device_get_vid (device),
		     g_usb_device_get_pid (device));
	return FALSE;
}

static void
g_usb_device_async_not_open_error (GUsbDevice          *device,
                                   GAsyncReadyCallback  callback,
                                   gpointer             user_data,
                                   gpointer             source_tag)
{
	g_task_report_new_error (device, callback, user_data, source_tag,
	                         G_USB_DEVICE_ERROR,
	                         G_USB_DEVICE_ERROR_NOT_OPEN,
	                         "Device %04x:%04x has not been opened",
	                         g_usb_device_get_vid (device),
	                         g_usb_device_get_pid (device));
}

/**
 * g_usb_device_open:
 * @device: a #GUsbDevice
 * @error: a #GError, or %NULL
 *
 * Opens the device for use.
 *
 * Warning: this function is synchronous.
 *
 * Return value: %TRUE on success
 *
 * Since: 0.1.0
 **/
gboolean
g_usb_device_open (GUsbDevice  *device,
                   GError     **error)
{
	gint rc;

	g_return_val_if_fail (G_USB_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	if (device->priv->handle != NULL) {
		g_set_error (error,
			     G_USB_DEVICE_ERROR,
			     G_USB_DEVICE_ERROR_ALREADY_OPEN,
			     "Device %04x:%04x is already open",
			     g_usb_device_get_vid (device),
			     g_usb_device_get_pid (device));
		return FALSE;
	}

	/* open device */
	rc = libusb_open (device->priv->device, &device->priv->handle);
	return g_usb_device_libusb_error_to_gerror (device, rc, error);
}

/**
 * g_usb_device_close:
 * @device: a #GUsbDevice
 * @error: a #GError, or %NULL
 *
 * Closes the device when it is no longer required.
 *
 * Return value: %TRUE on success
 **/
gboolean
g_usb_device_close (GUsbDevice  *device,
                    GError     **error)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	if (device->priv->handle == NULL)
		return g_usb_device_not_open_error (device, error);

	libusb_close (device->priv->handle);
	device->priv->handle = NULL;
	return TRUE;
}

/**
 * g_usb_device_reset:
 * @device: a #GUsbDevice
 * @error: a #GError, or %NULL
 *
 * Perform a USB port reset to reinitialize a device.
 *
 * If the reset succeeds, the device will appear to disconnected and reconnected.
 * This means the @device will no longer be valid and should be closed and
 * rediscovered.
 *
 * This is a blocking function which usually incurs a noticeable delay.
 *
 * Return value: %TRUE on success
 **/
gboolean
g_usb_device_reset (GUsbDevice  *device,
                    GError     **error)
{
	gint rc;
	g_return_val_if_fail (G_USB_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	if (device->priv->handle == NULL)
		return g_usb_device_not_open_error (device, error);

	rc = libusb_reset_device (device->priv->handle);
	return g_usb_device_libusb_error_to_gerror (device, rc, error);
}

/**
 * g_usb_device_get_configuration:
 * @device: a #GUsbDevice
 * @error: a #GError, or %NULL
 *
 * Get the bConfigurationValue for the active configuration of the device.
 *
 * Warning: this function is synchronous.
 *
 * Return value: The bConfigurationValue of the active config, or -1 on error
 *
 * Since: 0.1.0
 **/
gint
g_usb_device_get_configuration (GUsbDevice  *device,
                                GError     **error)
{
	gint rc;
	int config;

	g_return_val_if_fail (G_USB_IS_DEVICE (device), -1);
	g_return_val_if_fail (error == NULL || *error == NULL, -1);

	if (device->priv->handle == NULL) {
		g_usb_device_not_open_error (device, error);
		return -1;
	}

	rc = libusb_get_configuration (device->priv->handle, &config);
	if (rc != LIBUSB_SUCCESS) {
		g_usb_device_libusb_error_to_gerror (device, rc, error);
		return -1;
	}

	return config;
}

/**
 * g_usb_device_set_configuration:
 * @device: a #GUsbDevice
 * @configuration: the configuration value to set
 * @error: a #GError, or %NULL
 *
 * Set the active bConfigurationValue for the device.
 *
 * Warning: this function is synchronous.
 *
 * Return value: %TRUE on success
 *
 * Since: 0.1.0
 **/
gboolean
g_usb_device_set_configuration (GUsbDevice  *device,
                                gint         configuration,
                                GError     **error)
{
	gint rc;
	gint config_tmp = 0;

	g_return_val_if_fail (G_USB_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	if (device->priv->handle == NULL)
		return g_usb_device_not_open_error (device, error);

	/* verify we've not already set the same configuration */
	rc = libusb_get_configuration (device->priv->handle,
				       &config_tmp);
	if (rc != LIBUSB_SUCCESS) {
		return g_usb_device_libusb_error_to_gerror (device,
							    rc,
							    error);
	}
	if (config_tmp == configuration)
		return TRUE;

	/* different, so change */
	rc = libusb_set_configuration (device->priv->handle, configuration);
	return g_usb_device_libusb_error_to_gerror (device, rc, error);
}

/**
 * g_usb_device_claim_interface:
 * @device: a #GUsbDevice
 * @interface: bInterfaceNumber of the interface you wish to claim
 * @flags: #GUsbDeviceClaimInterfaceFlags
 * @error: a #GError, or %NULL
 *
 * Claim an interface of the device.
 *
 * Return value: %TRUE on success
 *
 * Since: 0.1.0
 **/
gboolean
g_usb_device_claim_interface (GUsbDevice                     *device,
                              gint                            interface,
                              GUsbDeviceClaimInterfaceFlags   flags,
                              GError                        **error)
{
	gint rc;

	g_return_val_if_fail (G_USB_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	if (device->priv->handle == NULL)
		return g_usb_device_not_open_error (device, error);

	if (flags & G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER) {
		rc = libusb_detach_kernel_driver (device->priv->handle,
						  interface);
		if (rc != LIBUSB_SUCCESS &&
		    rc != LIBUSB_ERROR_NOT_FOUND && /* No driver attached */
		    rc != LIBUSB_ERROR_NOT_SUPPORTED && /* win32 */
		    rc != LIBUSB_ERROR_BUSY /* driver rebound already */)
			return g_usb_device_libusb_error_to_gerror (device, rc,
								    error);
	}

	rc = libusb_claim_interface (device->priv->handle, interface);
	return g_usb_device_libusb_error_to_gerror (device, rc, error);
}

/**
 * g_usb_device_release_interface:
 * @device: a #GUsbDevice
 * @interface: bInterfaceNumber of the interface you wish to release
 * @flags: #GUsbDeviceClaimInterfaceFlags
 * @error: a #GError, or %NULL
 *
 * Release an interface of the device.
 *
 * Return value: %TRUE on success
 *
 * Since: 0.1.0
 **/
gboolean
g_usb_device_release_interface (GUsbDevice                     *device,
                                gint                            interface,
                                GUsbDeviceClaimInterfaceFlags   flags,
                                GError                        **error)
{
	gint rc;

	g_return_val_if_fail (G_USB_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	if (device->priv->handle == NULL)
		return g_usb_device_not_open_error (device, error);

	rc = libusb_release_interface (device->priv->handle, interface);
	if (rc != LIBUSB_SUCCESS)
		return g_usb_device_libusb_error_to_gerror (device, rc, error);

	if (flags & G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER) {
		rc = libusb_attach_kernel_driver (device->priv->handle,
						  interface);
		if (rc != LIBUSB_SUCCESS &&
		    rc != LIBUSB_ERROR_NOT_FOUND && /* No driver attached */
		    rc != LIBUSB_ERROR_NOT_SUPPORTED && /* win32 */
		    rc != LIBUSB_ERROR_BUSY /* driver rebound already */)
			return g_usb_device_libusb_error_to_gerror (device, rc,
								    error);
	}

	return TRUE;
}

/**
 * g_usb_device_get_string_descriptor:
 * @desc_index: the index for the string descriptor to retreive
 * @error: a #GError, or %NULL
 *
 * Get a string descriptor from the device. The returned string should be freed
 * with g_free() when no longer needed.
 *
 * Return value: a newly-allocated string holding the descriptor, or NULL on error.
 *
 * Since: 0.1.0
 **/
gchar *
g_usb_device_get_string_descriptor (GUsbDevice  *device,
                                    guint8       desc_index,
                                    GError     **error)
{
	gint rc;
	/* libusb_get_string_descriptor_ascii returns max 128 bytes */
	unsigned char buf[128];

	g_return_val_if_fail (G_USB_IS_DEVICE (device), NULL);
	g_return_val_if_fail (error == NULL || *error == NULL, NULL);

	if (device->priv->handle == NULL) {
		g_usb_device_not_open_error (device, error);
		return NULL;
	}

	rc = libusb_get_string_descriptor_ascii (device->priv->handle,
						 desc_index, buf, sizeof(buf));
	if (rc < 0) {
		g_usb_device_libusb_error_to_gerror (device, rc, error);
		return NULL;
	}

	return g_strdup ((const gchar *)buf);
}

typedef gssize (GUsbDeviceTransferFinishFunc) (GUsbDevice *device, GAsyncResult *res, GError **error);

typedef struct {
	GError				**error;
	GMainContext			*context;
	GMainLoop			*loop;
	GUsbDeviceTransferFinishFunc	*finish_func;
	gssize				 ret;
} GUsbSyncHelper;

static void
g_usb_device_sync_transfer_cb (GUsbDevice     *device,
                               GAsyncResult   *res,
                               GUsbSyncHelper *helper)
{
	helper->ret = (*helper->finish_func) (device, res, helper->error);
	g_main_loop_quit (helper->loop);
}

/**
 * g_usb_device_control_transfer:
 * @device: a #GUsbDevice
 * @request_type: the request type field for the setup packet
 * @request: the request field for the setup packet
 * @value: the value field for the setup packet
 * @idx: the index field for the setup packet
 * @data: (array length=length): a suitably-sized data buffer for
 * either input or output
 * @length: the length field for the setup packet.
 * @actual_length: the actual number of bytes sent, or %NULL
 * @timeout: timeout timeout (in millseconds) that this function should wait
 * before giving up due to no response being received. For an unlimited
 * timeout, use 0.
 * @cancellable: a #GCancellable, or %NULL
 * @error: a #GError, or %NULL
 *
 * Perform a USB control transfer.
 *
 * Warning: this function is synchronous, and cannot be cancelled.
 *
 * Return value: %TRUE on success
 *
 * Since: 0.1.0
 **/
gboolean
g_usb_device_control_transfer (GUsbDevice            *device,
                               GUsbDeviceDirection    direction,
                               GUsbDeviceRequestType  request_type,
                               GUsbDeviceRecipient    recipient,
                               guint8                 request,
                               guint16                value,
                               guint16                idx,
                               guint8                *data,
                               gsize                  length,
                               gsize                 *actual_length,
                               guint                  timeout,
                               GCancellable          *cancellable,
                               GError               **error)
{
	GUsbSyncHelper helper;

	helper.ret = -1;
	helper.context = g_main_context_new ();
	helper.loop = g_main_loop_new (helper.context, FALSE);
	helper.error = error;
	helper.finish_func = g_usb_device_control_transfer_finish;

	g_usb_device_control_transfer_async (device,
					     direction,
					     request_type,
					     recipient,
					     request,
					     value,
					     idx,
					     data,
					     length,
					     timeout,
					     cancellable,
					     (GAsyncReadyCallback) g_usb_device_sync_transfer_cb,
					     &helper);
	g_main_loop_run (helper.loop);
	g_main_loop_unref (helper.loop);
	g_main_context_unref (helper.context);

	if (actual_length != NULL)
		*actual_length = (gsize) helper.ret;

	return helper.ret != -1;
}

/**
 * g_usb_device_bulk_transfer:
 * @device: a #GUsbDevice
 * @endpoint: the address of a valid endpoint to communicate with
 * @data: (array length=length): a suitably-sized data buffer for
 * either input or output
 * @length: the length field for the setup packet.
 * @actual_length: the actual number of bytes sent, or %NULL
 * @timeout: timeout timeout (in millseconds) that this function should wait
 * before giving up due to no response being received. For an unlimited
 * timeout, use 0.
 * @cancellable: a #GCancellable, or %NULL
 * @error: a #GError, or %NULL
 *
 * Perform a USB bulk transfer.
 *
 * Warning: this function is synchronous, and cannot be cancelled.
 *
 * Return value: %TRUE on success
 *
 * Since: 0.1.0
 **/
gboolean
g_usb_device_bulk_transfer (GUsbDevice    *device,
                            guint8         endpoint,
                            guint8        *data,
                            gsize          length,
                            gsize         *actual_length,
                            guint          timeout,
                            GCancellable  *cancellable,
                            GError       **error)
{
	GUsbSyncHelper helper;

	helper.ret = -1;
	helper.context = g_main_context_new ();
	helper.loop = g_main_loop_new (helper.context, FALSE);
	helper.error = error;
	helper.finish_func = g_usb_device_bulk_transfer_finish;

	g_usb_device_bulk_transfer_async (device,
					  endpoint,
					  data,
					  length,
					  timeout,
					  cancellable,
					  (GAsyncReadyCallback) g_usb_device_sync_transfer_cb,
					  &helper);
	g_main_loop_run (helper.loop);
	g_main_loop_unref (helper.loop);
	g_main_context_unref (helper.context);

	if (actual_length != NULL)
		*actual_length = (gsize) helper.ret;

	return helper.ret != -1;
}

/**
 * g_usb_device_interrupt_transfer:
 * @device: a #GUsbDevice
 * @endpoint: the address of a valid endpoint to communicate with
 * @data: (array length=length): a suitably-sized data buffer for
 * either input or output
 * @length: the length field for the setup packet.
 * @actual_length: the actual number of bytes sent, or %NULL
 * @timeout: timeout timeout (in millseconds) that this function should wait
 * before giving up due to no response being received. For an unlimited
 * timeout, use 0.
 * @cancellable: a #GCancellable, or %NULL
 * @error: a #GError, or %NULL
 *
 * Perform a USB interrupt transfer.
 *
 * Warning: this function is synchronous, and cannot be cancelled.
 *
 * Return value: %TRUE on success
 *
 * Since: 0.1.0
 **/
gboolean
g_usb_device_interrupt_transfer	(GUsbDevice    *device,
                                 guint8         endpoint,
                                 guint8        *data,
                                 gsize          length,
                                 gsize         *actual_length,
                                 guint          timeout,
                                 GCancellable  *cancellable,
                                 GError       **error)
{
	GUsbSyncHelper helper;

	helper.ret = -1;
	helper.context = g_main_context_new ();
	helper.loop = g_main_loop_new (helper.context, FALSE);
	helper.error = error;
	helper.finish_func = g_usb_device_interrupt_transfer_finish;

	g_usb_device_interrupt_transfer_async (device,
					       endpoint,
					       data,
					       length,
					       timeout,
					       cancellable,
					       (GAsyncReadyCallback) g_usb_device_sync_transfer_cb,
					       &helper);
	g_main_loop_run (helper.loop);
	g_main_loop_unref (helper.loop);
	g_main_context_unref (helper.context);

	if (actual_length != NULL)
		*actual_length = helper.ret;

	return helper.ret != -1;
}

typedef struct {
	GCancellable		*cancellable;
	gulong			 cancellable_id;
	struct libusb_transfer	*transfer;
	guint8			*data; /* owned by the user */
} GcmDeviceReq;

static void
g_usb_device_req_free (GcmDeviceReq *req)
{
	if (req->cancellable_id > 0) {
		g_cancellable_disconnect (req->cancellable,
					  req->cancellable_id);
		g_object_unref (req->cancellable);
	}

	libusb_free_transfer (req->transfer);
	g_slice_free (GcmDeviceReq, req);
}

static gboolean
g_usb_device_libusb_status_to_gerror (gint     status,
                                      GError **error)
{
	gboolean ret = FALSE;

	switch (status) {
	case LIBUSB_TRANSFER_COMPLETED:
		ret = TRUE;
		break;
	case LIBUSB_TRANSFER_ERROR:
		g_set_error_literal (error,
				     G_USB_DEVICE_ERROR,
				     G_USB_DEVICE_ERROR_FAILED,
				     "transfer failed");
		break;
	case LIBUSB_TRANSFER_TIMED_OUT:
		g_set_error_literal (error,
				     G_USB_DEVICE_ERROR,
				     G_USB_DEVICE_ERROR_TIMED_OUT,
				     "transfer timed out");
		break;
	case LIBUSB_TRANSFER_CANCELLED:
		g_set_error_literal (error,
				     G_USB_DEVICE_ERROR,
				     G_USB_DEVICE_ERROR_CANCELLED,
				     "transfer cancelled");
		break;
	case LIBUSB_TRANSFER_STALL:
		g_set_error_literal (error,
				     G_USB_DEVICE_ERROR,
				     G_USB_DEVICE_ERROR_NOT_SUPPORTED,
				     "endpoint stalled or request not supported");
		break;
	case LIBUSB_TRANSFER_NO_DEVICE:
		g_set_error_literal (error,
				     G_USB_DEVICE_ERROR,
				     G_USB_DEVICE_ERROR_NO_DEVICE,
				     "device was disconnected");
		break;
	case LIBUSB_TRANSFER_OVERFLOW:
		g_set_error_literal (error,
				     G_USB_DEVICE_ERROR,
				     G_USB_DEVICE_ERROR_INTERNAL,
				     "device sent more data than requested");
		break;
	default:
		g_set_error (error,
			     G_USB_DEVICE_ERROR,
			     G_USB_DEVICE_ERROR_INTERNAL,
			     "unknown status [%i]", status);
	}
	return ret;
}

static void
g_usb_device_async_transfer_cb (struct libusb_transfer *transfer)
{
	GTask *task = transfer->user_data;
	gboolean ret;
	GError *error = NULL;

	/* did request fail? */
	ret = g_usb_device_libusb_status_to_gerror (transfer->status, &error);
	if (!ret) {
		g_task_return_error (task, error);
	} else {
		g_task_return_int (task, transfer->actual_length);
	}

	g_object_unref (task);
}

static void
g_usb_device_cancelled_cb (GCancellable *cancellable,
                           GcmDeviceReq *req)
{
	libusb_cancel_transfer (req->transfer);
}

static void
g_usb_device_control_transfer_cb (struct libusb_transfer *transfer)
{
	GTask *task = transfer->user_data;
	GcmDeviceReq *req = g_task_get_task_data (task);
	gboolean ret;
	GError *error = NULL;

	/* did request fail? */
	ret = g_usb_device_libusb_status_to_gerror (transfer->status,
						    &error);
	if (!ret) {
		g_task_return_error (task, error);
	} else {
		memmove (req->data,
		         transfer->buffer + LIBUSB_CONTROL_SETUP_SIZE,
		         (gsize) transfer->actual_length);
		g_task_return_int (task, transfer->actual_length);
	}

	g_object_unref (task);
}

/**
 * g_usb_device_control_transfer_async:
 * @device: a #GUsbDevice
 * @data: (array length=length): a suitably-sized data buffer for
 * either input or output
 * @length: the length field for the setup packet.
 * @timeout: timeout timeout (in millseconds) that this function should wait
 * before giving up due to no response being received. For an unlimited
 * timeout, use 0.
 * @cancellable: a #GCancellable, or %NULL
 * @callback: the function to run on completion
 * @user_data: the data to pass to @callback
 *
 * Do an async control transfer
 *
 * Since: 0.1.0
 **/
void
g_usb_device_control_transfer_async (GUsbDevice           *device,
                                     GUsbDeviceDirection   direction,
                                     GUsbDeviceRequestType request_type,
                                     GUsbDeviceRecipient   recipient,
                                     guint8                request,
                                     guint16               value,
                                     guint16               idx,
                                     guint8               *data,
                                     gsize                 length,
                                     guint                 timeout,
                                     GCancellable         *cancellable,
                                     GAsyncReadyCallback   callback,
                                     gpointer              user_data)
{
	GTask *task;
	GcmDeviceReq *req;
	gint rc;
	guint8 request_type_raw = 0;
	guint8 *data_raw;
	GError *error = NULL;

	g_return_if_fail (G_USB_IS_DEVICE (device));

	if (device->priv->handle == NULL) {
		g_usb_device_async_not_open_error (device, callback, user_data,
		                                   g_usb_device_control_transfer_async);
		return;
	}

	req = g_slice_new0 (GcmDeviceReq);
	req->transfer = libusb_alloc_transfer (0);
	req->data = data;

	/* setup cancellation */
	if (cancellable != NULL) {
		req->cancellable = g_object_ref (cancellable);
		req->cancellable_id = g_cancellable_connect (req->cancellable,
		                                             G_CALLBACK (g_usb_device_cancelled_cb),
		                                             req,
		                                             NULL);
	}

	task = g_task_new (device, cancellable, callback, user_data);
	g_task_set_task_data (task, req, (GDestroyNotify)g_usb_device_req_free);

	/* munge back to flags */
	if (direction == G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST)
		request_type_raw |= 0x80;
	request_type_raw |= (request_type << 5);
	request_type_raw |= recipient;

	data_raw = g_malloc0 (length + LIBUSB_CONTROL_SETUP_SIZE);
	memmove (data_raw + LIBUSB_CONTROL_SETUP_SIZE, data, length);

	/* fill in setup packet */
	libusb_fill_control_setup (data_raw, request_type_raw,
	                           request, value, idx, length);

	/* fill in transfer details */
	libusb_fill_control_transfer (req->transfer,
	                              device->priv->handle,
	                              data_raw,
	                              g_usb_device_control_transfer_cb,
	                              task,
	                              timeout);

	/* submit transfer */
	rc = libusb_submit_transfer (req->transfer);
	if (rc < 0) {
		g_usb_device_libusb_error_to_gerror (device, rc, &error);
		g_task_return_error (task, error);
		g_object_unref (task);
	}
}

/**
 * g_usb_device_control_transfer_finish:
 * @device: a #GUsbDevice instance.
 * @res: the #GAsyncResult
 * @error: A #GError or %NULL
 *
 * Gets the result from the asynchronous function.
 *
 * Return value: the actual number of bytes sent, or -1 on error.
 *
 * Since: 0.1.0
 **/
gssize
g_usb_device_control_transfer_finish (GUsbDevice    *device,
                                      GAsyncResult  *res,
                                      GError       **error)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), -1);
	g_return_val_if_fail (g_task_is_valid (res, device), -1);
	g_return_val_if_fail (error == NULL || *error == NULL, -1);

	return g_task_propagate_int (G_TASK (res), error);
}

/**
 * g_usb_device_bulk_transfer_async:
 * @device: a #GUsbDevice instance.
 * @endpoint: the address of a valid endpoint to communicate with
 * @data: (array length=length): a suitably-sized data buffer for
 * either input or output
 * @length: the length field for the setup packet.
 * @timeout: timeout timeout (in millseconds) that this function should wait
 * before giving up due to no response being received. For an unlimited
 * timeout, use 0.
 * @cancellable: a #GCancellable, or %NULL
 * @callback: the function to run on completion
 * @user_data: the data to pass to @callback
 *
 * Do an async bulk transfer
 *
 * Since: 0.1.0
 **/
void
g_usb_device_bulk_transfer_async (GUsbDevice          *device,
                                  guint8               endpoint,
                                  guint8              *data,
                                  gsize                length,
                                  guint                timeout,
                                  GCancellable        *cancellable,
                                  GAsyncReadyCallback  callback,
                                  gpointer             user_data)
{
	GTask *task;
	GcmDeviceReq *req;
	gint rc;
	GError *error = NULL;

	g_return_if_fail (G_USB_IS_DEVICE (device));

	if (device->priv->handle == NULL) {
		g_usb_device_async_not_open_error (device, callback, user_data,
		                                   g_usb_device_bulk_transfer_async);
		return;
	}

	req = g_slice_new0 (GcmDeviceReq);
	req->transfer = libusb_alloc_transfer (0);

	/* setup cancellation */
	if (cancellable != NULL) {
		req->cancellable = g_object_ref (cancellable);
		req->cancellable_id = g_cancellable_connect (req->cancellable,
		                                             G_CALLBACK (g_usb_device_cancelled_cb),
		                                             req,
		                                             NULL);
	}

	task = g_task_new (device, cancellable, callback, user_data);
	g_task_set_task_data (task, req, (GDestroyNotify)g_usb_device_req_free);

	/* fill in transfer details */
	libusb_fill_bulk_transfer (req->transfer,
	                           device->priv->handle,
	                           endpoint,
	                           data,
	                           length,
	                           g_usb_device_async_transfer_cb,
	                           task,
	                           timeout);

	/* submit transfer */
	rc = libusb_submit_transfer (req->transfer);
	if (rc < 0) {
		g_usb_device_libusb_error_to_gerror (device, rc, &error);
		g_task_return_error (task, error);
		g_object_unref (task);
	}
}

/**
 * g_usb_device_bulk_transfer_finish:
 * @device: a #GUsbDevice instance.
 * @res: the #GAsyncResult
 * @error: A #GError or %NULL
 *
 * Gets the result from the asynchronous function.
 *
 * Return value: the actual number of bytes sent, or -1 on error.
 *
 * Since: 0.1.0
 **/
gssize
g_usb_device_bulk_transfer_finish (GUsbDevice    *device,
                                   GAsyncResult  *res,
                                   GError       **error)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), -1);
	g_return_val_if_fail (g_task_is_valid (res, device), -1);
	g_return_val_if_fail (error == NULL || *error == NULL, -1);

	return g_task_propagate_int (G_TASK (res), error);
}

/**
 * g_usb_device_interrupt_transfer_async:
 * @device: a #GUsbDevice instance.
 * @endpoint: the address of a valid endpoint to communicate with
 * @data: (array length=length): a suitably-sized data buffer for
 * either input or output
 * @length: the length field for the setup packet.
 * @timeout: timeout timeout (in millseconds) that this function should wait
 * before giving up due to no response being received. For an unlimited
 * timeout, use 0.
 * @cancellable: a #GCancellable, or %NULL
 * @callback: the function to run on completion
 * @user_data: the data to pass to @callback
 *
 * Do an async interrupt transfer
 *
 * Since: 0.1.0
 **/
void
g_usb_device_interrupt_transfer_async (GUsbDevice          *device,
                                       guint8               endpoint,
                                       guint8              *data,
                                       gsize                length,
                                       guint                timeout,
                                       GCancellable        *cancellable,
                                       GAsyncReadyCallback  callback,
                                       gpointer             user_data)
{
	GTask *task;
	GcmDeviceReq *req;
	GError *error = NULL;
	gint rc;

	g_return_if_fail (G_USB_IS_DEVICE (device));

	if (device->priv->handle == NULL) {
		g_usb_device_async_not_open_error (device, callback, user_data,
		                                   g_usb_device_interrupt_transfer_async);
		return;
	}

	req = g_slice_new0 (GcmDeviceReq);
	req->transfer = libusb_alloc_transfer (0);

	/* setup cancellation */
	if (cancellable != NULL) {
		req->cancellable = g_object_ref (cancellable);
		req->cancellable_id = g_cancellable_connect (req->cancellable,
		                                             G_CALLBACK (g_usb_device_cancelled_cb),
		                                             req,
		                                             NULL);
	}

	task = g_task_new (device, cancellable, callback, user_data);
	g_task_set_task_data (task, req, (GDestroyNotify)g_usb_device_req_free);

	/* fill in transfer details */
	libusb_fill_interrupt_transfer (req->transfer,
	                                device->priv->handle,
	                                endpoint,
	                                data,
	                                length,
	                                g_usb_device_async_transfer_cb,
	                                task,
	                                timeout);

	/* submit transfer */
	rc = libusb_submit_transfer (req->transfer);
	if (rc < 0) {
		g_usb_device_libusb_error_to_gerror (device, rc, &error);
		g_task_return_error (task, error);
		g_object_unref (task);
	}
}

/**
 * g_usb_device_interrupt_transfer_finish:
 * @device: a #GUsbDevice instance.
 * @res: the #GAsyncResult
 * @error: A #GError or %NULL
 *
 * Gets the result from the asynchronous function.
 *
 * Return value: the actual number of bytes sent, or -1 on error.
 *
 * Since: 0.1.0
 **/
gssize
g_usb_device_interrupt_transfer_finish (GUsbDevice    *device,
                                        GAsyncResult  *res,
                                        GError       **error)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), -1);
	g_return_val_if_fail (g_task_is_valid (res, device), -1);
	g_return_val_if_fail (error == NULL || *error == NULL, -1);

	return g_task_propagate_int (G_TASK (res), error);
}

/**
 * g_usb_device_get_platform_id:
 * @device: a #GUsbDevice
 *
 * Gets the platform identifier for the device.
 * On Linux, this is the full sysfs path of the device
 *
 * When the device is removed and then replugged, this value is not expected to
 * be different.
 *
 * Return value: The platform ID, e.g. "usb:02:00:03:01"
 *
 * Since: 0.1.1
 **/
const gchar *
g_usb_device_get_platform_id (GUsbDevice *device)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), NULL);

	return device->priv->platform_id;
}

/**
 * g_usb_device_get_parent:
 * @device: a #GUsbDevice instance
 *
 * Gets the device parent if one exists.
 *
 * Return value: (transfer full): #GUsbDevice or %NULL
 *
 * Since: 0.2.4
 **/
GUsbDevice *
g_usb_device_get_parent (GUsbDevice *device)
{
	GUsbDevicePrivate *priv = device->priv;
	libusb_device *parent;

	parent = libusb_get_parent (priv->device);
	if (parent == NULL)
		return NULL;

	return g_usb_context_find_by_bus_address (priv->context,
						  libusb_get_bus_number (parent),
						  libusb_get_device_address (parent),
						  NULL);
}

/**
 * g_usb_device_get_children:
 * @device: a #GUsbDevice instance
 *
 * Gets the device children if any exist.
 *
 * Return value: (transfer full): an array of #GUsbDevice
 *
 * Since: 0.2.4
 **/
GPtrArray *
g_usb_device_get_children (GUsbDevice *device)
{
	GPtrArray *children;
	GUsbDevice *device_tmp;
	GUsbDevicePrivate *priv = device->priv;
	guint i;
	GPtrArray *devices = NULL;

	/* find any devices that have @device as a parent */
	children = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
	devices = g_usb_context_get_devices (priv->context);
	for (i = 0; i < devices->len; i++) {
		device_tmp = g_ptr_array_index (devices, i);
		if (priv->device == libusb_get_parent (device_tmp->priv->device))
			g_ptr_array_add (children, g_object_ref (device_tmp));
	}

	g_ptr_array_unref (devices);

	return children;
}

/**
 * g_usb_device_get_bus:
 * @device: a #GUsbDevice
 *
 * Gets the USB bus number for the device.
 *
 * Return value: The 8-bit bus number
 *
 * Since: 0.1.0
 **/
guint8
g_usb_device_get_bus (GUsbDevice *device)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), 0);

	return libusb_get_bus_number (device->priv->device);
}

/**
 * g_usb_device_get_address:
 * @device: a #GUsbDevice
 *
 * Gets the USB address for the device.
 *
 * Return value: The 8-bit address
 *
 * Since: 0.1.0
 **/
guint8
g_usb_device_get_address (GUsbDevice *device)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), 0);

	return libusb_get_device_address (device->priv->device);
}

/**
 * g_usb_device_get_port_number:
 * @device: a #GUsbDevice
 *
 * Gets the USB port number for the device.
 *
 * Return value: The 8-bit port number
 *
 * Since: 0.2.4
 **/
guint8
g_usb_device_get_port_number (GUsbDevice *device)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), 0);
	return libusb_get_port_number (device->priv->device);
}

/**
 * g_usb_device_get_vid:
 * @device: a #GUsbDevice
 *
 * Gets the vendor ID for the device.
 *
 * Return value: an ID.
 *
 * Since: 0.1.0
 **/
guint16
g_usb_device_get_vid (GUsbDevice *device)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), 0);

	return device->priv->desc.idVendor;
}

/**
 * g_usb_device_get_pid:
 * @device: a #GUsbDevice
 *
 * Gets the product ID for the device.
 *
 * Return value: an ID.
 *
 * Since: 0.1.0
 **/
guint16
g_usb_device_get_pid (GUsbDevice *device)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), 0);

	return device->priv->desc.idProduct;
}

/**
 * g_usb_device_get_vid_as_str:
 * @device: a #GUsbDevice
 *
 * Gets the vendor ID for the device as a string.
 *
 * Return value: an string ID, or %NULL if not available.
 *
 * Since: 0.2.4
 **/
const gchar *
g_usb_device_get_vid_as_str (GUsbDevice *device)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), NULL);
	return _g_usb_context_lookup_vendor (device->priv->context,
					     device->priv->desc.idVendor,
					     NULL);
}

/**
 * g_usb_device_get_pid_as_str:
 * @device: a #GUsbDevice
 *
 * Gets the product ID for the device as a string.
 *
 * Return value: an string ID, or %NULL if not available.
 *
 * Since: 0.2.4
 **/
const gchar *
g_usb_device_get_pid_as_str (GUsbDevice *device)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), NULL);
	return _g_usb_context_lookup_product (device->priv->context,
					      device->priv->desc.idVendor,
					      device->priv->desc.idProduct,
					      NULL);
}

/**
 * g_usb_device_get_manufacturer_index:
 * @device: a #GUsbDevice
 *
 * Gets the index for the Manufacturer string descriptor.
 *
 * Return value: a string descriptor index.
 *
 * Since: 0.1.0
 **/
guint8
g_usb_device_get_manufacturer_index (GUsbDevice *device)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), 0);

	return device->priv->desc.iManufacturer;
}

/**
 * g_usb_device_get_device_class:
 * @device: a #GUsbDevice
 *
 * Gets the device class.
 *
 * Return value: a device class number, e.g. 0x09 is a USB hub.
 *
 * Since: 0.1.7
 **/
guint8
g_usb_device_get_device_class (GUsbDevice *device)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), 0);

	return device->priv->desc.bDeviceClass;
}

/**
 * g_usb_device_get_device_subclass:
 * @device: a #GUsbDevice
 *
 * Gets the device subclass qualified by the class number.
 * See g_usb_device_get_device_class().
 *
 * Return value: a device subclass number.
 *
 * Since: 0.2.4
 **/
guint8
g_usb_device_get_device_subclass (GUsbDevice *device)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), 0);

	return device->priv->desc.bDeviceSubClass;
}

/**
 * g_usb_device_get_device_protocol:
 * @device: a #GUsbDevice
 *
 * Gets the device protocol qualified by the class and subclass numbers.
 * See g_usb_device_get_device_class() and g_usb_device_get_device_subclass().
 *
 * Return value: a device protocol number.
 *
 * Since: 0.2.4
 **/
guint8
g_usb_device_get_device_protocol (GUsbDevice *device)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), 0);

	return device->priv->desc.bDeviceProtocol;
}

/**
 * g_usb_device_get_product_index:
 * @device: a #GUsbDevice
 *
 * Gets the index for the Product string descriptor.
 *
 * Return value: a string descriptor index.
 *
 * Since: 0.1.0
 **/
guint8
g_usb_device_get_product_index (GUsbDevice *device)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), 0);

	return device->priv->desc.iProduct;
}

/**
 * g_usb_device_get_serial_number_index:
 * @device: a #GUsbDevice
 *
 * Gets the index for the Serial Number string descriptor.
 *
 * Return value: a string descriptor index.
 *
 * Since: 0.1.0
 **/
guint8
g_usb_device_get_serial_number_index (GUsbDevice *device)
{
	g_return_val_if_fail (G_USB_IS_DEVICE (device), 0);

	return device->priv->desc.iSerialNumber;
}
