/*
 * Haiku Backend for libusb
 * Copyright © 2014 Akshay Jaggi <akshay1994.leo@gmail.com>
 *
 * 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
 */


#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <new>
#include <vector>

#include "haiku_usb.h"

static int _errno_to_libusb(int status)
{
	return status;
}

USBTransfer::USBTransfer(struct usbi_transfer *itransfer, USBDevice *device)
{
	fUsbiTransfer = itransfer;
	fLibusbTransfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer);
	fUSBDevice = device;
	fCancelled = false;
}

USBTransfer::~USBTransfer()
{
}

struct usbi_transfer *
USBTransfer::UsbiTransfer()
{
	return fUsbiTransfer;
}

void
USBTransfer::SetCancelled()
{
	fCancelled = true;
}

bool
USBTransfer::IsCancelled()
{
	return fCancelled;
}

void
USBTransfer::Do(int fRawFD)
{
	switch (fLibusbTransfer->type) {
		case LIBUSB_TRANSFER_TYPE_CONTROL:
		{
			struct libusb_control_setup *setup = (struct libusb_control_setup *)fLibusbTransfer->buffer;
			usb_raw_command command;
			command.control.request_type = setup->bmRequestType;
			command.control.request = setup->bRequest;
			command.control.value = setup->wValue;
			command.control.index = setup->wIndex;
			command.control.length = setup->wLength;
			command.control.data = fLibusbTransfer->buffer + LIBUSB_CONTROL_SETUP_SIZE;
			if (fCancelled)
				break;
			if (ioctl(fRawFD, B_USB_RAW_COMMAND_CONTROL_TRANSFER, &command, sizeof(command)) ||
					command.control.status != B_USB_RAW_STATUS_SUCCESS) {
				fUsbiTransfer->transferred = -1;
				usbi_err(TRANSFER_CTX(fLibusbTransfer), "failed control transfer");
				break;
			}
			fUsbiTransfer->transferred = command.control.length;
		}
		break;
		case LIBUSB_TRANSFER_TYPE_BULK:
		case LIBUSB_TRANSFER_TYPE_INTERRUPT:
		{
			usb_raw_command command;
			command.transfer.interface = fUSBDevice->EndpointToInterface(fLibusbTransfer->endpoint);
			command.transfer.endpoint = fUSBDevice->EndpointToIndex(fLibusbTransfer->endpoint);
			command.transfer.data = fLibusbTransfer->buffer;
			command.transfer.length = fLibusbTransfer->length;
			if (fCancelled)
				break;
			if (fLibusbTransfer->type == LIBUSB_TRANSFER_TYPE_BULK) {
				if (ioctl(fRawFD, B_USB_RAW_COMMAND_BULK_TRANSFER, &command, sizeof(command)) ||
						command.transfer.status != B_USB_RAW_STATUS_SUCCESS) {
					fUsbiTransfer->transferred = -1;
					usbi_err(TRANSFER_CTX(fLibusbTransfer), "failed bulk transfer");
					break;
				}
			}
			else {
				if (ioctl(fRawFD, B_USB_RAW_COMMAND_INTERRUPT_TRANSFER, &command, sizeof(command)) ||
						command.transfer.status != B_USB_RAW_STATUS_SUCCESS) {
					fUsbiTransfer->transferred = -1;
					usbi_err(TRANSFER_CTX(fLibusbTransfer), "failed interrupt transfer");
					break;
				}
			}
			fUsbiTransfer->transferred = command.transfer.length;
		}
		break;
		// IsochronousTransfers not tested
		case LIBUSB_TRANSFER_TYPE_ISOCHRONOUS:
		{
			usb_raw_command command;
			command.isochronous.interface = fUSBDevice->EndpointToInterface(fLibusbTransfer->endpoint);
			command.isochronous.endpoint = fUSBDevice->EndpointToIndex(fLibusbTransfer->endpoint);
			command.isochronous.data = fLibusbTransfer->buffer;
			command.isochronous.length = fLibusbTransfer->length;
			command.isochronous.packet_count = fLibusbTransfer->num_iso_packets;
			int i;
			usb_iso_packet_descriptor *packetDescriptors = new usb_iso_packet_descriptor[fLibusbTransfer->num_iso_packets];
			for (i = 0; i < fLibusbTransfer->num_iso_packets; i++) {
				if ((fLibusbTransfer->iso_packet_desc[i]).length > (unsigned int)INT16_MAX) {
					fUsbiTransfer->transferred = -1;
					usbi_err(TRANSFER_CTX(fLibusbTransfer), "failed isochronous transfer");
					break;
				}
				packetDescriptors[i].request_length = (int16)(fLibusbTransfer->iso_packet_desc[i]).length;
			}
			if (i < fLibusbTransfer->num_iso_packets)
				break;	// TODO Handle this error
			command.isochronous.packet_descriptors = packetDescriptors;
			if (fCancelled)
				break;
			if (ioctl(fRawFD, B_USB_RAW_COMMAND_ISOCHRONOUS_TRANSFER, &command, sizeof(command)) ||
					command.isochronous.status != B_USB_RAW_STATUS_SUCCESS) {
				fUsbiTransfer->transferred = -1;
				usbi_err(TRANSFER_CTX(fLibusbTransfer), "failed isochronous transfer");
				break;
			}
			for (i = 0; i < fLibusbTransfer->num_iso_packets; i++) {
				(fLibusbTransfer->iso_packet_desc[i]).actual_length = packetDescriptors[i].actual_length;
				switch (packetDescriptors[i].status) {
					case B_OK:
						(fLibusbTransfer->iso_packet_desc[i]).status = LIBUSB_TRANSFER_COMPLETED;
						break;
					default:
						(fLibusbTransfer->iso_packet_desc[i]).status = LIBUSB_TRANSFER_ERROR;
						break;
				}
			}
			delete[] packetDescriptors;
			// Do we put the length of transfer here, for isochronous transfers?
			fUsbiTransfer->transferred = command.transfer.length;
		}
		break;
		default:
			usbi_err(TRANSFER_CTX(fLibusbTransfer), "Unknown type of transfer");
	}
}

