/*
 * libdivecomputer
 *
 * Copyright (C) 2016 Jef Driesen
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301 USA
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <stdlib.h>
#include <string.h>
#ifdef HAVE_PTHREAD_H
#include <pthread.h>
#endif
#ifdef _WIN32
#define NOGDI
#include <windows.h>
#endif

#if defined(HAVE_HIDAPI)
#define USE_HIDAPI
#define USBHID
#elif defined(HAVE_LIBUSB) && !defined(__APPLE__)
#define USE_LIBUSB
#define USBHID
#endif

#if defined(USE_LIBUSB)
#ifdef _WIN32
#define NOGDI
#endif
#include <libusb.h>
#elif defined(USE_HIDAPI)
#include <hidapi.h>
#endif

#include <libdivecomputer/usbhid.h>

#include "common-private.h"
#include "context-private.h"
#include "iostream-private.h"
#include "descriptor-private.h"
#include "iterator-private.h"
#include "platform.h"

#ifdef _WIN32
typedef LONG dc_mutex_t;
#define DC_MUTEX_INIT 0
#else
typedef pthread_mutex_t dc_mutex_t;
#define DC_MUTEX_INIT PTHREAD_MUTEX_INITIALIZER
#endif

#define ISINSTANCE(device) dc_iostream_isinstance((device), &dc_usbhid_vtable)

typedef struct dc_usbhid_session_t {
	size_t refcount;
#if defined(USE_LIBUSB)
	libusb_context *handle;
#endif
} dc_usbhid_session_t;

struct dc_usbhid_device_t {
	unsigned short vid, pid;
	dc_usbhid_session_t *session;
#if defined(USE_LIBUSB)
	struct libusb_device *handle;
	int interface;
	unsigned char endpoint_in;
	unsigned char endpoint_out;
#elif defined(USE_HIDAPI)
	char *path;
#endif
};

#ifdef USBHID
static dc_status_t dc_usbhid_iterator_next (dc_iterator_t *iterator, void *item);
static dc_status_t dc_usbhid_iterator_free (dc_iterator_t *iterator);

static dc_status_t dc_usbhid_set_timeout (dc_iostream_t *iostream, int timeout);
static dc_status_t dc_usbhid_read (dc_iostream_t *iostream, void *data, size_t size, size_t *actual);
static dc_status_t dc_usbhid_write (dc_iostream_t *iostream, const void *data, size_t size, size_t *actual);
static dc_status_t dc_usbhid_close (dc_iostream_t *iostream);

typedef struct dc_usbhid_iterator_t {
	dc_iterator_t base;
	dc_filter_t filter;
	dc_usbhid_session_t *session;
#if defined(USE_LIBUSB)
	struct libusb_device **devices;
	size_t count;
	size_t current;
#elif defined(USE_HIDAPI)
	struct hid_device_info *devices, *current;
#endif
} dc_usbhid_iterator_t;

typedef struct dc_usbhid_t {
	/* Base class. */
	dc_iostream_t base;
	/* Internal state. */
	dc_usbhid_session_t *session;
#if defined(USE_LIBUSB)
	libusb_device_handle *handle;
	int interface;
	unsigned char endpoint_in;
	unsigned char endpoint_out;
	unsigned int timeout;
#elif defined(USE_HIDAPI)
	hid_device *handle;
	int timeout;
#endif
} dc_usbhid_t;

static const dc_iterator_vtable_t dc_usbhid_iterator_vtable = {
	sizeof(dc_usbhid_iterator_t),
	dc_usbhid_iterator_next,
	dc_usbhid_iterator_free,
};

static const dc_iostream_vtable_t dc_usbhid_vtable = {
	sizeof(dc_usbhid_t),
	dc_usbhid_set_timeout, /* set_timeout */
	NULL, /* set_latency */
	NULL, /* set_break */
	NULL, /* set_dtr */
	NULL, /* set_rts */
	NULL, /* get_lines */
	NULL, /* get_available */
	NULL, /* configure */
	dc_usbhid_read, /* read */
	dc_usbhid_write, /* write */
	NULL, /* flush */
	NULL, /* purge */
	NULL, /* sleep */
	dc_usbhid_close, /* close */
};

