/*
 * $Id$
 *
 * libusb.c: device i/o for libusb
 *
 * Copyright (c) 2004 Zephaniah E. Hull & Florent Pillet.
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Library General Public License as published by
 * the Free Software Foundation; either version 2 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 Library
 * General Public License for more details.
 *
 * You should have received a copy of the GNU Library 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 <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>

#include "pi-debug.h"
#include "pi-source.h"
#include "pi-usb.h"
#include "pi-util.h"

#ifdef HAVE_SYS_IOCTL_COMPAT_H
#include <sys/ioctl_compat.h>
#endif

#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif

#include <usb.h>

#if defined(sun) && defined(__SVR4)
#define __FUNCTION__ __func__
#endif

static int u_open(struct pi_socket *ps, struct pi_sockaddr *addr, size_t addrlen);
static int u_close(struct pi_socket *ps);
static ssize_t u_write(struct pi_socket *ps, const unsigned char *buf, size_t len, 
	int flags);
static ssize_t u_read(struct pi_socket *ps, pi_buffer_t *buf, size_t len, 
	int flags);
static int u_read_i(struct pi_socket *ps, pi_buffer_t *buf, size_t len, int flags, 
	int timeout);
static int u_poll(struct pi_socket *ps, int timeout);
static int u_wait_for_device(struct pi_socket *ps, int *timeout);
static int u_flush(pi_socket_t *ps, int flags);
static int u_control_request (pi_usb_data_t *usb_data, int request_type, 
	int request, int value, int control_index, void *data, int size, int timeout);
static int u_interrupt_read (pi_usb_data_t *usb_data, int ep, void *data, int size,
	int timeout);

void pi_usb_impl_init (struct pi_usb_impl *impl)
{
	impl->open 		= u_open;
	impl->close		= u_close;
	impl->write		= u_write;
	impl->read 		= u_read;
	impl->flush		= u_flush;
	impl->poll 		= u_poll;
	impl->wait_for_device	= u_wait_for_device;
	impl->changebaud	= NULL;		/* we don't need this one for libusb (yet) */
	impl->control_request	= u_control_request;
	impl->interrupt_read	= u_interrupt_read;
}


/***********************************************************************
 *
 * Start of the device identification code.
 *
 ***********************************************************************/

static usb_dev_handle	*USB_handle;
static int				USB_interface;
static int				USB_in_endpoint;
static int				USB_out_endpoint;

static int
USB_open (pi_usb_data_t *data)
{
	usb_init ();

	return 1;
}