bool
USBDeviceHandle::InitCheck()
{
	return fInitCheck;
}

status_t
USBDeviceHandle::TransfersThread(void *self)
{
	USBDeviceHandle *handle = (USBDeviceHandle *)self;
	handle->TransfersWorker();
	return B_OK;
}

void
USBDeviceHandle::TransfersWorker()
{
	while (true) {
		status_t status = acquire_sem(fTransfersSem);
		if (status == B_BAD_SEM_ID)
			break;
		if (status == B_INTERRUPTED)
			continue;
		fTransfersLock.Lock();
		USBTransfer *fPendingTransfer = (USBTransfer *) fTransfers.RemoveItem((int32)0);
		fTransfersLock.Unlock();
		fPendingTransfer->Do(fRawFD);
		usbi_signal_transfer_completion(fPendingTransfer->UsbiTransfer());
	}
}

status_t
USBDeviceHandle::SubmitTransfer(struct usbi_transfer *itransfer)
{
	USBTransfer *transfer = new USBTransfer(itransfer, fUSBDevice);
	*((USBTransfer **)usbi_get_transfer_priv(itransfer)) = transfer;
	BAutolock locker(fTransfersLock);
	fTransfers.AddItem(transfer);
	release_sem(fTransfersSem);
	return LIBUSB_SUCCESS;
}

status_t
USBDeviceHandle::CancelTransfer(USBTransfer *transfer)
{
	transfer->SetCancelled();
	fTransfersLock.Lock();
	bool removed = fTransfers.RemoveItem(transfer);
	fTransfersLock.Unlock();
	if (removed)
		usbi_signal_transfer_completion(transfer->UsbiTransfer());
	return LIBUSB_SUCCESS;
}

