#include <errno.h>
#include <string.h>
#include <stdlib.h>
#ifdef __linux__
#include <linux/usb/cdc.h>
#else
#define USB_CDC_SUBCLASS_ACM			0x02

#define USB_CDC_PROTO_NONE			0
#define USB_CDC_ACM_PROTO_AT_V25TER		1
#endif
#ifdef __linux__
#include <linux/usb/ch9.h>
#else
#include <stdint.h>

#define USB_DT_INTERFACE_ASSOCIATION	0x0b

#define USB_CLASS_COMM			2
#define USB_CLASS_CDC_DATA		0x0a

struct usb_descriptor_header {
	uint8_t  bLength;
	uint8_t  bDescriptorType;
} __attribute__ ((packed));

struct usb_interface_assoc_descriptor {
	uint8_t  bLength;
	uint8_t  bDescriptorType;

	uint8_t  bFirstInterface;
	uint8_t  bInterfaceCount;
	uint8_t  bFunctionClass;
	uint8_t  bFunctionSubClass;
	uint8_t  bFunctionProtocol;
	uint8_t  iFunction;
} __attribute__ ((packed));
#endif
#include <libusb-1.0/libusb.h>

#include "thor.h"
#include "thor_internal.h"
#include "thor_transport.h"

#define MAX_SERIAL_LEN 256

struct hotplug_helper {
	struct usb_device_handle *uh;
	struct usb_device_id *dev_id;
	int completed;
};

struct t_usb_transfer;

typedef void (*t_usb_transfer_cb)(struct t_usb_transfer *);

struct t_usb_transfer {
	struct libusb_transfer *ltransfer;
	t_usb_transfer_cb transfer_finished;
	off_t size;
	int ret;
	int cancelled;
};

struct t_thor_usb_chunk {
	struct t_usb_transfer data_transfer;
	struct t_usb_transfer resp_transfer;
	void *user_data;
	off_t useful_size;
	struct data_res_pkt resp;
	unsigned char *buf;
	off_t trans_unit_size;
	int chunk_number;
	int data_finished;
	int resp_finished;
};

struct t_thor_usb_transfer {
	thor_device_handle *th;
	struct thor_data_src *data;
	thor_progress_cb report_progress;
	void *user_data;
	off_t data_left;
	off_t data_sent;
	off_t data_in_progress;
	int chunk_number;
	int completed;
	int ret;
};

static struct usb_device_id *thor_choose_id(struct thor_device_id *dev_id)
{
	static struct usb_device_id default_id = {
		.busid = NULL,
		.vid = 0x04e8,
		.pid = 0x685d,
		.serial = NULL,
	};
	struct usb_device_id *usb_dev_id = &dev_id->usb_dev;

	if (usb_dev_id->busid == NULL
	    && usb_dev_id->vid < 0
	    && usb_dev_id->pid < 0
	    && usb_dev_id->serial == NULL)
		usb_dev_id = &default_id;

	return usb_dev_id;
}

static int check_busid_match(const char *expected, libusb_device *dev)
{
	/* Max USB depth is 7 */
	uint8_t dev_port[8];
	int nports;
	uint8_t bus_number;
	int val;
	int i;
	int ret;

	bus_number = libusb_get_bus_number(dev);
	ret = sscanf(expected, "%d", &val);
	if (ret < 1)
		return -EINVAL;

	if (val != bus_number)
		return 0;

	expected = strchr(expected, '-');
	if (!expected)
		return -EINVAL;
	++expected;

	nports = libusb_get_port_numbers(dev, (uint8_t *)dev_port, sizeof(dev_port));
	if (nports < 0)
		return nports;


	for (i = 0; i < nports; ++i) {
		ret = sscanf(expected, "%d", &val);
		if (ret < 1)
			return -EINVAL;

		if (val != dev_port[i])
			return 0;

		expected = strchr(expected, '.');
		if (!expected) {
			if (i + 1 == nports)
				return 1;
			else
				break;
		}
		++expected;
	}

	return 0;
}