#ifdef USE_HIDAPI
static dc_mutex_t g_usbhid_mutex = DC_MUTEX_INIT;
static dc_usbhid_session_t *g_usbhid_session = NULL;
#endif

#if defined(USE_LIBUSB)
static dc_status_t
syserror(int errcode)
{
	switch (errcode) {
	case LIBUSB_ERROR_INVALID_PARAM:
		return DC_STATUS_INVALIDARGS;
	case LIBUSB_ERROR_NO_MEM:
		return DC_STATUS_NOMEMORY;
	case LIBUSB_ERROR_NO_DEVICE:
	case LIBUSB_ERROR_NOT_FOUND:
		return DC_STATUS_NODEVICE;
	case LIBUSB_ERROR_ACCESS:
	case LIBUSB_ERROR_BUSY:
		return DC_STATUS_NOACCESS;
	case LIBUSB_ERROR_TIMEOUT:
		return DC_STATUS_TIMEOUT;
	default:
		return DC_STATUS_IO;
	}
}
#endif

#ifdef USE_HIDAPI
static void
dc_mutex_lock (dc_mutex_t *mutex)
{
#ifdef _WIN32
	while (InterlockedCompareExchange (mutex, 1, 0) == 1) {
		SleepEx (0, TRUE);
	}
#else
	pthread_mutex_lock (mutex);
#endif
}

static void
dc_mutex_unlock (dc_mutex_t *mutex)
{
#ifdef _WIN32
	InterlockedExchange (mutex, 0);
#else
	pthread_mutex_unlock (mutex);
#endif
}
#endif

static dc_status_t
dc_usbhid_session_new (dc_usbhid_session_t **out, dc_context_t *context)
{
	dc_status_t status = DC_STATUS_SUCCESS;
	dc_usbhid_session_t *session = NULL;

	if (out == NULL)
		return DC_STATUS_INVALIDARGS;

#ifdef USE_HIDAPI
	dc_mutex_lock (&g_usbhid_mutex);

	if (g_usbhid_session) {
		g_usbhid_session->refcount++;
		*out = g_usbhid_session;
		dc_mutex_unlock (&g_usbhid_mutex);
		return DC_STATUS_SUCCESS;
	}
#endif

	session = (dc_usbhid_session_t *) malloc (sizeof(dc_usbhid_session_t));
	if (session == NULL) {
		ERROR (context, "Failed to allocate memory.");
		status = DC_STATUS_NOMEMORY;
		goto error_unlock;
	}

	session->refcount = 1;

#if defined(USE_LIBUSB)
	int rc = libusb_init (&session->handle);
	if (rc != LIBUSB_SUCCESS) {
		ERROR (context, "Failed to initialize usb support (%s).",
			libusb_error_name (rc));
		status = syserror (rc);
		goto error_free;
	}
#elif defined(USE_HIDAPI)
	int rc = hid_init();
	if (rc < 0) {
		ERROR (context, "Failed to initialize usb support.");
		status = DC_STATUS_IO;
		goto error_free;
	}

	g_usbhid_session = session;

	dc_mutex_unlock (&g_usbhid_mutex);
#endif

	*out = session;

	return status;

error_free:
	free (session);
error_unlock:
#ifdef USE_HIDAPI
	dc_mutex_unlock (&g_usbhid_mutex);
#endif
	return status;
}

static dc_usbhid_session_t *
dc_usbhid_session_ref (dc_usbhid_session_t *session)
{
	if (session == NULL)
		return NULL;

#ifdef USE_HIDAPI
	dc_mutex_lock (&g_usbhid_mutex);
#endif

	session->refcount++;

#ifdef USE_HIDAPI
	dc_mutex_unlock (&g_usbhid_mutex);
#endif

	return session;
}