USBDeviceHandle::USBDeviceHandle(USBDevice *dev)
	:
	fUSBDevice(dev),
	fClaimedInterfaces(0),
	fTransfersThread(-1),
	fInitCheck(false)
{
	fRawFD = open(dev->Location(), O_RDWR | O_CLOEXEC);
	if (fRawFD < 0) {
		usbi_err(NULL,"failed to open device");
		return;
	}
	fTransfersSem = create_sem(0, "Transfers Queue Sem");
	fTransfersThread = spawn_thread(TransfersThread, "Transfer Worker", B_NORMAL_PRIORITY, this);
	resume_thread(fTransfersThread);
	fInitCheck = true;
}

USBDeviceHandle::~USBDeviceHandle()
{
	if (fRawFD > 0)
		close(fRawFD);
	for (int i = 0; i < 32; i++) {
		if (fClaimedInterfaces & (1U << i))
			ReleaseInterface(i);
	}
	delete_sem(fTransfersSem);
	if (fTransfersThread > 0)
		wait_for_thread(fTransfersThread, NULL);
}

int
USBDeviceHandle::ClaimInterface(uint8 inumber)
{
	int status = fUSBDevice->ClaimInterface(inumber);
	if (status == LIBUSB_SUCCESS)
		fClaimedInterfaces |= (1U << inumber);
	return status;
}

int
USBDeviceHandle::ReleaseInterface(uint8 inumber)
{
	fUSBDevice->ReleaseInterface(inumber);
	fClaimedInterfaces &= ~(1U << inumber);
	return LIBUSB_SUCCESS;
}

int
USBDeviceHandle::SetConfiguration(uint8 config)
{
	int config_index = fUSBDevice->CheckInterfacesFree(config);
	if (config_index == LIBUSB_ERROR_BUSY || config_index == LIBUSB_ERROR_NOT_FOUND)
		return config_index;
	usb_raw_command command;
	command.config.config_index = config_index;
	if (ioctl(fRawFD, B_USB_RAW_COMMAND_SET_CONFIGURATION, &command, sizeof(command)) ||
			command.config.status != B_USB_RAW_STATUS_SUCCESS) {
		return _errno_to_libusb(command.config.status);
	}
	fUSBDevice->SetActiveConfiguration((uint8)config_index);
	return LIBUSB_SUCCESS;
}

int
USBDeviceHandle::SetAltSetting(uint8 inumber, uint8 alt)
{
	usb_raw_command command;
	command.alternate.config_index = fUSBDevice->ActiveConfigurationIndex();
	command.alternate.interface_index = inumber;
	if (ioctl(fRawFD, B_USB_RAW_COMMAND_GET_ACTIVE_ALT_INTERFACE_INDEX, &command, sizeof(command)) ||
			command.alternate.status != B_USB_RAW_STATUS_SUCCESS) {
		usbi_err(NULL, "Error retrieving active alternate interface");
		return _errno_to_libusb(command.alternate.status);
	}
	if (command.alternate.alternate_info == (uint32)alt) {
		usbi_dbg(NULL, "Setting alternate interface successful");
		return LIBUSB_SUCCESS;
	}
	command.alternate.alternate_info = alt;
	if (ioctl(fRawFD, B_USB_RAW_COMMAND_SET_ALT_INTERFACE, &command, sizeof(command)) ||
			command.alternate.status != B_USB_RAW_STATUS_SUCCESS) { //IF IOCTL FAILS DEVICE DISCONNECTED PROBABLY
		usbi_err(NULL, "Error setting alternate interface");
		return _errno_to_libusb(command.alternate.status);
	}
	usbi_dbg(NULL, "Setting alternate interface successful");
	return LIBUSB_SUCCESS;
}

int
USBDeviceHandle::ClearHalt(uint8 endpoint)
{
	usb_raw_command command;
	command.control.request_type = USB_REQTYPE_ENDPOINT_OUT;
	command.control.request = USB_REQUEST_CLEAR_FEATURE;
	command.control.value = USB_FEATURE_ENDPOINT_HALT;
	command.control.index = endpoint;
	command.control.length = 0;

	if (ioctl(fRawFD, B_USB_RAW_COMMAND_CONTROL_TRANSFER, &command, sizeof(command)) ||
			command.control.status != B_USB_RAW_STATUS_SUCCESS) {
		return _errno_to_libusb(command.control.status);
	}
	return LIBUSB_SUCCESS;
}