static int check_vid_pid_match(int vid, int pid, libusb_device *dev)
{
	struct libusb_device_descriptor desc;
	int ret;

	ret = libusb_get_device_descriptor(dev, &desc);
	if (ret < 0)
		return ret;

	if (vid >= 0 && vid != desc.idVendor)
		return 0;

	if (pid >= 0 && pid != desc.idProduct)
		return 0;

	return 1;
}

static int check_serial_match(const char *serial, libusb_device *dev,
		       libusb_device_handle **devh)
{
	char buf[MAX_SERIAL_LEN];
	struct libusb_device_descriptor desc;
	libusb_device_handle *handle;
	int ret;

	ret = libusb_get_device_descriptor(dev, &desc);
	if (ret < 0)
		return ret;

	ret = libusb_open(dev, &handle);
	if (ret < 0)
		return ret;

	ret = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber,
						 (unsigned char*)buf,
						 sizeof(buf));
	if (ret < 0)
		return ret;

	if (strcmp(serial, buf))
		return 0;

	*devh = handle;
	return 1;
}

static inline int
is_data_interface(const struct libusb_interface_descriptor *idesc)
{
	return idesc->bInterfaceClass == USB_CLASS_CDC_DATA;
}

static inline int
is_control_interface(const struct libusb_interface_descriptor *idesc)
{
	return idesc->bInterfaceClass == USB_CLASS_COMM
		&& idesc->bInterfaceSubClass == USB_CDC_SUBCLASS_ACM
		&& idesc->bInterfaceProtocol == USB_CDC_ACM_PROTO_AT_V25TER;
}

static int find_idesc_by_id(struct libusb_config_descriptor *cdesc, int id)
{
	int i;

	for (i = 0; i < cdesc->bNumInterfaces; ++i)
		if (cdesc->interface[i].altsetting[0].bInterfaceNumber == id)
			return i;

	return -ENODEV;
}

static int check_assoc(struct libusb_config_descriptor *cdesc,
		       struct usb_interface_assoc_descriptor *assoc_desc,
		       struct usb_device_handle *uh)
{
	int intf_a, intf_b;

	if (assoc_desc->bInterfaceCount != 2
	    || assoc_desc->bFunctionClass != USB_CLASS_COMM
	    || assoc_desc->bFunctionSubClass != USB_CDC_SUBCLASS_ACM
	    || assoc_desc->bFunctionProtocol != USB_CDC_PROTO_NONE)
		return -EINVAL;

	intf_a = find_idesc_by_id(cdesc, assoc_desc->bFirstInterface);
	intf_b = find_idesc_by_id(cdesc, assoc_desc->bFirstInterface + 1);

	if (is_data_interface(cdesc->interface[intf_a].altsetting + 0)
	    && is_control_interface(cdesc->interface[intf_b].altsetting + 0)) {
		uh->data_interface = intf_a;
		uh->data_interface_id = assoc_desc->bFirstInterface;
		uh->control_interface = intf_b;
		uh->control_interface_id = assoc_desc->bFirstInterface + 1;
	} else if (is_control_interface(cdesc->interface[intf_a].altsetting + 0)
		   && is_data_interface(cdesc->interface[intf_b].altsetting + 0)) {
		uh->data_interface = intf_b;
		uh->data_interface_id = assoc_desc->bFirstInterface + 1;
		uh->control_interface = intf_a;
		uh->control_interface_id = assoc_desc->bFirstInterface;
	} else {
		return -ENODEV;
	}

	return 0;
}

static int find_interfaces(struct libusb_config_descriptor *cdesc,
			   struct usb_device_handle *uh)
{
	struct usb_descriptor_header *header;
	struct usb_interface_assoc_descriptor *assoc_desc = NULL;
	int assoc_valid = 0;
	int pos;
	int ret;

	/* Try to find IAD and use it */
	pos = 0;
	for (; pos < cdesc->extra_length; pos += header->bLength) {
		header = (struct usb_descriptor_header *)(cdesc->extra + pos);
		if (header->bDescriptorType != USB_DT_INTERFACE_ASSOCIATION)
			continue;

		if (pos + sizeof(assoc_desc) > cdesc->extra_length)
			break;

		assoc_desc = (struct usb_interface_assoc_descriptor *)header;
		ret = check_assoc(cdesc, assoc_desc, uh);
		if (!ret) {
			assoc_valid = 1;
			break;
		}
	}