static int
USB_poll (pi_usb_data_t *data)
{
	struct usb_bus *bus;
	struct usb_device *dev;
	int ret;
	u_int8_t input_endpoint = 0xFF, output_endpoint = 0xFF;
#ifdef LIBUSB_HAS_DETACH_KERNEL_DRIVER_NP
	int first;
#endif

	usb_find_busses ();
	usb_find_devices ();
	CHECK (PI_DBG_DEV, PI_DBG_LVL_DEBUG, usb_set_debug (2));

	for (bus = usb_busses; bus; bus = bus->next) {
		for (dev = bus->devices; dev; dev = dev->next) {
			int i;
			LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s: checking device %p\n", 
				__FILE__, dev));

			if (dev->descriptor.bNumConfigurations < 1)
				continue;
			if (!dev->config)
				continue;
			if (dev->config[0].bNumInterfaces < 1)
				continue;
			if (dev->config[0].interface[0].num_altsetting < 1)
				continue;
			if (dev->config[0].interface[0].altsetting[0].bNumEndpoints < 2)
				continue;

			LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s: %d, 0x%04x 0x%04x.\n", 
				__FILE__, __LINE__, dev->descriptor.idVendor, dev->descriptor.idProduct));

			if (USB_check_device (data, dev->descriptor.idVendor, dev->descriptor.idProduct))
				continue;

			LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s: trying to open device %p\n",
				__FILE__, dev));

			{
				char devpath[64];

				snprintf(devpath, sizeof(devpath), "/dev/bus/usb/%s/%s",
					bus->dirname, dev->filename);
				if (access(devpath, R_OK | W_OK) != 0) {
					LOG((PI_DBG_DEV, PI_DBG_LVL_ERR,
						"%s: unable to access %s for device 0x%04x:0x%04x (errno=%d)\n",
						__FILE__, devpath, dev->descriptor.idVendor,
						dev->descriptor.idProduct, errno));
					data->ref = NULL;
					continue;
				}
			}

			USB_handle = usb_open(dev);

			LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s: USB_handle=%p\n", 
				__FILE__, USB_handle));

			if (!USB_handle) {
				LOG((PI_DBG_DEV, PI_DBG_LVL_ERR,
					"%s: unable to open device 0x%04x:0x%04x (errno=%d)\n",
					__FILE__, dev->descriptor.idVendor,
					dev->descriptor.idProduct, errno));
				data->ref = NULL;
				continue;
			}

			data->ref = USB_handle;

			input_endpoint = output_endpoint = 0xFF;
			USB_in_endpoint = USB_out_endpoint = 0xFF;

			ret = USB_configure_device (data, &input_endpoint, &output_endpoint);
			if (ret < 0) {
				LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, 
					"%s: USB configure failed for familar device: 0x%04x 0x%04x. (LifeDrive issue?)\n", 
					__FILE__, dev->descriptor.idVendor, dev->descriptor.idProduct));

				if (USB_handle)
					usb_close(USB_handle);
				USB_handle = NULL;
				data->ref = NULL;
				continue;
			}

			for (i = 0; i < dev->config[0].interface[0].altsetting[0].bNumEndpoints; i++) {
				struct usb_endpoint_descriptor *endpoint;
				u_int8_t address;

				endpoint = &dev->config[0].interface[0].altsetting[0].endpoint[i];

				if (endpoint->wMaxPacketSize != 0x40)
					continue;
				if ((endpoint->bmAttributes & USB_ENDPOINT_TYPE_MASK) != USB_ENDPOINT_TYPE_BULK)
					continue;
				address = endpoint->bEndpointAddress;
				if ((address & USB_ENDPOINT_DIR_MASK)) {
					LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "In: 0x%x 0x%x.\n", address, input_endpoint));
					if (input_endpoint == 0xFF)
						USB_in_endpoint = address;
					else if ((address & USB_ENDPOINT_ADDRESS_MASK) == input_endpoint)
						USB_in_endpoint = address;
				} else {
					LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "Out: 0x%x 0x%x.\n", address, output_endpoint));
					if (output_endpoint == 0xFF)
						USB_out_endpoint = address;
					else if ((address & USB_ENDPOINT_ADDRESS_MASK) == output_endpoint)
						USB_out_endpoint = address;
				}
			}

			if (USB_in_endpoint == 0xFF || USB_out_endpoint == 0xFF) {
				usb_close (USB_handle);
				USB_handle = NULL;
				data->ref = NULL;
				continue;
			}

			LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, 
				"Config: %d, 0x%x 0x%x | 0x%x 0x%x.\n", 
				ret, input_endpoint, output_endpoint, USB_in_endpoint, USB_out_endpoint));

			USB_interface = dev->config[0].interface[0].altsetting[0].bInterfaceNumber;
#ifdef LIBUSB_HAS_DETACH_KERNEL_DRIVER_NP
			first = 1;
claim:
#endif
			i = usb_claim_interface (USB_handle, USB_interface);
			if (i < 0) {
				if (i == -EBUSY) {
					LOG((PI_DBG_DEV, PI_DBG_LVL_ERR, "Unable to claim device: Busy.\n"));
#ifdef LIBUSB_HAS_DETACH_KERNEL_DRIVER_NP
					if (first) {
						usb_detach_kernel_driver_np (USB_handle, USB_interface);
						first = 0;
						goto claim;
					}
#endif
				} else if (i == -ENOMEM)
					LOG((PI_DBG_DEV, PI_DBG_LVL_ERR, "Unable to claim device: No memory.\n"));
				else
					LOG((PI_DBG_DEV, PI_DBG_LVL_ERR, "Unable to claim device: %d.\n", i));
				usb_close (USB_handle);

				errno = -i;
				LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s: %d.\n", 
					__FILE__, __LINE__));

				return 0;
			}

			LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s: %d.\n", 
				__FILE__, __LINE__));
			return 1;
		}
	}

	errno = ENODEV;
	CHECK (PI_DBG_DEV, PI_DBG_LVL_DEBUG, usb_set_debug (0));
	return 0;
}