USBDevice::USBDevice(const char *path)
	:
	fClaimedInterfaces(0),
	fConfigurationDescriptors(NULL),
	fActiveConfiguration(0),	//0?
	fPath(NULL),
	fEndpointToIndex(NULL),
	fEndpointToInterface(NULL),
	fInitCheck(false)
{
	fPath=strdup(path);
	Initialise();
}

USBDevice::~USBDevice()
{
	free(fPath);
	if (fConfigurationDescriptors) {
		for (uint8 i = 0; i < fDeviceDescriptor.num_configurations; i++) {
			if (fConfigurationDescriptors[i])
				delete fConfigurationDescriptors[i];
		}
		delete[] fConfigurationDescriptors;
	}
	if (fEndpointToIndex)
		delete[] fEndpointToIndex;
	if (fEndpointToInterface)
		delete[] fEndpointToInterface;
}

bool
USBDevice::InitCheck()
{
	return fInitCheck;
}

const char *
USBDevice::Location() const
{
	return fPath;
}

uint8
USBDevice::CountConfigurations() const
{
	return fDeviceDescriptor.num_configurations;
}

const usb_device_descriptor *
USBDevice::Descriptor() const
{
	return &fDeviceDescriptor;
}

const usb_configuration_descriptor *
USBDevice::ConfigurationDescriptor(uint8 index) const
{
	if (index > CountConfigurations())
		return NULL;
	return (usb_configuration_descriptor *) fConfigurationDescriptors[index];
}

const usb_configuration_descriptor *
USBDevice::ActiveConfiguration() const
{
	return (usb_configuration_descriptor *) fConfigurationDescriptors[fActiveConfiguration];
}

uint8
USBDevice::ActiveConfigurationIndex() const
{
	return fActiveConfiguration;
}

int USBDevice::ClaimInterface(uint8 interface)
{
	if (interface > ActiveConfiguration()->number_interfaces)
		return LIBUSB_ERROR_NOT_FOUND;
	if (fClaimedInterfaces & (1U << interface))
		return LIBUSB_ERROR_BUSY;
	fClaimedInterfaces |= (1U << interface);
	return LIBUSB_SUCCESS;
}

int USBDevice::ReleaseInterface(uint8 interface)
{
	fClaimedInterfaces &= ~(1U << interface);
	return LIBUSB_SUCCESS;
}

int
USBDevice::CheckInterfacesFree(uint8 config)
{
	if (fConfigToIndex.count(config) == 0)
		return LIBUSB_ERROR_NOT_FOUND;
	if (fClaimedInterfaces == 0)
		return fConfigToIndex[config];
	return LIBUSB_ERROR_BUSY;
}

void
USBDevice::SetActiveConfiguration(uint8 config_index)
{
	fActiveConfiguration = config_index;
}

uint8
USBDevice::EndpointToIndex(uint8 address) const
{
	return fEndpointToIndex[fActiveConfiguration][address];
}

uint8
USBDevice::EndpointToInterface(uint8 address) const
{
	return fEndpointToInterface[fActiveConfiguration][address];
}