	/*
	 * If we were unable to find IAD let's
	 * just try to manually find interfaces
	 */
	if (!assoc_valid) {
		int i;
#define get_intf_desc(_intf) (&(cdesc->interface[_intf].altsetting[0]))
		uh->data_interface = -1;
		uh->control_interface = -1;

		for (i = 0; i < cdesc->bNumInterfaces; ++i) {
			if (!is_data_interface(get_intf_desc(i)))
				continue;

			uh->data_interface = i;
			uh->data_interface_id =
				get_intf_desc(i)->bInterfaceNumber;
			break;
		}

		if (uh->data_interface < 0)
			return -ENODEV;

		for (i = 0; i < cdesc->bNumInterfaces; ++i) {
			if (!is_control_interface(get_intf_desc(i)))
				continue;
			uh->control_interface = i;
			uh->control_interface_id =
				get_intf_desc(i)->bInterfaceNumber;
		}

		if (uh->control_interface < 0)
			return -ENODEV;
#undef get_intf_desc
	}

	return 0;
}

static int find_data_eps(struct libusb_config_descriptor *cdesc,
			 struct usb_device_handle *uh)
{
	const struct libusb_interface_descriptor *idesc;
	int i;

	idesc = cdesc->interface[uh->data_interface_id].altsetting + 0;

	if (idesc->bNumEndpoints != 2)
		return -EINVAL;

	uh->data_ep_in = -1;
	uh->data_ep_out = -1;

	for (i = 0; i < idesc->bNumEndpoints; ++i) {
		if ((idesc->endpoint[i].bmAttributes & 0x03) !=
		    LIBUSB_TRANSFER_TYPE_BULK)
			return -1;
		if ((idesc->endpoint[i].bEndpointAddress & (1 << 7))
		    == LIBUSB_ENDPOINT_IN)
			uh->data_ep_in = idesc->endpoint[i].bEndpointAddress;
		else
			uh->data_ep_out = idesc->endpoint[i].bEndpointAddress;
	}

	if (uh->data_ep_in < 0 || uh->data_ep_out < 0)
		return -EINVAL;

	return 0;
}

static int find_intf_and_eps(libusb_device *dev,
			     struct usb_device_handle *uh)
{
	struct libusb_config_descriptor *cdesc;
	int ret;

	ret = libusb_get_active_config_descriptor(dev, &cdesc);
	if (ret < 0)
		return ret;

	ret = find_interfaces(cdesc, uh);
	if (ret) {
		ret = -ENODEV;
		goto cleanup_desc;
	}

	ret = find_data_eps(cdesc, uh);
	if (ret) {
		ret = -ENODEV;
		goto cleanup_desc;
	}

	ret = 0;
cleanup_desc:
	libusb_free_config_descriptor(cdesc);
	return ret;
}

static int claim_intf(struct usb_device_handle *uh)
{
	int ret;

	/*
	 * Check if our OS allows us to detach kernel driver.
	 * If yes then we mark this device as auto-detach and try to claim
	 * our interfaces. libusb will detach kernel driver, if any when we
	 * will try to claim interface.
	 * If our os doesn't support detaching kernel driver we simply try
	 * to claim our interfaces. If we fail it means that probably there
	 * is some kernel driver bound to this device but we cannot do anything
	 * with this.
	 */
	ret = libusb_has_capability(LIBUSB_CAP_SUPPORTS_DETACH_KERNEL_DRIVER);
	if (ret) {
		ret = libusb_set_auto_detach_kernel_driver(uh->devh, 1);
		if (ret < 0)
			goto out;
	}

	ret = libusb_claim_interface(uh->devh, uh->data_interface_id);
	if (ret < 0)
		goto out;

	ret = libusb_claim_interface(uh->devh, uh->control_interface_id);
	if (ret < 0)
		goto release_data;

	return 0;

release_data:
	libusb_release_interface(uh->devh, uh->data_interface);
out:
	return ret;
}

static int check_device_match(struct usb_device_id *dev_id,
			      libusb_device *dev,
			      struct usb_device_handle *uh)
{
	int ret;

