/*
 * Copyright (C) 2015 Michael Brown <mbrown@fensystems.co.uk>.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 *
 * You can also choose to distribute this program under the terms of
 * the Unmodified Binary Distribution Licence (as given in the file
 * COPYING.UBDL), provided that you have satisfied its requirements.
 */

FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <assert.h>
#include <ipxe/efi/efi.h>
#include <ipxe/efi/efi_utils.h>
#include <ipxe/efi/efi_driver.h>
#include <ipxe/efi/efi_usb.h>
#include <ipxe/usb.h>

/** @file
 *
 * EFI USB I/O PROTOCOL
 *
 */

/**
 * Transcribe data direction (for debugging)
 *
 * @v direction		Data direction
 * @ret text		Transcribed data direction
 */
static const char * efi_usb_direction_name ( EFI_USB_DATA_DIRECTION direction ){

	switch ( direction ) {
	case EfiUsbDataIn:	return "in";
	case EfiUsbDataOut:	return "out";
	case EfiUsbNoData:	return "none";
	default:		return "<UNKNOWN>";
	}
}

/******************************************************************************
 *
 * Endpoints
 *
 ******************************************************************************
 */

/**
 * Poll USB bus (from endpoint event timer)
 *
 * @v event		EFI event
 * @v context		EFI USB endpoint
 */
static VOID EFIAPI efi_usb_timer ( EFI_EVENT event __unused,
				   VOID *context ) {
	struct efi_usb_endpoint *usbep = context;
	struct usb_bus *bus = usbep->usbintf->usbdev->usb->port->hub->bus;

	/* Poll bus */
	usb_poll ( bus );

	/* Refill endpoint */
	usb_refill ( &usbep->ep );
}

/**
 * Get endpoint MTU
 *
 * @v usbintf		EFI USB interface
 * @v endpoint		Endpoint address
 * @ret mtu		Endpoint MTU, or negative error
 */
static int efi_usb_mtu ( struct efi_usb_interface *usbintf,
			 unsigned int endpoint ) {
	struct efi_usb_device *usbdev = usbintf->usbdev;
	struct usb_interface_descriptor *interface;
	struct usb_endpoint_descriptor *desc;

	/* Locate cached interface descriptor */
	interface = usb_interface_descriptor ( usbdev->config,
					       usbintf->interface,
					       usbintf->alternate );
	if ( ! interface ) {
		DBGC ( usbdev, "USBDEV %s alt %d has no interface descriptor\n",
		       usbintf->name, usbintf->alternate );
		return -ENOENT;
	}

	/* Locate and copy cached endpoint descriptor */
	for_each_interface_descriptor ( desc, usbdev->config, interface ) {
		if ( ( desc->header.type == USB_ENDPOINT_DESCRIPTOR ) &&
		     ( desc->endpoint == endpoint ) )
			return USB_ENDPOINT_MTU ( le16_to_cpu ( desc->sizes ) );
	}

	DBGC ( usbdev, "USBDEV %s alt %d ep %02x has no descriptor\n",
	       usbintf->name, usbintf->alternate, endpoint );
	return -ENOENT;
}

/**
 * Open endpoint
 *
 * @v usbintf		EFI USB interface
 * @v endpoint		Endpoint address
 * @v attributes	Endpoint attributes
 * @v interval		Interval (in milliseconds)
 * @v driver		Driver operations
 * @ret rc		Return status code
 */
static int efi_usb_open ( struct efi_usb_interface *usbintf,
			  unsigned int endpoint, unsigned int attributes,
			  unsigned int interval,
			  struct usb_endpoint_driver_operations *driver ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	struct efi_usb_device *usbdev = usbintf->usbdev;
	struct efi_usb_endpoint *usbep;
	unsigned int index = USB_ENDPOINT_IDX ( endpoint );
	int mtu;
	EFI_STATUS efirc;
	int rc;

	/* Get endpoint MTU */
	mtu = efi_usb_mtu ( usbintf, endpoint );
	if ( mtu < 0 ) {
		rc = mtu;
		goto err_mtu;
	}

	/* Allocate and initialise structure */
	usbep = zalloc ( sizeof ( *usbep ) );
	if ( ! usbep ) {
		rc = -ENOMEM;
		goto err_alloc;
	}
	usbep->usbintf = usbintf;
	usb_endpoint_init ( &usbep->ep, usbdev->usb, driver );
	usb_endpoint_describe ( &usbep->ep, endpoint, attributes, mtu, 0,
				( interval << 3 /* microframes */ ) );

	/* Open endpoint */
	if ( ( rc = usb_endpoint_open ( &usbep->ep ) ) != 0 ) {
		DBGC ( usbdev, "USBDEV %s %s could not open: %s\n",
		       usbintf->name, usb_endpoint_name ( &usbep->ep ),
		       strerror ( rc ) );
		goto err_open;
	}

	/* Record opened endpoint */
	usbintf->endpoint[index] = usbep;
	DBGC ( usbdev, "USBDEV %s %s opened\n",
	       usbintf->name, usb_endpoint_name ( &usbep->ep ) );

	/* Create event */
	if ( ( efirc = bs->CreateEvent ( ( EVT_TIMER | EVT_NOTIFY_SIGNAL ),
					 TPL_CALLBACK, efi_usb_timer, usbep,
					 &usbep->event ) ) != 0 ) {
		rc = -EEFI ( efirc );
		DBGC ( usbdev, "USBDEV %s %s could not create event: %s\n",
		       usbintf->name, usb_endpoint_name ( &usbep->ep ),
		       strerror ( rc ) );
		goto err_event;
	}

	return 0;

	bs->CloseEvent ( usbep->event );
 err_event:
	usbintf->endpoint[index] = usbep;
	usb_endpoint_close ( &usbep->ep );
 err_open:
	free ( usbep );
 err_alloc:
 err_mtu:
	return rc;
}