int
USBDevice::Initialise()		//Do we need more error checking, etc? How to report?
{
	int fRawFD = open(fPath, O_RDWR | O_CLOEXEC);
	if (fRawFD < 0)
		return B_ERROR;
	usb_raw_command command;
	command.device.descriptor = &fDeviceDescriptor;
	if (ioctl(fRawFD, B_USB_RAW_COMMAND_GET_DEVICE_DESCRIPTOR, &command, sizeof(command)) ||
			command.device.status != B_USB_RAW_STATUS_SUCCESS) {
		close(fRawFD);
		return B_ERROR;
	}

	fConfigurationDescriptors = new(std::nothrow) unsigned char *[fDeviceDescriptor.num_configurations];
	fEndpointToIndex = new(std::nothrow) map<uint8,uint8> [fDeviceDescriptor.num_configurations];
	fEndpointToInterface = new(std::nothrow) map<uint8,uint8> [fDeviceDescriptor.num_configurations];
	for (uint8 i = 0; i < fDeviceDescriptor.num_configurations; i++) {
		usb_configuration_descriptor tmp_config;
		command.config.descriptor = &tmp_config;
		command.config.config_index = i;
		if (ioctl(fRawFD, B_USB_RAW_COMMAND_GET_CONFIGURATION_DESCRIPTOR, &command, sizeof(command)) ||
				command.config.status != B_USB_RAW_STATUS_SUCCESS) {
			usbi_err(NULL, "failed retrieving configuration descriptor");
			close(fRawFD);
			return B_ERROR;
		}
		fConfigToIndex[tmp_config.configuration_value] = i;
		fConfigurationDescriptors[i] = new(std::nothrow) unsigned char[tmp_config.total_length];

		command.config_etc.descriptor = (usb_configuration_descriptor*)fConfigurationDescriptors[i];
		command.config_etc.length = tmp_config.total_length;
		command.config_etc.config_index = i;
		if (ioctl(fRawFD, B_USB_RAW_COMMAND_GET_CONFIGURATION_DESCRIPTOR_ETC, &command, sizeof(command)) ||
				command.config_etc.status != B_USB_RAW_STATUS_SUCCESS) {
			usbi_err(NULL, "failed retrieving full configuration descriptor");
			close(fRawFD);
			return B_ERROR;
		}

		for (uint8 j = 0; j < tmp_config.number_interfaces; j++) {
			command.alternate.config_index = i;
			command.alternate.interface_index = j;
			if (ioctl(fRawFD, B_USB_RAW_COMMAND_GET_ALT_INTERFACE_COUNT, &command, sizeof(command)) ||
					command.config.status != B_USB_RAW_STATUS_SUCCESS) {
				usbi_err(NULL, "failed retrieving number of alternate interfaces");
				close(fRawFD);
				return B_ERROR;
			}
			uint8 num_alternate = (uint8)command.alternate.alternate_info;
			for (uint8 k = 0; k < num_alternate; k++) {
				usb_interface_descriptor tmp_interface;
				command.interface_etc.config_index = i;
				command.interface_etc.interface_index = j;
				command.interface_etc.alternate_index = k;
				command.interface_etc.descriptor = &tmp_interface;
				if (ioctl(fRawFD, B_USB_RAW_COMMAND_GET_INTERFACE_DESCRIPTOR_ETC, &command, sizeof(command)) ||
						command.config.status != B_USB_RAW_STATUS_SUCCESS) {
					usbi_err(NULL, "failed retrieving interface descriptor");
					close(fRawFD);
					return B_ERROR;
				}
				for (uint8 l = 0; l < tmp_interface.num_endpoints; l++) {
					usb_endpoint_descriptor tmp_endpoint;
					command.endpoint_etc.config_index = i;
					command.endpoint_etc.interface_index = j;
					command.endpoint_etc.alternate_index = k;
					command.endpoint_etc.endpoint_index = l;
					command.endpoint_etc.descriptor = &tmp_endpoint;
					if (ioctl(fRawFD, B_USB_RAW_COMMAND_GET_ENDPOINT_DESCRIPTOR_ETC, &command, sizeof(command)) ||
							command.config.status != B_USB_RAW_STATUS_SUCCESS) {
						usbi_err(NULL, "failed retrieving endpoint descriptor");
						close(fRawFD);
						return B_ERROR;
					}
					fEndpointToIndex[i][tmp_endpoint.endpoint_address] = l;
					fEndpointToInterface[i][tmp_endpoint.endpoint_address] = j;
				}
			}
		}
	}
	close(fRawFD);
	fInitCheck = true;
	return B_OK;
}