static dc_status_t
dc_usbhid_session_unref (dc_usbhid_session_t *session)
{
	if (session == NULL)
		return DC_STATUS_SUCCESS;

#ifdef USE_HIDAPI
	dc_mutex_lock (&g_usbhid_mutex);
#endif

	if (--session->refcount == 0) {
#if defined(USE_LIBUSB)
		libusb_exit (session->handle);
#elif defined(USE_HIDAPI)
		hid_exit ();
		g_usbhid_session = NULL;
#endif
		free (session);
	}

#ifdef USE_HIDAPI
	dc_mutex_unlock (&g_usbhid_mutex);
#endif

	return DC_STATUS_SUCCESS;
}
#endif

unsigned int
dc_usbhid_device_get_vid (dc_usbhid_device_t *device)
{
	if (device == NULL)
		return 0;

	return device->vid;
}

unsigned int
dc_usbhid_device_get_pid (dc_usbhid_device_t *device)
{
	if (device == NULL)
		return 0;

	return device->pid;
}

void
dc_usbhid_device_free(dc_usbhid_device_t *device)
{
	if (device == NULL)
		return;

#ifdef USBHID
#if defined(USE_LIBUSB)
	libusb_unref_device (device->handle);
#elif defined(USE_HIDAPI)
	free (device->path);
#endif
	dc_usbhid_session_unref (device->session);
#endif
	free (device);
}

dc_status_t
dc_usbhid_iterator_new (dc_iterator_t **out, dc_context_t *context, dc_descriptor_t *descriptor)
{
#ifdef USBHID
	dc_status_t status = DC_STATUS_SUCCESS;
	dc_usbhid_iterator_t *iterator = NULL;

	if (out == NULL)
		return DC_STATUS_INVALIDARGS;

	iterator = (dc_usbhid_iterator_t *) dc_iterator_allocate (context, &dc_usbhid_iterator_vtable);
	if (iterator == NULL) {
		ERROR (context, "Failed to allocate memory.");
		return DC_STATUS_NOMEMORY;
	}

	// Initialize the usb library.
	status = dc_usbhid_session_new (&iterator->session, context);
	if (status != DC_STATUS_SUCCESS) {
		goto error_free;
	}

#if defined(USE_LIBUSB)
	// Enumerate the USB devices.
	struct libusb_device **devices = NULL;
	ssize_t ndevices = libusb_get_device_list (iterator->session->handle, &devices);
	if (ndevices < 0) {
		ERROR (context, "Failed to enumerate the usb devices (%s).",
			libusb_error_name (ndevices));
		status = syserror (ndevices);
		goto error_session_unref;
	}

	iterator->devices = devices;
	iterator->count = ndevices;
	iterator->current = 0;
#elif defined(USE_HIDAPI)
	struct hid_device_info *devices = hid_enumerate(0x0, 0x0);
	if (devices == NULL) {
		status = DC_STATUS_IO;
		goto error_session_unref;
	}

	iterator->devices = devices;
	iterator->current = devices;
#endif
	iterator->filter = dc_descriptor_get_filter (descriptor);

	*out = (dc_iterator_t *) iterator;

	return DC_STATUS_SUCCESS;

error_session_unref:
	dc_usbhid_session_unref (iterator->session);
error_free:
	dc_iterator_deallocate ((dc_iterator_t *) iterator);
	return status;
#else
	return DC_STATUS_UNSUPPORTED;
#endif
}