static int
USB_close (void)
{
	if (!USB_handle)
		return 0;

	usb_release_interface (USB_handle, USB_interface);
	usb_close (USB_handle);
	USB_handle = NULL;
	return 1;
}


/***********************************************************************
 *
 * Start of the read thread code, please note that all of this runs
 * in a separate thread.
 *
 ***********************************************************************/

#define MAX_READ_SIZE	16384
#define AUTO_READ_SIZE	64
static char		*RD_buffer = NULL;
static size_t		RD_buffer_size;
static size_t		RD_buffer_used;
static pthread_mutex_t	RD_buffer_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t	RD_buffer_available_cond = PTHREAD_COND_INITIALIZER;
static int		RD_wanted;
static int		RD_running = 0;
static char		RD_usb_buffer[MAX_READ_SIZE];
static pthread_t	RD_thread = 0;

static void
RD_do_read (int timeout)
{
	int	bytes_read, read_size;

	read_size = RD_wanted - RD_buffer_used;
	if (read_size < AUTO_READ_SIZE)
		read_size = AUTO_READ_SIZE;
	else if (read_size > MAX_READ_SIZE)
		read_size = MAX_READ_SIZE;

	LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "Reading: len: %d, timeout: %d.\n", read_size, timeout));
	bytes_read = usb_bulk_read (USB_handle, USB_in_endpoint, RD_usb_buffer, read_size, timeout);
	LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s %d (%s): %d\n", 
		__FILE__, __LINE__, __FUNCTION__, bytes_read));
	if (bytes_read < 0) {
		if (bytes_read == -ENODEV) {
			LOG((PI_DBG_DEV, PI_DBG_LVL_NONE, "Device went byebye!\n"));
			RD_running = 0;
			return;
#ifdef ELAST
		} else if (bytes_read == -(ELAST + 1)) {
			usb_clear_halt (USB_handle, USB_in_endpoint);
			return;
#endif
		} else if (bytes_read == -ETIMEDOUT)
			return;

		LOG((PI_DBG_DEV, PI_DBG_LVL_ERR, "libusb: USB bulk read returned error code %d\n", bytes_read));
		return;
	}
	if (!bytes_read)
		return;

	
	pthread_mutex_lock (&RD_buffer_mutex);
	if ((RD_buffer_used + bytes_read) > RD_buffer_size) {
		RD_buffer_size = ((RD_buffer_used + bytes_read + 0xfffe) & ~0xffff) - 1;	/* 64k chunks. */
		RD_buffer = realloc (RD_buffer, RD_buffer_size);
	}

	memcpy (RD_buffer + RD_buffer_used, RD_usb_buffer, bytes_read);
	RD_buffer_used += bytes_read;
	pthread_cond_broadcast (&RD_buffer_available_cond);
	pthread_mutex_unlock (&RD_buffer_mutex);
}

static void *
RD_main (void *foo)
{
	RD_buffer_used = 0;
	RD_buffer = NULL;
	RD_buffer_size = 0;

	pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, NULL);

	while (RD_running == 1) {
		RD_do_read (0);
	}

	RD_running = 0;

	return NULL;
}


static int
RD_start (void)
{
	if (RD_thread || RD_running)
		return 0;

	RD_running = 1;
	pthread_create (&RD_thread, NULL, RD_main, NULL);

	return 1;
}

static int
RD_stop (void)
{
	if (!RD_thread && !RD_running)
		return 0;

	if (RD_running)
		RD_running = 0;

	if (RD_thread) {
		pthread_cancel(RD_thread);
		RD_thread = 0;
	}

	if (RD_thread || RD_running)
		return 0;

	return 1;
}