	if (dev_id->busid) {
		ret = check_busid_match(dev_id->busid, dev);
		if (ret <= 0)
			goto no_match;
	}

	if (dev_id->vid >= 0 || dev_id->pid >= 0) {
		ret = check_vid_pid_match(dev_id->vid, dev_id->pid, dev);
		if (ret <= 0)
			goto no_match;
	}

	if (dev_id->serial) {
		ret = check_serial_match(dev_id->serial, dev, &uh->devh);
		if (ret <= 0)
			goto no_match;
	} else {
		ret = libusb_open(dev, &uh->devh);
		if (ret < 0)
			goto no_match;
	}

	ret = find_intf_and_eps(dev, uh);
	if (ret < 0)
		goto err;

	ret = claim_intf(uh);
	if (ret < 0)
		goto err;

	return 1;
err:
	libusb_close(uh->devh);
no_match:
	return 0;
}

static int find_existing_device(struct usb_device_id *dev_id,
				struct usb_device_handle *uh)
{
	libusb_device **dev_list;
	int i, ndevices;
	int ret = 0;

	ndevices = libusb_get_device_list(NULL, &dev_list);
	if (ndevices < 0)
		return ndevices;

	for (i = 0; i < ndevices; ++i) {
		ret = check_device_match(dev_id, dev_list[i], uh);
		if (ret > 0)
			/* device match and opened */
			break;
	}

	libusb_free_device_list(dev_list, 1);

	return ret > 0 ? 1 : 0;

}

static int hotplug_device_arrived(libusb_context *ctx, libusb_device *device,
				  libusb_hotplug_event event, void *user_data)
{
	struct hotplug_helper *helper = user_data;

	if (check_device_match(helper->dev_id, device, helper->uh) > 0) {
		helper->completed = 1;
		return 1;
	}

	return 0;
}

static int t_usb_find_device(struct usb_device_id *dev_id, int wait,
			     struct usb_device_handle *uh)
{
	struct hotplug_helper helper = {
		.uh = uh,
		.dev_id = dev_id,
		.completed = 0,
	};
	int found;

	found = find_existing_device(dev_id, uh);
	if (found <= 0) {
		if (!wait)
			return found;

		libusb_hotplug_register_callback(NULL,
						 LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED,
						 0,
						 dev_id->vid >= 0 ? dev_id->vid
						 : LIBUSB_HOTPLUG_MATCH_ANY,
						 dev_id->pid >= 0 ? dev_id->pid
						 : LIBUSB_HOTPLUG_MATCH_ANY,
						 LIBUSB_HOTPLUG_MATCH_ANY,
						 hotplug_device_arrived,
						 &helper,
						 NULL);

		while (!helper.completed)
			libusb_handle_events_completed(NULL, &helper.completed);
	}

	return 1;
}

static void t_usb_transfer_finished(struct libusb_transfer *ltransfer)
{
	struct t_usb_transfer *t = ltransfer->user_data;

	t->cancelled = 0;
	t->ret = 0;
	switch (ltransfer->status) {
	case LIBUSB_TRANSFER_COMPLETED:
		if (ltransfer->actual_length != t->size)
			t->ret = -EIO;
		break;
	case LIBUSB_TRANSFER_CANCELLED:
		t->cancelled = 1;
		break;
	default:
		t->ret = -EIO;
	}

	if (t->transfer_finished)
		t->transfer_finished(t);
}

static int t_usb_init_transfer(struct t_usb_transfer *t,
			       libusb_device_handle *devh,
			       unsigned char ep,
			       unsigned char *buf, off_t size,
			       t_usb_transfer_cb transfer_finished,
			       unsigned int timeout)
{
	t->ltransfer = libusb_alloc_transfer(0);
	if (!t->ltransfer)
		return -ENOMEM;

	t->transfer_finished = transfer_finished;
	t->size = size;
	libusb_fill_bulk_transfer(t->ltransfer, devh, ep,
				  buf, size, t_usb_transfer_finished, t,
				  0);

	return 0;
}