#ifdef USBHID
static dc_status_t
dc_usbhid_iterator_next (dc_iterator_t *abstract, void *out)
{
	dc_usbhid_iterator_t *iterator = (dc_usbhid_iterator_t *) abstract;
	dc_usbhid_device_t *device = NULL;

#if defined(USE_LIBUSB)
	while (iterator->current < iterator->count) {
		struct libusb_device *current = iterator->devices[iterator->current++];

		// Get the device descriptor.
		struct libusb_device_descriptor dev;
		int rc = libusb_get_device_descriptor (current, &dev);
		if (rc < 0) {
			ERROR (abstract->context, "Failed to get the device descriptor (%s).",
				libusb_error_name (rc));
			return syserror (rc);
		}

		dc_usb_desc_t usb = {dev.idVendor, dev.idProduct};
		if (iterator->filter && !iterator->filter (DC_TRANSPORT_USBHID, &usb)) {
			continue;
		}

		// Get the active configuration descriptor.
		struct libusb_config_descriptor *config = NULL;
		rc = libusb_get_active_config_descriptor (current, &config);
		if (rc != LIBUSB_SUCCESS) {
			ERROR (abstract->context, "Failed to get the configuration descriptor (%s).",
				libusb_error_name (rc));
			return syserror (rc);
		}

		// Find the first HID interface.
		const struct libusb_interface_descriptor *interface = NULL;
		for (unsigned int i = 0; i < config->bNumInterfaces; i++) {
			const struct libusb_interface *iface = &config->interface[i];
			for (int j = 0; j < iface->num_altsetting; j++) {
				const struct libusb_interface_descriptor *desc = &iface->altsetting[j];
				if (desc->bInterfaceClass == LIBUSB_CLASS_HID && interface == NULL) {
					interface = desc;
				}
			}
		}

		if (interface == NULL) {
			libusb_free_config_descriptor (config);
			continue;
		}

		// Find the first input and output interrupt endpoints.
		const struct libusb_endpoint_descriptor *ep_in = NULL, *ep_out = NULL;
		for (unsigned int i = 0; i < interface->bNumEndpoints; i++) {
			const struct libusb_endpoint_descriptor *desc = &interface->endpoint[i];

			unsigned int type = desc->bmAttributes & LIBUSB_TRANSFER_TYPE_MASK;
			unsigned int direction = desc->bEndpointAddress & LIBUSB_ENDPOINT_DIR_MASK;

			if (type != LIBUSB_TRANSFER_TYPE_INTERRUPT) {
				continue;
			}

			if (direction == LIBUSB_ENDPOINT_IN && ep_in == NULL) {
				ep_in = desc;
			}

			if (direction == LIBUSB_ENDPOINT_OUT && ep_out == NULL) {
				ep_out = desc;
			}
		}

		if (ep_in == NULL || ep_out == NULL) {
			libusb_free_config_descriptor (config);
			continue;
		}

		device = (dc_usbhid_device_t *) malloc (sizeof(dc_usbhid_device_t));
		if (device == NULL) {
			ERROR (abstract->context, "Failed to allocate memory.");
			libusb_free_config_descriptor (config);
			return DC_STATUS_NOMEMORY;
		}

		device->session = dc_usbhid_session_ref (iterator->session);
		device->vid = dev.idVendor;
		device->pid = dev.idProduct;
		device->handle = libusb_ref_device (current);
		device->interface = interface->bInterfaceNumber;
		device->endpoint_in = ep_in->bEndpointAddress;
		device->endpoint_out = ep_out->bEndpointAddress;

		*(dc_usbhid_device_t **) out = device;

		libusb_free_config_descriptor (config);

		return DC_STATUS_SUCCESS;
	}
#elif defined(USE_HIDAPI)
	while (iterator->current) {
		struct hid_device_info *current = iterator->current;
		iterator->current = current->next;

		dc_usb_desc_t usb = {current->vendor_id, current->product_id};
		if (iterator->filter && !iterator->filter (DC_TRANSPORT_USBHID, &usb)) {
			continue;
		}

		device = (dc_usbhid_device_t *) malloc (sizeof(dc_usbhid_device_t));
		if (device == NULL) {
			ERROR (abstract->context, "Failed to allocate memory.");
			return DC_STATUS_NOMEMORY;
		}

		device->session = dc_usbhid_session_ref (iterator->session);
		device->vid = current->vendor_id;
		device->pid = current->product_id;
		device->path = strdup (current->path);

		*(dc_usbhid_device_t **) out = device;

		return DC_STATUS_SUCCESS;
	}
#endif

	return DC_STATUS_DONE;
}