/**
 * Close endpoint
 *
 * @v usbep		EFI USB endpoint
 */
static void efi_usb_close ( struct efi_usb_endpoint *usbep ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	struct efi_usb_interface *usbintf = usbep->usbintf;
	struct efi_usb_device *usbdev = usbintf->usbdev;
	unsigned int index = USB_ENDPOINT_IDX ( usbep->ep.address );

	/* Sanity check */
	assert ( usbintf->endpoint[index] == usbep );

	/* Cancel timer (if applicable) and close event */
	bs->SetTimer ( usbep->event, TimerCancel, 0 );
	bs->CloseEvent ( usbep->event );

	/* Close endpoint */
	usb_endpoint_close ( &usbep->ep );
	DBGC ( usbdev, "USBDEV %s %s closed\n",
	       usbintf->name, usb_endpoint_name ( &usbep->ep ) );

	/* Free endpoint */
	free ( usbep );

	/* Record closed endpoint */
	usbintf->endpoint[index] = NULL;
}

/**
 * Close all endpoints
 *
 * @v usbintf		EFI USB interface
 */
static void efi_usb_close_all ( struct efi_usb_interface *usbintf ) {
	struct efi_usb_endpoint *usbep;
	unsigned int i;

	for ( i = 0 ; i < ( sizeof ( usbintf->endpoint ) /
			    sizeof ( usbintf->endpoint[0] ) ) ; i++ ) {
		usbep = usbintf->endpoint[i];
		if ( usbep )
			efi_usb_close ( usbep );
	}
}

/**
 * Complete synchronous transfer
 *
 * @v ep		USB endpoint
 * @v iobuf		I/O buffer
 * @v rc		Completion status code
 */
static void efi_usb_sync_complete ( struct usb_endpoint *ep,
				    struct io_buffer *iobuf __unused, int rc ) {
	struct efi_usb_endpoint *usbep =
		container_of ( ep, struct efi_usb_endpoint, ep );

	/* Record completion status */
	usbep->rc = rc;
}

/** Synchronous endpoint operations */
static struct usb_endpoint_driver_operations efi_usb_sync_driver = {
	.complete = efi_usb_sync_complete,
};

/**
 * Perform synchronous transfer
 *
 * @v usbintf		USB endpoint
 * @v endpoint		Endpoint address
 * @v attributes	Endpoint attributes
 * @v timeout		Timeout (in milliseconds)
 * @v data		Data buffer
 * @v len		Length of data buffer
 * @ret rc		Return status code
 */
static int efi_usb_sync_transfer ( struct efi_usb_interface *usbintf,
				   unsigned int endpoint,
				   unsigned int attributes,
				   unsigned int timeout,
				   void *data, size_t *len ) {
	struct efi_usb_device *usbdev = usbintf->usbdev;
	struct efi_usb_endpoint *usbep;
	struct io_buffer *iobuf;
	unsigned int index = USB_ENDPOINT_IDX ( endpoint );
	unsigned int i;
	int rc;

	/* Open endpoint, if applicable */
	if ( ( ! usbintf->endpoint[index] ) &&
	     ( ( rc = efi_usb_open ( usbintf, endpoint, attributes, 0,
				     &efi_usb_sync_driver ) ) != 0 ) ) {
		goto err_open;
	}
	usbep = usbintf->endpoint[index];

	/* Allocate and construct I/O buffer */
	iobuf = alloc_iob ( *len );
	if ( ! iobuf ) {
		rc = -ENOMEM;
		goto err_alloc;
	}
	iob_put ( iobuf, *len );
	if ( ! ( endpoint & USB_ENDPOINT_IN ) )
		memcpy ( iobuf->data, data, *len );

	/* Initialise completion status */
	usbep->rc = -EINPROGRESS;

	/* Enqueue transfer */
	if ( ( rc = usb_stream ( &usbep->ep, iobuf, 0 ) ) != 0 ) {
		DBGC ( usbdev, "USBDEV %s %s could not enqueue: %s\n",
		       usbintf->name, usb_endpoint_name ( &usbep->ep ),
		       strerror ( rc ) );
		goto err_stream;
	}

	/* Wait for completion */
	rc = -ETIMEDOUT;
	for ( i = 0 ; ( ( timeout == 0 ) || ( i < timeout ) ) ; i++ ) {

		/* Poll bus */
		usb_poll ( usbdev->usb->port->hub->bus );

		/* Check for completion */
		if ( usbep->rc != -EINPROGRESS ) {
			rc = usbep->rc;
			break;
		}

		/* Delay */
		mdelay ( 1 );
	}

	/* Check for errors */
	if ( rc != 0 ) {
		DBGC ( usbdev, "USBDEV %s %s failed: %s\n", usbintf->name,
		       usb_endpoint_name ( &usbep->ep ), strerror ( rc ) );
		goto err_completion;
	}

	/* Copy completion to data buffer, if applicable */
	assert ( iob_len ( iobuf ) <= *len );
	if ( endpoint & USB_ENDPOINT_IN )
		memcpy ( data, iobuf->data, iob_len ( iobuf ) );
	*len = iob_len ( iobuf );

	/* Free I/O buffer */
	free_iob ( iobuf );

	/* Leave endpoint open */
	return 0;

 err_completion:
 err_stream:
	free_iob ( iobuf );
 err_alloc:
	efi_usb_close ( usbep );
 err_open:
	return EFIRC ( rc );
}