static int t_usb_handle_events_completed(int *completed)
{
	struct timeval tv = {0, 0};
	int ret = 0;

	while (!*completed) {
		ret = libusb_handle_events_timeout_completed(NULL,
							     &tv,
							     completed);
		if (ret < 0 && ret != LIBUSB_ERROR_BUSY
		    && ret != LIBUSB_ERROR_TIMEOUT
		    && ret != LIBUSB_ERROR_OVERFLOW
		    && ret != LIBUSB_ERROR_INTERRUPTED)
			break;
		else
			ret = 0;
	}

	return ret;
}

static inline void t_usb_cleanup_transfer(struct t_usb_transfer *t)
{
	libusb_free_transfer(t->ltransfer);
}

static inline int t_usb_init_in_transfer(struct t_usb_transfer *t,
					 struct usb_device_handle *uh,
					 unsigned char *buf, off_t size,
					 t_usb_transfer_cb transfer_finished,
					 unsigned int timeout)
{
	return t_usb_init_transfer(t, uh->devh, uh->data_ep_in, buf, size,
				   transfer_finished, timeout);
}

static inline int t_usb_init_out_transfer(struct t_usb_transfer *t,
					  struct usb_device_handle *uh,
					  unsigned char *buf, off_t size,
					  t_usb_transfer_cb transfer_finished,
					  unsigned int timeout)
{
	return t_usb_init_transfer(t, uh->devh, uh->data_ep_out, buf, size,
				   transfer_finished, timeout);
}

static inline int t_usb_submit_transfer(struct t_usb_transfer *t)
{
	return libusb_submit_transfer(t->ltransfer);
}

static inline int t_usb_cancel_transfer(struct t_usb_transfer *t)
{
	return libusb_cancel_transfer(t->ltransfer);
}

static int t_thor_submit_chunk(struct t_thor_usb_chunk *chunk)
{
	int ret;

	chunk->data_finished = chunk->resp_finished = 0;

	ret = t_usb_submit_transfer(&chunk->data_transfer);
	if (ret)
		goto out;

	memset(&chunk->resp, 0, DATA_RES_PKT_SIZE);
	ret = t_usb_submit_transfer(&chunk->resp_transfer);
	if (ret)
		goto cancel_data_transfer;

	return 0;
cancel_data_transfer:
	t_usb_cancel_transfer(&chunk->data_transfer);
out:
	return ret;
}

static int t_thor_prep_next_chunk(struct t_thor_usb_chunk *chunk,
				  struct t_thor_usb_transfer *transfer_data)
{
	off_t to_read;
	int ret;

	to_read = transfer_data->data_left - transfer_data->data_in_progress;
	if (to_read <= 0) {
		printf("to big data in progress\n");
		fflush(stdout);
		return -EINVAL;
	}

	chunk->useful_size = to_read > chunk->trans_unit_size ?
			     chunk->trans_unit_size : to_read;

	ret = transfer_data->data->get_block(transfer_data->data,
					     chunk->buf, chunk->useful_size);
	if (ret < 0 || ret != chunk->useful_size)
		return ret;

	memset(chunk->buf + chunk->useful_size, 0,
	       chunk->trans_unit_size - chunk->useful_size);
	chunk->chunk_number = transfer_data->chunk_number++;

	ret = t_thor_submit_chunk(chunk);
	if (!ret)
		transfer_data->data_in_progress += chunk->useful_size;

	return ret;
}

static void check_next_chunk(struct t_thor_usb_chunk *chunk,
			     struct t_thor_usb_transfer *transfer_data)
{
	/* If there is some more data to be queued */
	if (transfer_data->data_left - transfer_data->data_in_progress) {
		int ret;

		ret = t_thor_prep_next_chunk(chunk, transfer_data);
		if (ret) {
			transfer_data->ret = ret;
			transfer_data->completed = 1;
		}
	} else {
		/* Last one turns the light off */
		if (transfer_data->data_in_progress == 0)
			transfer_data->completed = 1;
	}
}

static void data_transfer_finished(struct t_usb_transfer *_data_transfer)
{
	struct t_thor_usb_chunk *chunk = container_of(_data_transfer,
						      struct t_thor_usb_chunk,
						      data_transfer);
	struct t_thor_usb_transfer *transfer_data = chunk->user_data;

	chunk->data_finished = 1;

	if (_data_transfer->cancelled || transfer_data->ret)
		return;

	if (_data_transfer->ret) {
		transfer_data->ret = _data_transfer->ret;
		transfer_data->completed = 1;
	}

	if (chunk->resp_finished)
		check_next_chunk(chunk, transfer_data);
}