/***********************************************************************
 *
 * Start of the glue code which makes this whole mess WORK.
 *
 ***********************************************************************/


static int
u_open(struct pi_socket *ps, struct pi_sockaddr *addr, size_t addrlen)
{
	pi_usb_data_t *data = (pi_usb_data_t *)ps->device->data;

	LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s %d (%s).\n", 
		__FILE__, __LINE__, __FUNCTION__));

	if (RD_running)
		return -1;
	if (!USB_open (data))
		return -1;

	LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s %d (%s).\n", 
		__FILE__, __LINE__, __FUNCTION__));

	return 1;
}

static int
u_close(struct pi_socket *ps)
{
	LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s %d (%s).\n", 
		__FILE__, __LINE__, __FUNCTION__));

	RD_stop ();
	USB_close ();

	LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s %d (%s).\n", 
		__FILE__, __LINE__, __FUNCTION__));

	return close (ps->sd);
}

static int
u_wait_for_device(struct pi_socket *ps, int *timeout)
{
	pi_usb_data_t *data = (pi_usb_data_t *)ps->device->data;
	struct timespec when;
	int ret = 0;

	LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s %d (%s).\n", 
		__FILE__, __LINE__, __FUNCTION__));

	if (*timeout)
		pi_timeout_to_timespec (*timeout, &when);

	while (1) {
		ret = USB_poll (data);
		if (ret > 0) {
			/* Evil, calculate how much longer the timeout is. */
			if (*timeout) {
				*timeout = pi_timespec_to_timeout (&when);
				if (*timeout <= 0)
					*timeout = 1;
			}
			if (!RD_start ()) {
				USB_close ();
				return -1;
			}
			return ret;

		}

		if (*timeout) {
			if (pi_timeout_expired(&when)) {
				*timeout = 1;
				return 0;
			}
		}
		usleep (500000);
	}

	return 0;
}

static int
u_poll(struct pi_socket *ps, int timeout)
{
	LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s %d (%s).\n", 
		__FILE__, __LINE__, __FUNCTION__));

	return u_read_i (ps, NULL, 1, PI_MSG_PEEK, timeout);
}

static ssize_t
u_write(struct pi_socket *ps, const unsigned char *buf, size_t len, int flags)
{
	int timeout = ((struct pi_usb_data *)ps->device->data)->timeout;
	int ret;

	if (!RD_running)
		return -1;

	LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "Writing: len: %d, flags: %d, timeout: %d.\n", len, flags, timeout));
	if (len <= 0)
		return 0;

	ret = usb_bulk_write (USB_handle, USB_out_endpoint, buf, len, timeout);
	LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "Wrote: %d.\n", ret));
	if (ret > 0)
		CHECK (PI_DBG_DEV, PI_DBG_LVL_DEBUG, pi_dumpdata (buf, ret));

	return (ssize_t)ret;
}

static ssize_t
u_read(struct pi_socket *ps, pi_buffer_t *buf, size_t len, int flags)
{
	int ret;

	ret = u_read_i (ps, buf, len, flags, ((struct pi_usb_data *)ps->device->data)->timeout);
	LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "Read: %d (%d).\n", ret, len));
	if (ret > 0)
		CHECK (PI_DBG_DEV, PI_DBG_LVL_DEBUG, pi_dumpdata (buf->data, ret));

	return (ssize_t)ret;
}