/**
 * Complete asynchronous transfer
 *
 * @v ep		USB endpoint
 * @v iobuf		I/O buffer
 * @v rc		Completion status code
 */
static void efi_usb_async_complete ( struct usb_endpoint *ep,
				     struct io_buffer *iobuf, int rc ) {
	struct efi_usb_endpoint *usbep =
		container_of ( ep, struct efi_usb_endpoint, ep );
	UINT32 status;

	/* Ignore packets cancelled when the endpoint closes */
	if ( ! ep->open )
		goto drop;

	/* Construct status */
	status = ( ( rc == 0 ) ? 0 : EFI_USB_ERR_STALL );

	/* Report completion */
	usbep->callback ( iobuf->data, iob_len ( iobuf ), usbep->context,
			  status );

 drop:
	/* Recycle I/O buffer */
	usb_recycle ( &usbep->ep, iobuf );
}

/** Asynchronous endpoint operations */
static struct usb_endpoint_driver_operations efi_usb_async_driver = {
	.complete = efi_usb_async_complete,
};

/**
 * Start asynchronous transfer
 *
 * @v usbintf		EFI USB interface
 * @v endpoint		Endpoint address
 * @v interval		Interval (in milliseconds)
 * @v len		Transfer length
 * @v callback		Callback function
 * @v context		Context for callback function
 * @ret rc		Return status code
 */
static int efi_usb_async_start ( struct efi_usb_interface *usbintf,
				 unsigned int endpoint, unsigned int interval,
				 size_t len,
				 EFI_ASYNC_USB_TRANSFER_CALLBACK callback,
				 void *context ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	struct efi_usb_device *usbdev = usbintf->usbdev;
	struct efi_usb_endpoint *usbep;
	unsigned int index = USB_ENDPOINT_IDX ( endpoint );
	EFI_STATUS efirc;
	int rc;

	/* Open endpoint */
	if ( ( rc = efi_usb_open ( usbintf, endpoint,
				   USB_ENDPOINT_ATTR_INTERRUPT, interval,
				   &efi_usb_async_driver ) ) != 0 )
		goto err_open;
	usbep = usbintf->endpoint[index];

	/* Record callback parameters */
	usbep->callback = callback;
	usbep->context = context;

	/* Prefill endpoint */
	usb_refill_init ( &usbep->ep, 0, len, EFI_USB_ASYNC_FILL );
	if ( ( rc = usb_prefill ( &usbep->ep ) ) != 0 ) {
		DBGC ( usbdev, "USBDEV %s %s could not prefill: %s\n",
		       usbintf->name, usb_endpoint_name ( &usbep->ep ),
		       strerror ( rc ) );
		goto err_prefill;
	}

	/* Start timer */
	if ( ( efirc = bs->SetTimer ( usbep->event, TimerPeriodic,
				      ( interval * 10000 ) ) ) != 0 ) {
		rc = -EEFI ( efirc );
		DBGC ( usbdev, "USBDEV %s %s could not set timer: %s\n",
		       usbintf->name, usb_endpoint_name ( &usbep->ep ),
		       strerror ( rc ) );
		goto err_timer;
	}

	return 0;

	bs->SetTimer ( usbep->event, TimerCancel, 0 );
 err_timer:
 err_prefill:
	efi_usb_close ( usbep );
 err_open:
	return rc;
}

/**
 * Stop asynchronous transfer
 *
 * @v usbintf		EFI USB interface
 * @v endpoint		Endpoint address
 */
static void efi_usb_async_stop ( struct efi_usb_interface *usbintf,
				 unsigned int endpoint ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	struct efi_usb_endpoint *usbep;
	unsigned int index = USB_ENDPOINT_IDX ( endpoint );

	/* Do nothing if endpoint is already closed */
	usbep = usbintf->endpoint[index];
	if ( ! usbep )
		return;

	/* Stop timer */
	bs->SetTimer ( usbep->event, TimerCancel, 0 );

	/* Close endpoint */
	efi_usb_close ( usbep );
}

/******************************************************************************
 *
 * USB I/O protocol
 *
 ******************************************************************************
 */