static void resp_transfer_finished(struct t_usb_transfer *_resp_transfer)
{
	struct t_thor_usb_chunk *chunk = container_of(_resp_transfer,
						      struct t_thor_usb_chunk,
						      resp_transfer);
	struct t_thor_usb_transfer *transfer_data = chunk->user_data;

	chunk->resp_finished = 1;
	transfer_data->data_in_progress -= chunk->useful_size;

	if (_resp_transfer->cancelled || transfer_data->ret) {
		if (transfer_data->data_in_progress == 0)
			transfer_data->completed = 1;
		return;
	}

	if (_resp_transfer->ret) {
		transfer_data->ret = _resp_transfer->ret;
		goto complete_all;
	}

	if (chunk->resp.cnt != chunk->chunk_number) {
		printf("chunk number mismatch: %d != %d\n",
			chunk->resp.cnt, chunk->chunk_number);
		fflush(stdout);
		transfer_data->ret = -EINVAL;
		goto complete_all;
	}

	transfer_data->data_sent += chunk->useful_size;
	transfer_data->data_left -= chunk->useful_size;
	if (transfer_data->report_progress)
		transfer_data->report_progress(transfer_data->th,
					       transfer_data->data,
					       transfer_data->data_sent,
					       transfer_data->data_left,
					       chunk->chunk_number,
					       transfer_data->user_data);

	if (chunk->data_finished)
		check_next_chunk(chunk, transfer_data);

	return;
complete_all:
	transfer_data->completed = 1;
}

static int t_thor_init_chunk(struct t_thor_usb_chunk *chunk,
			     struct usb_device_handle *uh,
			     off_t trans_unit_size,
			     void *user_data)
{
	int ret;

	chunk->user_data = user_data;
	chunk->useful_size = 0;
	chunk->trans_unit_size = trans_unit_size;

	chunk->buf = malloc(trans_unit_size);
	if (!chunk->buf)
		return -ENOMEM;

	ret = t_usb_init_out_transfer(&chunk->data_transfer, uh, chunk->buf,
				      trans_unit_size, data_transfer_finished,
				      DEFAULT_TIMEOUT);
	if (ret)
		goto free_buf;

	ret = t_usb_init_in_transfer(&chunk->resp_transfer, uh,
				     (unsigned char *)&chunk->resp,
				     DATA_RES_PKT_SIZE,
				     resp_transfer_finished,
				     2 * DEFAULT_TIMEOUT);
	if (ret)
		goto cleanup_data_transfer;

	return 0;
cleanup_data_transfer:
	t_usb_cleanup_transfer(&chunk->data_transfer);
free_buf:
	free(chunk->buf);

	return ret;
}

static void t_thor_cleanup_chunk(struct t_thor_usb_chunk *chunk)
{
	t_usb_cleanup_transfer(&chunk->data_transfer);
	t_usb_cleanup_transfer(&chunk->resp_transfer);
	free(chunk->buf);
}

static inline int
t_thor_handle_events(struct t_thor_usb_transfer *transfer_data)
{
	return t_usb_handle_events_completed(&transfer_data->completed);
}

static inline void t_thor_cancel_chunk(struct t_thor_usb_chunk *chunk)
{
	t_usb_cancel_transfer(&chunk->data_transfer);
	t_usb_cancel_transfer(&chunk->resp_transfer);
}