static dc_status_t
dc_usbhid_iterator_free (dc_iterator_t *abstract)
{
	dc_usbhid_iterator_t *iterator = (dc_usbhid_iterator_t *) abstract;

#if defined(USE_LIBUSB)
	libusb_free_device_list (iterator->devices, 1);
#elif defined(USE_HIDAPI)
	hid_free_enumeration (iterator->devices);
#endif
	dc_usbhid_session_unref (iterator->session);

	return DC_STATUS_SUCCESS;
}
#endif

dc_status_t
dc_usbhid_open (dc_iostream_t **out, dc_context_t *context, dc_usbhid_device_t *device)
{
#ifdef USBHID
	dc_status_t status = DC_STATUS_SUCCESS;
	dc_usbhid_t *usbhid = NULL;

	if (out == NULL || device == NULL)
		return DC_STATUS_INVALIDARGS;

	INFO (context, "Open: vid=%04x, pid=%04x", device->vid, device->pid);

	// Allocate memory.
	usbhid = (dc_usbhid_t *) dc_iostream_allocate (context, &dc_usbhid_vtable, DC_TRANSPORT_USBHID);
	if (usbhid == NULL) {
		ERROR (context, "Out of memory.");
		return DC_STATUS_NOMEMORY;
	}

	// Initialize the usb library.
	usbhid->session = dc_usbhid_session_ref (device->session);
	if (usbhid->session == NULL) {
		goto error_free;
	}

#if defined(USE_LIBUSB)
	INFO (context, "Open: interface=%u, endpoints=%02x,%02x",
		device->interface, device->endpoint_in, device->endpoint_out);

	// Open the USB device.
	int rc = libusb_open (device->handle, &usbhid->handle);
	if (rc != LIBUSB_SUCCESS) {
		ERROR (context, "Failed to open the usb device (%s).",
			libusb_error_name (rc));
		status = syserror (rc);
		goto error_session_unref;
	}

#if defined(LIBUSB_API_VERSION) && (LIBUSB_API_VERSION >= 0x01000102)
	libusb_set_auto_detach_kernel_driver (usbhid->handle, 1);
#endif

	// Claim the HID interface.
	rc = libusb_claim_interface (usbhid->handle, device->interface);
	if (rc != LIBUSB_SUCCESS) {
		ERROR (context, "Failed to claim the usb interface (%s).",
			libusb_error_name (rc));
		status = syserror (rc);
		goto error_usb_close;
	}

	usbhid->interface = device->interface;
	usbhid->endpoint_in = device->endpoint_in;
	usbhid->endpoint_out = device->endpoint_out;
	usbhid->timeout = 0;

#elif defined(USE_HIDAPI)
	INFO (context, "Open: path=%s", device->path);

	// Open the USB device.
	usbhid->handle = hid_open_path (device->path);
	if (usbhid->handle == NULL) {
		ERROR (context, "Failed to open the usb device.");
		status = DC_STATUS_IO;
		goto error_session_unref;
	}

	usbhid->timeout = -1;
#endif

	*out = (dc_iostream_t *) usbhid;

	return DC_STATUS_SUCCESS;

#if defined(USE_LIBUSB)
error_usb_close:
	libusb_close (usbhid->handle);
#endif
error_session_unref:
	dc_usbhid_session_unref (usbhid->session);
error_free:
	dc_iostream_deallocate ((dc_iostream_t *) usbhid);
	return status;
#else
	return DC_STATUS_UNSUPPORTED;
#endif
}