/**
 * Perform control transfer
 *
 * @v usbio		USB I/O protocol
 * @v packet		Setup packet
 * @v direction		Data direction
 * @v timeout		Timeout (in milliseconds)
 * @v data		Data buffer
 * @v len		Length of data
 * @ret status		Transfer status
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_usb_control_transfer ( EFI_USB_IO_PROTOCOL *usbio,
			   EFI_USB_DEVICE_REQUEST *packet,
			   EFI_USB_DATA_DIRECTION direction,
			   UINT32 timeout, VOID *data, UINTN len,
			   UINT32 *status ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	struct efi_usb_interface *usbintf =
		container_of ( usbio, struct efi_usb_interface, usbio );
	struct efi_usb_device *usbdev = usbintf->usbdev;
	unsigned int request = ( packet->RequestType |
				 USB_REQUEST_TYPE ( packet->Request ) );
	unsigned int value = le16_to_cpu ( packet->Value );
	unsigned int index = le16_to_cpu ( packet->Index );
	EFI_TPL saved_tpl;
	int rc;

	DBGC2 ( usbdev, "USBDEV %s control %04x:%04x:%04x:%04x %s %dms "
		"%p+%zx\n", usbintf->name, request, value, index,
		le16_to_cpu ( packet->Length ),
		efi_usb_direction_name ( direction ), timeout, data,
		( ( size_t ) len ) );

	/* Raise TPL */
	saved_tpl = bs->RaiseTPL ( TPL_CALLBACK );

	/* Clear status */
	*status = 0;

	/* Block attempts to change the device configuration, since
	 * this is logically impossible to do given the constraints of
	 * the EFI_USB_IO_PROTOCOL design.
	 */
	if ( ( request == USB_SET_CONFIGURATION ) &&
	     ( value != usbdev->config->config ) ) {
		DBGC ( usbdev, "USBDEV %s cannot set configuration %d: not "
		       "logically possible\n", usbintf->name, index );
		rc = -ENOTSUP;
		goto err_change_config;
	}

	/* If we are selecting a new alternate setting then close all
	 * open endpoints.
	 */
	if ( ( request == USB_SET_INTERFACE ) &&
	     ( value != usbintf->alternate ) )
		efi_usb_close_all ( usbintf );

	/* Issue control transfer */
	if ( ( rc = usb_control ( usbdev->usb, request, value, index,
				  data, len ) ) != 0 ) {
		DBGC ( usbdev, "USBDEV %s control %04x:%04x:%04x:%04x %p+%zx "
		       "failed: %s\n", usbintf->name, request, value, index,
		       le16_to_cpu ( packet->Length ), data, ( ( size_t ) len ),
		       strerror ( rc ) );
		/* Assume that any error represents a stall */
		*status = EFI_USB_ERR_STALL;
		goto err_control;
	}

	/* Update alternate setting, if applicable */
	if ( request == USB_SET_INTERFACE ) {
		usbintf->alternate = value;
		DBGC ( usbdev, "USBDEV %s alt %d selected\n",
		       usbintf->name, usbintf->alternate );
	}

 err_control:
 err_change_config:
	bs->RestoreTPL ( saved_tpl );
	return EFIRC ( rc );
}

/**
 * Perform bulk transfer
 *
 * @v usbio		USB I/O protocol
 * @v endpoint		Endpoint address
 * @v data		Data buffer
 * @v len		Length of data
 * @v timeout		Timeout (in milliseconds)
 * @ret status		Transfer status
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_usb_bulk_transfer ( EFI_USB_IO_PROTOCOL *usbio, UINT8 endpoint, VOID *data,
			UINTN *len, UINTN timeout, UINT32 *status ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	struct efi_usb_interface *usbintf =
		container_of ( usbio, struct efi_usb_interface, usbio );
	struct efi_usb_device *usbdev = usbintf->usbdev;
	size_t actual = *len;
	EFI_TPL saved_tpl;
	int rc;

	DBGC2 ( usbdev, "USBDEV %s bulk %s %p+%zx %dms\n", usbintf->name,
		( ( endpoint & USB_ENDPOINT_IN ) ? "IN" : "OUT" ), data,
		( ( size_t ) *len ), ( ( unsigned int ) timeout ) );

	/* Raise TPL */
	saved_tpl = bs->RaiseTPL ( TPL_CALLBACK );

	/* Clear status */
	*status = 0;

	/* Perform synchronous transfer */
	if ( ( rc = efi_usb_sync_transfer ( usbintf, endpoint,
					    USB_ENDPOINT_ATTR_BULK, timeout,
					    data, &actual ) ) != 0 ) {
		/* Assume that any error represents a timeout */
		*status = EFI_USB_ERR_TIMEOUT;
		goto err_transfer;
	}

 err_transfer:
	bs->RestoreTPL ( saved_tpl );
	return EFIRC ( rc );
}

/**
 * Perform synchronous interrupt transfer
 *
 * @v usbio		USB I/O protocol
 * @v endpoint		Endpoint address
 * @v data		Data buffer
 * @v len		Length of data
 * @v timeout		Timeout (in milliseconds)
 * @ret status		Transfer status
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_usb_sync_interrupt_transfer ( EFI_USB_IO_PROTOCOL *usbio, UINT8 endpoint,
				  VOID *data, UINTN *len, UINTN timeout,
				  UINT32 *status ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	struct efi_usb_interface *usbintf =
		container_of ( usbio, struct efi_usb_interface, usbio );
	struct efi_usb_device *usbdev = usbintf->usbdev;
	size_t actual = *len;
	EFI_TPL saved_tpl;
	int rc;

	DBGC2 ( usbdev, "USBDEV %s sync intr %s %p+%zx %dms\n", usbintf->name,
		( ( endpoint & USB_ENDPOINT_IN ) ? "IN" : "OUT" ), data,
		( ( size_t ) *len ), ( ( unsigned int ) timeout ) );

	/* Raise TPL */
	saved_tpl = bs->RaiseTPL ( TPL_CALLBACK );

	/* Clear status */
	*status = 0;

	/* Perform synchronous transfer */
	if ( ( rc = efi_usb_sync_transfer ( usbintf, endpoint,
					    USB_ENDPOINT_ATTR_INTERRUPT,
					    timeout, data, &actual ) ) != 0 ) {
		/* Assume that any error represents a timeout */
		*status = EFI_USB_ERR_TIMEOUT;
		goto err_transfer;
	}

 err_transfer:
	bs->RestoreTPL ( saved_tpl );
	return EFIRC ( rc );
}