static int thor_usb_send_raw_data_async(thor_device_handle *th,
					struct thor_data_src *data,
					off_t trans_unit_size,
					thor_progress_cb report_progress,
					void *user_data)
{
	struct usb_device_handle *uh = th->dev_priv;
	struct t_thor_usb_chunk chunk[3];
	struct t_thor_usb_transfer transfer_data;
	int i, j;
	int ret;

	if (!uh)
		return -ENODEV;

	for (i = 0; i < ARRAY_SIZE(chunk); ++i) {
		ret = t_thor_init_chunk(chunk + i, uh, trans_unit_size,
					&transfer_data);
		if (ret)
			goto cleanup_chunks;
	}

	transfer_data.th = th;
	transfer_data.data = data;
	transfer_data.report_progress = report_progress;
	transfer_data.user_data = user_data;
	transfer_data.data_left = data->get_file_length(data);
	transfer_data.data_sent = 0;
	transfer_data.chunk_number = 1;
	transfer_data.completed = 0;
	transfer_data.data_in_progress = 0;
	transfer_data.ret = 0;

	for (i = 0;
	     i < ARRAY_SIZE(chunk)
	      && (transfer_data.data_left - transfer_data.data_in_progress > 0);
	     ++i) {
		ret = t_thor_prep_next_chunk(chunk + i, &transfer_data);
		if (ret)
			goto cancel_chunks;
	}

	t_thor_handle_events(&transfer_data);

	if (transfer_data.data_in_progress) {
		ret = transfer_data.ret;
		goto cancel_chunks;
	}

	for (i = 0; i < ARRAY_SIZE(chunk); ++i)
		t_thor_cleanup_chunk(chunk + i);

	return transfer_data.ret;

cancel_chunks:
	for (j = 0; j < i; ++j)
		t_thor_cancel_chunk(chunk + j);
	if (i) {
		transfer_data.completed = 0;
		t_thor_handle_events(&transfer_data);
	}

	i = ARRAY_SIZE(chunk);
cleanup_chunks:
	for (j = 0; j < i; ++j)
		t_thor_cleanup_chunk(chunk + j);

	return ret;
}

static int thor_usb_open(thor_device_handle *th,
			 struct thor_device_id *user_dev_id,
			 int wait)
{
	struct usb_device_handle *uh;
	struct usb_device_id *dev_id = thor_choose_id(user_dev_id);
	int found, ret;

	uh = calloc(sizeof(*uh), 1);
	if (!uh)
		return -ENOMEM;

	found = t_usb_find_device(dev_id, wait, uh);
	if (found <= 0) {
		ret = -ENODEV;
		goto close_dev;
	}

	ret = t_acm_prepare_device(uh);
	if (ret)
		goto close_dev;

	th->dev_priv = uh;

	return 0;

close_dev:
	th->ops->close(th);
	return ret;
}

static void thor_usb_close(thor_device_handle *th)
{
	struct usb_device_handle *uh = th->dev_priv;

	if (!uh)
		return;

	if (uh->devh)
		libusb_close(uh->devh);

	free(uh);
	th->dev_priv = NULL;
}

static int thor_usb_send(thor_device_handle *th, unsigned char *buf,
			 off_t count, int timeout)
{
	struct usb_device_handle *uh = th->dev_priv;
	int ret;
	int transferred = 0;

	if (!uh)
		return -ENODEV;

	ret = libusb_bulk_transfer(uh->devh,
				   uh->data_ep_out,
				   (unsigned char *)buf,
				   count,
				   &transferred,
				   timeout);

	if (ret < 0)
		return ret;
	if (transferred < (int)count)
		return -EIO;

	return 0;
}

static int thor_usb_recv(thor_device_handle *th, unsigned char *buf,
			 off_t count, int timeout)
{
	struct usb_device_handle *uh = th->dev_priv;
	int ret;
	int transferred = 0;

	if (!uh)
		return -ENODEV;

	ret = libusb_bulk_transfer(uh->devh,
				   uh->data_ep_in,
				   (unsigned char *)buf,
				   count,
				   &transferred,
				   timeout);

	if (ret < 0)
		return ret;
	if (transferred < (int)count)
		return -EIO;

	return 0;
}

static struct thor_transport_ops thor_usb_ops = {
	.open = thor_usb_open,
	.close = thor_usb_close,
	.send = thor_usb_send,
	.recv = thor_usb_recv,
	.send_data = thor_usb_send_raw_data_async,
};

int thor_usb_init(thor_device_handle *th)
{
	int ret;

	ret = libusb_init(NULL);
	if (ret < 0)
		return -EINVAL;

	th->ops = &thor_usb_ops;

	return 0;
}

void thor_usb_cleanup(thor_device_handle *th)
{
	libusb_exit(NULL);
}