static int
u_read_i(struct pi_socket *ps, pi_buffer_t *buf, size_t len, int flags, int timeout)
{
	if (!RD_running)
		return PI_ERR_SOCK_DISCONNECTED;

	LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s %d (%s): %d %d %d\n", 
		__FILE__, __LINE__, __FUNCTION__, len, flags, timeout));

	pthread_mutex_lock (&RD_buffer_mutex);
	if (flags & PI_MSG_PEEK && len > 256)
		len = 256;

	if (RD_buffer_used < len) {
		struct timeval now;
		struct timespec when, nownow;
		int last_used;
		gettimeofday(&now, NULL);
		when.tv_sec = now.tv_sec + timeout / 1000;
		when.tv_nsec = (now.tv_usec + (timeout % 1000) * 1000) * 1000;
		if (when.tv_nsec >= 1000000000) {
			when.tv_nsec -= 1000000000;
			when.tv_sec++;
		}

		RD_wanted = len;
		do {
			last_used = RD_buffer_used;

			LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s %d (%s): %d %d.\n", 
				__FILE__, __LINE__, __FUNCTION__, len, RD_buffer_used));

			if (timeout) {
				gettimeofday(&now, NULL);
				nownow.tv_sec = now.tv_sec;
				nownow.tv_nsec = now.tv_usec * 1000;
				if ((nownow.tv_sec == when.tv_sec ? (nownow.tv_nsec > when.tv_nsec) : (nownow.tv_sec > when.tv_sec))) {
					LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s %d (%s): %d %d.\n", 
						__FILE__, __LINE__, __FUNCTION__, len, RD_buffer_used));
					break;
				}
				LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s %d (%s): %d %d.\n", 
					__FILE__, __LINE__, __FUNCTION__, len, RD_buffer_used));
				if (pthread_cond_timedwait (&RD_buffer_available_cond, &RD_buffer_mutex, &when) == ETIMEDOUT) {
					LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s %d (%s): %d %d.\n", 
						__FILE__, __LINE__, __FUNCTION__, len, RD_buffer_used));
					break;
				}
			} else
				pthread_cond_wait (&RD_buffer_available_cond, &RD_buffer_mutex);
			LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s %d (%s): %d %d.\n", 
				__FILE__, __LINE__, __FUNCTION__, len, RD_buffer_used));
		} while (RD_buffer_used < len);

		RD_wanted = 0;
	}

	LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s %d (%s): %d %d.\n", 
		__FILE__, __LINE__, __FUNCTION__, len, RD_buffer_used));

	if (!RD_running) {
		pthread_mutex_unlock (&RD_buffer_mutex);
		return PI_ERR_SOCK_DISCONNECTED;
	}

	if (RD_buffer_used < len)
		len = RD_buffer_used;
	
	if (len && buf) {
		pi_buffer_append (buf, RD_buffer, len);
		if (!(flags & PI_MSG_PEEK)) {
			RD_buffer_used -= len;
			if (RD_buffer_used)
				memmove (RD_buffer, RD_buffer + len, RD_buffer_used);

			if ((RD_buffer_size - RD_buffer_used) > (1024 * 1024)) {
				/* If we have more then 1M free in the buffer, shrink it. */
				RD_buffer_size = ((RD_buffer_used + 0xfffe) & ~0xffff) - 1;
				RD_buffer = realloc (RD_buffer, RD_buffer_size);
			}
		}
	}

	pthread_mutex_unlock (&RD_buffer_mutex);
	LOG((PI_DBG_DEV, PI_DBG_LVL_DEBUG, "%s %d (%s).\n", 
		__FILE__, __LINE__, __FUNCTION__));
	return len;
}

static int
u_flush(pi_socket_t *ps, int flags)
{
	if (flags & PI_FLUSH_INPUT) {
		/* clear internal buffer */
		pthread_mutex_lock (&RD_buffer_mutex);
		RD_buffer_used = 0;
		pthread_mutex_unlock (&RD_buffer_mutex);
	}
	return 0;
}

static int
u_control_request (pi_usb_data_t *usb_data, int request_type, int request,
		int value, int control_index, void *data, int size, int timeout)
{
	return usb_control_msg (usb_data->ref, request_type, request, value, control_index, data, size, timeout);
}

static int
u_interrupt_read (pi_usb_data_t *usb_data, int ep, void *data, int size, int timeout)
{
	return usb_interrupt_read(usb_data->ref, ep, data, size, timeout);
}

/* vi: set ts=8 sw=4 sts=4 noexpandtab: cin */
/* ex: set tabstop=4 expandtab: */
/* Local Variables: */
/* indent-tabs-mode: t */
/* c-basic-offset: 8 */
/* End: */