/**
 * Perform asynchronous interrupt transfer
 *
 * @v usbio		USB I/O protocol
 * @v endpoint		Endpoint address
 * @v start		Start (rather than stop) transfer
 * @v interval		Polling interval (in milliseconds)
 * @v len		Data length
 * @v callback		Callback function
 * @v context		Context for callback function
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_usb_async_interrupt_transfer ( EFI_USB_IO_PROTOCOL *usbio, UINT8 endpoint,
				   BOOLEAN start, UINTN interval, UINTN len,
				   EFI_ASYNC_USB_TRANSFER_CALLBACK callback,
				   VOID *context ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	struct efi_usb_interface *usbintf =
		container_of ( usbio, struct efi_usb_interface, usbio );
	struct efi_usb_device *usbdev = usbintf->usbdev;
	EFI_TPL saved_tpl;
	int rc;

	DBGC2 ( usbdev, "USBDEV %s async intr %s len %#zx int %d %p/%p\n",
		usbintf->name,
		( ( endpoint & USB_ENDPOINT_IN ) ? "IN" : "OUT" ),
		( ( size_t ) len ), ( ( unsigned int ) interval ),
		callback, context );

	/* Raise TPL */
	saved_tpl = bs->RaiseTPL ( TPL_CALLBACK );

	/* Start/stop transfer as applicable */
	if ( start ) {

		/* Start new transfer */
		if ( ( rc = efi_usb_async_start ( usbintf, endpoint, interval,
						  len, callback,
						  context ) ) != 0 )
			goto err_start;

	} else {

		/* Stop transfer */
		efi_usb_async_stop ( usbintf, endpoint );

		/* Success */
		rc = 0;

	}

 err_start:
	bs->RestoreTPL ( saved_tpl );
	return EFIRC ( rc );
}

/**
 * Perform synchronous isochronous transfer
 *
 * @v usbio		USB I/O protocol
 * @v endpoint		Endpoint address
 * @v data		Data buffer
 * @v len		Length of data
 * @ret status		Transfer status
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_usb_isochronous_transfer ( EFI_USB_IO_PROTOCOL *usbio, UINT8 endpoint,
			       VOID *data, UINTN len, UINT32 *status ) {
	struct efi_usb_interface *usbintf =
		container_of ( usbio, struct efi_usb_interface, usbio );
	struct efi_usb_device *usbdev = usbintf->usbdev;

	DBGC2 ( usbdev, "USBDEV %s sync iso %s %p+%zx\n", usbintf->name,
		( ( endpoint & USB_ENDPOINT_IN ) ? "IN" : "OUT" ), data,
		( ( size_t ) len ) );

	/* Clear status */
	*status = 0;

	/* Not supported */
	return EFI_UNSUPPORTED;
}

/**
 * Perform asynchronous isochronous transfers
 *
 * @v usbio		USB I/O protocol
 * @v endpoint		Endpoint address
 * @v data		Data buffer
 * @v len		Length of data
 * @v callback		Callback function
 * @v context		Context for callback function
 * @ret status		Transfer status
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_usb_async_isochronous_transfer ( EFI_USB_IO_PROTOCOL *usbio, UINT8 endpoint,
				     VOID *data, UINTN len,
				     EFI_ASYNC_USB_TRANSFER_CALLBACK callback,
				     VOID *context ) {
	struct efi_usb_interface *usbintf =
		container_of ( usbio, struct efi_usb_interface, usbio );
	struct efi_usb_device *usbdev = usbintf->usbdev;

	DBGC2 ( usbdev, "USBDEV %s async iso %s %p+%zx %p/%p\n", usbintf->name,
		( ( endpoint & USB_ENDPOINT_IN ) ? "IN" : "OUT" ), data,
		( ( size_t ) len ), callback, context );

	/* Not supported */
	return EFI_UNSUPPORTED;
}

/**
 * Get device descriptor
 *
 * @v usbio		USB I/O protocol
 * @ret efidesc		EFI device descriptor
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_usb_get_device_descriptor ( EFI_USB_IO_PROTOCOL *usbio,
				EFI_USB_DEVICE_DESCRIPTOR *efidesc ) {
	struct efi_usb_interface *usbintf =
		container_of ( usbio, struct efi_usb_interface, usbio );
	struct efi_usb_device *usbdev = usbintf->usbdev;

	DBGC2 ( usbdev, "USBDEV %s get device descriptor\n", usbintf->name );

	/* Copy cached device descriptor */
	memcpy ( efidesc, &usbdev->usb->device, sizeof ( *efidesc ) );

	return 0;
}

/**
 * Get configuration descriptor
 *
 * @v usbio		USB I/O protocol
 * @ret efidesc		EFI interface descriptor
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_usb_get_config_descriptor ( EFI_USB_IO_PROTOCOL *usbio,
				EFI_USB_CONFIG_DESCRIPTOR *efidesc ) {
	struct efi_usb_interface *usbintf =
		container_of ( usbio, struct efi_usb_interface, usbio );
	struct efi_usb_device *usbdev = usbintf->usbdev;

	DBGC2 ( usbdev, "USBDEV %s get configuration descriptor\n",
		usbintf->name );

	/* Copy cached configuration descriptor */
	memcpy ( efidesc, usbdev->config, sizeof ( *efidesc ) );

	return 0;
}