#ifdef USBHID
static dc_status_t
dc_usbhid_close (dc_iostream_t *abstract)
{
	dc_status_t status = DC_STATUS_SUCCESS;
	dc_usbhid_t *usbhid = (dc_usbhid_t *) abstract;

#if defined(USE_LIBUSB)
	libusb_release_interface (usbhid->handle, usbhid->interface);
	libusb_close (usbhid->handle);
#elif defined(USE_HIDAPI)
	hid_close(usbhid->handle);
#endif
	dc_usbhid_session_unref (usbhid->session);

	return status;
}

static dc_status_t
dc_usbhid_set_timeout (dc_iostream_t *abstract, int timeout)
{
	dc_usbhid_t *usbhid = (dc_usbhid_t *) abstract;

#if defined(USE_LIBUSB)
	if (timeout < 0) {
		usbhid->timeout = 0;
	} else if (timeout == 0) {
		return DC_STATUS_UNSUPPORTED;
	} else {
		usbhid->timeout = timeout;
	}
#elif defined(USE_HIDAPI)
	if (timeout < 0) {
		usbhid->timeout = -1;
	} else {
		usbhid->timeout = timeout;
	}
#endif

	return DC_STATUS_SUCCESS;
}

static dc_status_t
dc_usbhid_read (dc_iostream_t *abstract, void *data, size_t size, size_t *actual)
{
	dc_status_t status = DC_STATUS_SUCCESS;
	dc_usbhid_t *usbhid = (dc_usbhid_t *) abstract;
	int nbytes = 0;

#if defined(USE_LIBUSB)
	int rc = libusb_interrupt_transfer (usbhid->handle, usbhid->endpoint_in, data, size, &nbytes, usbhid->timeout);
	if (rc != LIBUSB_SUCCESS) {
		ERROR (abstract->context, "Usb read interrupt transfer failed (%s).",
			libusb_error_name (rc));
		status = syserror (rc);
		goto out;
	}
#elif defined(USE_HIDAPI)
	nbytes = hid_read_timeout(usbhid->handle, data, size, usbhid->timeout);
	if (nbytes < 0) {
		ERROR (abstract->context, "Usb read interrupt transfer failed.");
		status = DC_STATUS_IO;
		nbytes = 0;
		goto out;
	}
#endif

out:
	if (actual)
		*actual = nbytes;

	return status;
}

static dc_status_t
dc_usbhid_write (dc_iostream_t *abstract, const void *data, size_t size, size_t *actual)
{
	dc_status_t status = DC_STATUS_SUCCESS;
	dc_usbhid_t *usbhid = (dc_usbhid_t *) abstract;
	int nbytes = 0;

	if (size == 0) {
		goto out;
	}

#if defined(USE_LIBUSB)
	const unsigned char *buffer = (const unsigned char *) data;
	size_t length = size;

	// Skip a report id of zero.
	unsigned char report = buffer[0];
	if (report == 0) {
		buffer++;
		length--;
	}

	int rc = libusb_interrupt_transfer (usbhid->handle, usbhid->endpoint_out, (void *) buffer, length, &nbytes, 0);
	if (rc != LIBUSB_SUCCESS) {
		ERROR (abstract->context, "Usb write interrupt transfer failed (%s).",
			libusb_error_name (rc));
		status = syserror (rc);
		goto out;
	}

	if (report == 0) {
		nbytes++;
	}
#elif defined(USE_HIDAPI)
	nbytes = hid_write(usbhid->handle, data, size);
	if (nbytes < 0) {
		ERROR (abstract->context, "Usb write interrupt transfer failed.");
		status = DC_STATUS_IO;
		nbytes = 0;
		goto out;
	}
#endif

out:
#ifdef _WIN32
	if (nbytes > size) {
		WARNING (abstract->context, "Number of bytes exceeds the buffer size (" DC_PRINTF_SIZE " > " DC_PRINTF_SIZE ")!", nbytes, size);
		nbytes = size;
	}
#endif

	if (actual)
		*actual = nbytes;

	return status;
}
#endif