/**
 * Get interface descriptor
 *
 * @v usbio		USB I/O protocol
 * @ret efidesc		EFI interface descriptor
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_usb_get_interface_descriptor ( EFI_USB_IO_PROTOCOL *usbio,
				   EFI_USB_INTERFACE_DESCRIPTOR *efidesc ) {
	struct efi_usb_interface *usbintf =
		container_of ( usbio, struct efi_usb_interface, usbio );
	struct efi_usb_device *usbdev = usbintf->usbdev;
	struct usb_interface_descriptor *desc;

	DBGC2 ( usbdev, "USBDEV %s get interface descriptor\n", usbintf->name );

	/* Locate cached interface descriptor */
	desc = usb_interface_descriptor ( usbdev->config, usbintf->interface,
					  usbintf->alternate );
	if ( ! desc ) {
		DBGC ( usbdev, "USBDEV %s alt %d has no interface descriptor\n",
		       usbintf->name, usbintf->alternate );
		return -ENOENT;
	}

	/* Copy cached interface descriptor */
	memcpy ( efidesc, desc, sizeof ( *efidesc ) );

	return 0;
}

/**
 * Get endpoint descriptor
 *
 * @v usbio		USB I/O protocol
 * @v address		Endpoint index
 * @ret efidesc		EFI interface descriptor
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_usb_get_endpoint_descriptor ( EFI_USB_IO_PROTOCOL *usbio, UINT8 index,
				  EFI_USB_ENDPOINT_DESCRIPTOR *efidesc ) {
	struct efi_usb_interface *usbintf =
		container_of ( usbio, struct efi_usb_interface, usbio );
	struct efi_usb_device *usbdev = usbintf->usbdev;
	struct usb_interface_descriptor *interface;
	struct usb_endpoint_descriptor *desc;

	DBGC2 ( usbdev, "USBDEV %s get endpoint %d descriptor\n",
		usbintf->name, index );

	/* Locate cached interface descriptor */
	interface = usb_interface_descriptor ( usbdev->config,
					       usbintf->interface,
					       usbintf->alternate );
	if ( ! interface ) {
		DBGC ( usbdev, "USBDEV %s alt %d has no interface descriptor\n",
		       usbintf->name, usbintf->alternate );
		return -ENOENT;
	}

	/* Locate and copy cached endpoint descriptor */
	for_each_interface_descriptor ( desc, usbdev->config, interface ) {
		if ( ( desc->header.type == USB_ENDPOINT_DESCRIPTOR ) &&
		     ( index-- == 0 ) ) {
			memcpy ( efidesc, desc, sizeof ( *efidesc ) );
			return 0;
		}
	}
	return -ENOENT;
}

/**
 * Get string descriptor
 *
 * @v usbio		USB I/O protocol
 * @v language		Language ID
 * @v index		String index
 * @ret string		String
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_usb_get_string_descriptor ( EFI_USB_IO_PROTOCOL *usbio, UINT16 language,
				UINT8 index, CHAR16 **string ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	struct efi_usb_interface *usbintf =
		container_of ( usbio, struct efi_usb_interface, usbio );
	struct efi_usb_device *usbdev = usbintf->usbdev;
	struct usb_descriptor_header header;
	VOID *buffer;
	size_t len;
	EFI_TPL saved_tpl;
	EFI_STATUS efirc;
	int rc;

	DBGC2 ( usbdev, "USBDEV %s get string %d:%d descriptor\n",
		usbintf->name, language, index );

	/* Raise TPL */
	saved_tpl = bs->RaiseTPL ( TPL_CALLBACK );

	/* Read descriptor header */
	if ( ( rc = usb_get_descriptor ( usbdev->usb, 0, USB_STRING_DESCRIPTOR,
					 index, language, &header,
					 sizeof ( header ) ) ) != 0 ) {
		DBGC ( usbdev, "USBDEV %s could not get string %d:%d "
		       "descriptor header: %s\n", usbintf->name, language,
		       index, strerror ( rc ) );
		goto err_get_header;
	}
	len = header.len;

	/* Allocate buffer */
	if ( ( efirc = bs->AllocatePool ( EfiBootServicesData, len,
					  &buffer ) ) != 0 ) {
		rc = -EEFI ( efirc );
		goto err_alloc;
	}

	/* Read whole descriptor */
	if ( ( rc = usb_get_descriptor ( usbdev->usb, 0, USB_STRING_DESCRIPTOR,
					 index, language, buffer,
					 len ) ) != 0 ) {
		DBGC ( usbdev, "USBDEV %s could not get string %d:%d "
		       "descriptor: %s\n", usbintf->name, language,
		       index, strerror ( rc ) );
		goto err_get_descriptor;
	}

	/* Shuffle down and terminate string */
	memmove ( buffer, ( buffer + sizeof ( header ) ),
		  ( len - sizeof ( header ) ) );
	memset ( ( buffer + len - sizeof ( header ) ), 0, sizeof ( **string ) );

	/* Restore TPL */
	bs->RestoreTPL ( saved_tpl );

	/* Return allocated string */
	*string = buffer;
	return 0;

 err_get_descriptor:
	bs->FreePool ( buffer );
 err_alloc:
 err_get_header:
	bs->RestoreTPL ( saved_tpl );
	return EFIRC ( rc );
}

/**
 * Get supported languages
 *
 * @v usbio		USB I/O protocol
 * @ret languages	Language ID table
 * @ret len		Length of language ID table
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_usb_get_supported_languages ( EFI_USB_IO_PROTOCOL *usbio,
				  UINT16 **languages, UINT16 *len ) {
	struct efi_usb_interface *usbintf =
		container_of ( usbio, struct efi_usb_interface, usbio );
	struct efi_usb_device *usbdev = usbintf->usbdev;

	DBGC2 ( usbdev, "USBDEV %s get supported languages\n", usbintf->name );

	/* Return cached supported languages */
	*languages = ( ( ( void * ) usbdev->languages ) +
		       sizeof ( *(usbdev->languages) ) );
	*len = usbdev->languages->len;

	return 0;
}

/**
 * Reset port
 *
 * @v usbio		USB I/O protocol
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_usb_port_reset ( EFI_USB_IO_PROTOCOL *usbio ) {
	struct efi_usb_interface *usbintf =
		container_of ( usbio, struct efi_usb_interface, usbio );
	struct efi_usb_device *usbdev = usbintf->usbdev;

	DBGC2 ( usbdev, "USBDEV %s reset port\n", usbintf->name );

	/* This is logically impossible to do, since resetting the
	 * port may destroy state belonging to other
	 * EFI_USB_IO_PROTOCOL instances belonging to the same USB
	 * device.  (This is yet another artifact of the incredibly
	 * poor design of the EFI_USB_IO_PROTOCOL.)
	 */
	return EFI_INVALID_PARAMETER;
}

/** USB I/O protocol */
static EFI_USB_IO_PROTOCOL efi_usb_io_protocol = {
	.UsbControlTransfer		= efi_usb_control_transfer,
	.UsbBulkTransfer		= efi_usb_bulk_transfer,
	.UsbAsyncInterruptTransfer	= efi_usb_async_interrupt_transfer,
	.UsbSyncInterruptTransfer	= efi_usb_sync_interrupt_transfer,
	.UsbIsochronousTransfer		= efi_usb_isochronous_transfer,
	.UsbAsyncIsochronousTransfer	= efi_usb_async_isochronous_transfer,
	.UsbGetDeviceDescriptor		= efi_usb_get_device_descriptor,
	.UsbGetConfigDescriptor		= efi_usb_get_config_descriptor,
	.UsbGetInterfaceDescriptor	= efi_usb_get_interface_descriptor,
	.UsbGetEndpointDescriptor	= efi_usb_get_endpoint_descriptor,
	.UsbGetStringDescriptor		= efi_usb_get_string_descriptor,
	.UsbGetSupportedLanguages	= efi_usb_get_supported_languages,
	.UsbPortReset			= efi_usb_port_reset,
};

/******************************************************************************
 *
 * USB driver
 *
 ******************************************************************************
 */

/**
 * Install interface
 *
 * @v usbdev		EFI USB device
 * @v interface		Interface number
 * @ret rc		Return status code
 */
static int efi_usb_install ( struct efi_usb_device *usbdev,
			     unsigned int interface ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	struct efi_device *efidev = usbdev->efidev;
	struct efi_usb_interface *usbintf;
	struct usb_device *usb;
	EFI_DEVICE_PATH_PROTOCOL *path_end;
	USB_DEVICE_PATH *usbpath;
	unsigned int path_count;
	size_t path_prefix_len;
	size_t path_len;
	EFI_STATUS efirc;
	int rc;

	/* Calculate device path length */
	path_count = ( usb_depth ( usbdev->usb ) + 1 );
	path_prefix_len = efi_devpath_len ( efidev->path );
	path_len = ( path_prefix_len + ( path_count * sizeof ( *usbpath ) ) +
		     sizeof ( *path_end ) );

	/* Allocate and initialise structure */
	usbintf = zalloc ( sizeof ( *usbintf ) + path_len );
	if ( ! usbintf ) {
		rc = -ENOMEM;
		goto err_alloc;
	}
	snprintf ( usbintf->name, sizeof ( usbintf->name ), "%s[%d]",
		   usbdev->name, interface );
	usbintf->usbdev = usbdev;
	usbintf->interface = interface;
	memcpy ( &usbintf->usbio, &efi_usb_io_protocol,
		 sizeof ( usbintf->usbio ) );
	usbintf->path = ( ( ( void * ) usbintf ) + sizeof ( *usbintf ) );

	/* Construct device path */
	memcpy ( usbintf->path, efidev->path, path_prefix_len );
	path_end = ( ( ( void * ) usbintf->path ) + path_len -
		     sizeof ( *path_end ) );
	path_end->Type = END_DEVICE_PATH_TYPE;
	path_end->SubType = END_ENTIRE_DEVICE_PATH_SUBTYPE;
	path_end->Length[0] = sizeof ( *path_end );
	usbpath = ( ( ( void * ) path_end ) - sizeof ( *usbpath ) );
	usbpath->InterfaceNumber = interface;
	for ( usb = usbdev->usb ; usb ; usbpath--, usb = usb->port->hub->usb ) {
		usbpath->Header.Type = MESSAGING_DEVICE_PATH;
		usbpath->Header.SubType = MSG_USB_DP;
		usbpath->Header.Length[0] = sizeof ( *usbpath );
		usbpath->ParentPortNumber = ( usb->port->address - 1 );
	}

	/* Add to list of interfaces */
	list_add_tail ( &usbintf->list, &usbdev->interfaces );

	/* Install protocols */
	if ( ( efirc = bs->InstallMultipleProtocolInterfaces (
			&usbintf->handle,
			&efi_usb_io_protocol_guid, &usbintf->usbio,
			&efi_device_path_protocol_guid, usbintf->path,
			NULL ) ) != 0 ) {
		rc = -EEFI ( efirc );
		DBGC ( usbdev, "USBDEV %s could not install protocols: %s\n",
		       usbintf->name, strerror ( rc ) );
		goto err_install_protocol;
	}

	DBGC ( usbdev, "USBDEV %s installed as %s\n",
	       usbintf->name, efi_handle_name ( usbintf->handle ) );
	return 0;

	efi_usb_close_all ( usbintf );
	bs->UninstallMultipleProtocolInterfaces (
			usbintf->handle,
			&efi_usb_io_protocol_guid, &usbintf->usbio,
			&efi_device_path_protocol_guid, usbintf->path,
			NULL );
 err_install_protocol:
	list_del ( &usbintf->list );
	free ( usbintf );
 err_alloc:
	return rc;
}

/**
 * Uninstall interface
 *
 * @v usbintf		EFI USB interface
 */
static void efi_usb_uninstall ( struct efi_usb_interface *usbintf ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;

	/* Close all endpoints */
	efi_usb_close_all ( usbintf );

	/* Uninstall protocols */
	bs->UninstallMultipleProtocolInterfaces (
			usbintf->handle,
			&efi_usb_io_protocol_guid, &usbintf->usbio,
			&efi_device_path_protocol_guid, usbintf->path,
			NULL );

	/* Remove from list of interfaces */
	list_del ( &usbintf->list );

	/* Free interface */
	free ( usbintf );
}

/**
 * Uninstall all interfaces
 *
 * @v usbdev		EFI USB device
 */
static void efi_usb_uninstall_all ( struct efi_usb_device *efiusb ) {
	struct efi_usb_interface *usbintf;

	/* Uninstall all interfaces */
	while ( ( usbintf = list_first_entry ( &efiusb->interfaces,
					       struct efi_usb_interface,
					       list ) ) ) {
		efi_usb_uninstall ( usbintf );
	}
}

/**
 * Probe device
 *
 * @v func		USB function
 * @v config		Configuration descriptor
 * @ret rc		Return status code
 */
static int efi_usb_probe ( struct usb_function *func,
			   struct usb_configuration_descriptor *config ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	struct usb_device *usb = func->usb;
	struct efi_usb_device *usbdev;
	struct efi_usb_interface *usbintf;
	struct efi_device *efidev;
	struct usb_descriptor_header header;
	size_t config_len;
	unsigned int i;
	int rc;

	/* Find parent EFI device */
	efidev = efidev_parent ( &func->dev );
	if ( ! efidev ) {
		rc = -ENOTTY;
		goto err_no_efidev;
	}

	/* Get configuration length */
	config_len = le16_to_cpu ( config->len );

	/* Get supported languages descriptor header */
	if ( ( rc = usb_get_descriptor ( usb, 0, USB_STRING_DESCRIPTOR, 0, 0,
					 &header, sizeof ( header ) ) ) != 0 ) {
		/* Assume no strings are present */
		header.len = 0;
	}

	/* Allocate and initialise structure */
	usbdev = zalloc ( sizeof ( *usbdev ) + config_len + header.len );
	if ( ! usbdev ) {
		rc = -ENOMEM;
		goto err_alloc;
	}
	usb_func_set_drvdata ( func, usbdev );
	usbdev->name = func->name;
	usbdev->usb = usb;
	usbdev->efidev = efidev;
	usbdev->config = ( ( ( void * ) usbdev ) + sizeof ( *usbdev ) );
	memcpy ( usbdev->config, config, config_len );
	usbdev->languages = ( ( ( void * ) usbdev->config ) + config_len );
	INIT_LIST_HEAD ( &usbdev->interfaces );

	/* Get supported languages descriptor */
	if ( header.len &&
	     ( rc = usb_get_descriptor ( usb, 0, USB_STRING_DESCRIPTOR, 0, 0,
					 usbdev->languages,
					 header.len ) ) != 0 ) {
		DBGC ( usbdev, "USBDEV %s could not get supported languages: "
		       "%s\n", usbdev->name, strerror ( rc ) );
		goto err_get_languages;
	}

	/* Install interfaces */
	for ( i = 0 ; i < func->desc.count ; i++ ) {
		if ( ( rc = efi_usb_install ( usbdev,
					      func->interface[i] ) ) != 0 )
			goto err_install;
	}

	/* Connect any external drivers */
	list_for_each_entry ( usbintf, &usbdev->interfaces, list )
		bs->ConnectController ( usbintf->handle, NULL, NULL, TRUE );

	return 0;

 err_install:
	efi_usb_uninstall_all ( usbdev );
	assert ( list_empty ( &usbdev->interfaces ) );
 err_get_languages:
	free ( usbdev );
 err_alloc:
 err_no_efidev:
	return rc;
}

/**
 * Remove device
 *
 * @v func		USB function
 */
static void efi_usb_remove ( struct usb_function *func ) {
	struct efi_usb_device *usbdev = usb_func_get_drvdata ( func );

	/* Uninstall all interfaces */
	efi_usb_uninstall_all ( usbdev );
	assert ( list_empty ( &usbdev->interfaces ) );

	/* Free device */
	free ( usbdev );
}

/** USB I/O protocol device IDs */
static struct usb_device_id efi_usb_ids[] = {
	{
		.name = "usbio",
		.vendor = USB_ANY_ID,
		.product = USB_ANY_ID,
	},
};

/** USB I/O protocol driver */
struct usb_driver usbio_driver __usb_driver = {
	.ids = efi_usb_ids,
	.id_count = ( sizeof ( efi_usb_ids ) / sizeof ( efi_usb_ids[0] ) ),
	.class = USB_CLASS_ID ( USB_ANY_ID, USB_ANY_ID, USB_ANY_ID ),
	.score = USB_SCORE_FALLBACK,
	.probe = efi_usb_probe,
	.remove = efi_usb_remove,
};
