/*
 * 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 <string.h>
#include <errno.h>
#include <assert.h>
#include <ipxe/console.h>
#include <ipxe/keys.h>
#include <ipxe/usb.h>
#include "usbkbd.h"

/** @file
 *
 * USB keyboard driver
 *
 */

/** List of USB keyboards */
static LIST_HEAD ( usb_keyboards );

/******************************************************************************
 *
 * Keyboard map
 *
 ******************************************************************************
 */

/**
 * Map USB keycode to iPXE key
 *
 * @v keycode		Keycode
 * @v modifiers		Modifiers
 * @v leds		LED state
 * @ret key		iPXE key
 *
 * Key codes are defined in the USB HID Usage Tables Keyboard/Keypad
 * page.
 */
static unsigned int usbkbd_map ( unsigned int keycode, unsigned int modifiers,
				 unsigned int leds ) {
	unsigned int key;

	if ( keycode < USBKBD_KEY_A ) {
		/* Not keys */
		key = 0;
	} else if ( keycode <= USBKBD_KEY_Z ) {
		/* Alphabetic keys */
		key = ( keycode - USBKBD_KEY_A + 'a' );
		if ( modifiers & USBKBD_CTRL ) {
			key -= ( 'a' - CTRL_A );
		} else if ( ( modifiers & USBKBD_SHIFT ) ||
			    ( leds & USBKBD_LED_CAPS_LOCK ) ) {
			key -= ( 'a' - 'A' );
		}
	} else if ( keycode <= USBKBD_KEY_0 ) {
		/* Numeric key row */
		if ( modifiers & USBKBD_SHIFT ) {
			key = "!@#$%^&*()" [ keycode - USBKBD_KEY_1 ];
		} else {
			key = ( ( ( keycode - USBKBD_KEY_1 + 1 ) % 10 ) + '0' );
		}
	} else if ( keycode <= USBKBD_KEY_SPACE ) {
		/* Unmodifiable keys */
		static const uint8_t unmodifable[] =
			{ LF, ESC, BACKSPACE, TAB, ' ' };
		key = unmodifable[ keycode - USBKBD_KEY_ENTER ];
	} else if ( keycode <= USBKBD_KEY_SLASH ) {
		/* Punctuation keys */
		if ( modifiers & USBKBD_SHIFT ) {
			key = "_+{}|~:\"~<>?" [ keycode - USBKBD_KEY_MINUS ];
		} else {
			key = "-=[]\\#;'`,./" [ keycode - USBKBD_KEY_MINUS ];
		}
	} else if ( keycode <= USBKBD_KEY_UP ) {
		/* Special keys */
		static const uint16_t special[] = {
			0, 0, 0, 0, 0, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9,
			KEY_F10, KEY_F11, KEY_F12, 0, 0, 0, KEY_IC, KEY_HOME,
			KEY_PPAGE, KEY_DC, KEY_END, KEY_NPAGE, KEY_RIGHT,
			KEY_LEFT, KEY_DOWN, KEY_UP
		};
		key = special[ keycode - USBKBD_KEY_CAPS_LOCK ];
	} else if ( keycode <= USBKBD_KEY_PAD_ENTER ) {
		/* Keypad (unaffected by Num Lock) */
		key = "\0/*-+\n" [ keycode - USBKBD_KEY_NUM_LOCK ];
	} else if ( keycode <= USBKBD_KEY_PAD_DOT ) {
		/* Keypad (affected by Num Lock) */
		if ( leds & USBKBD_LED_NUM_LOCK ) {
			key = "1234567890." [ keycode - USBKBD_KEY_PAD_1 ];
		} else {
			static const uint16_t keypad[] = {
				KEY_END, KEY_DOWN, KEY_NPAGE, KEY_LEFT, 0,
				KEY_RIGHT, KEY_HOME, KEY_UP, KEY_PPAGE,
				KEY_IC, KEY_DC
			};
			key = keypad[ keycode - USBKBD_KEY_PAD_1 ];
		};
	} else {
		key = 0;
	}

	return key;
}

/******************************************************************************
 *
 * Keyboard buffer
 *
 ******************************************************************************
 */

/**
 * Insert keypress into keyboard buffer
 *
 * @v kbd		USB keyboard
 * @v keycode		Keycode
 * @v modifiers		Modifiers
 */
static void usbkbd_produce ( struct usb_keyboard *kbd, unsigned int keycode,
			     unsigned int modifiers ) {
	unsigned int leds = 0;
	unsigned int key;

	/* Check for LED-modifying keys */
	if ( keycode == USBKBD_KEY_CAPS_LOCK ) {
		leds = USBKBD_LED_CAPS_LOCK;
	} else if ( keycode == USBKBD_KEY_NUM_LOCK ) {
		leds = USBKBD_LED_NUM_LOCK;
	}

	/* Handle LED-modifying keys */
	if ( leds ) {
		kbd->leds ^= leds;
		kbd->leds_changed = 1;
		return;
	}

	/* Map to iPXE key */
	key = usbkbd_map ( keycode, modifiers, kbd->leds );

	/* Do nothing if this keycode has no corresponding iPXE key */
	if ( ! key ) {
		DBGC ( kbd, "KBD %s has no key for keycode %#02x:%#02x\n",
		       kbd->name, modifiers, keycode );
		return;
	}

	/* Check for buffer overrun */
	if ( usbkbd_fill ( kbd ) >= USBKBD_BUFSIZE ) {
		DBGC ( kbd, "KBD %s buffer overrun (key %#02x)\n",
		       kbd->name, key );
		return;
	}

	/* Insert into buffer */
	kbd->key[ ( kbd->prod++ ) % USBKBD_BUFSIZE ] = key;
	DBGC2 ( kbd, "KBD %s key %#02x produced\n", kbd->name, key );
}

/**
 * Consume character from keyboard buffer
 *
 * @v kbd		USB keyboard
 * @ret character	Character
 */
static unsigned int usbkbd_consume ( struct usb_keyboard *kbd ) {
	static char buf[] = "\x1b[xx~";
	char *tmp = &buf[2];
	unsigned int key;
	unsigned int character;
	unsigned int ansi_n;
	unsigned int len;

	/* Sanity check */
	assert ( usbkbd_fill ( kbd ) > 0 );

	/* Get current keypress */
	key = kbd->key[ kbd->cons % USBKBD_BUFSIZE ];

	/* If this is a straightforward key, just consume and return it */
	if ( key < KEY_MIN ) {
		kbd->cons++;
		DBGC2 ( kbd, "KBD %s key %#02x consumed\n", kbd->name, key );
		return key;
	}

	/* Construct ANSI sequence */
	ansi_n = KEY_ANSI_N ( key );
	if ( ansi_n )
		tmp += sprintf ( tmp, "%d", ansi_n );
	*(tmp++) = KEY_ANSI_TERMINATOR ( key );
	*tmp = '\0';
	len = ( tmp - buf );
	assert ( len < sizeof ( buf ) );
	if ( kbd->subcons == 0 ) {
		DBGC2 ( kbd, "KBD %s key %#02x consumed as ^[%s\n",
			kbd->name, key, &buf[1] );
	}

	/* Extract character from ANSI sequence */
	assert ( kbd->subcons < len );
	character = buf[ kbd->subcons++ ];

	/* Consume key if applicable */
	if ( kbd->subcons == len ) {
		kbd->cons++;
		kbd->subcons = 0;
	}

	return character;
}

/******************************************************************************
 *
 * Keyboard report
 *
 ******************************************************************************
 */

/**
 * Check for presence of keycode in report
 *
 * @v report		Keyboard report
 * @v keycode		Keycode (must be non-zero)
 * @ret has_keycode	Keycode is present in report
 */
static int usbkbd_has_keycode ( struct usb_keyboard_report *report,
				unsigned int keycode ) {
	unsigned int i;

	/* Check for keycode */
	for ( i = 0 ; i < ( sizeof ( report->keycode ) /
			    sizeof ( report->keycode[0] ) ) ; i++ ) {
		if ( report->keycode[i] == keycode )
			return keycode;
	}

	return 0;
}

/**
 * Handle keyboard report
 *
 * @v kbd		USB keyboard
 * @v new		New keyboard report
 */
static void usbkbd_report ( struct usb_keyboard *kbd,
			    struct usb_keyboard_report *new ) {
	struct usb_keyboard_report *old = &kbd->report;
	unsigned int keycode;
	unsigned int i;

	/* Check if current key has been released */
	if ( kbd->keycode && ! usbkbd_has_keycode ( new, kbd->keycode ) ) {
		DBGC2 ( kbd, "KBD %s keycode %#02x released\n",
			kbd->name, kbd->keycode );
		kbd->keycode = 0;
	}

	/* Decrement auto-repeat hold-off timer, if applicable */
	if ( kbd->holdoff )
		kbd->holdoff--;

	/* Check if a new key has been pressed */
	for ( i = 0 ; i < ( sizeof ( new->keycode ) /
			    sizeof ( new->keycode[0] ) ) ; i++ ) {

		/* Ignore keys present in the previous report */
		keycode = new->keycode[i];
		if ( ( keycode == 0 ) || usbkbd_has_keycode ( old, keycode ) )
			continue;
		DBGC2 ( kbd, "KBD %s keycode %#02x pressed\n",
			kbd->name, keycode );

		/* Insert keypress into keyboard buffer */
		usbkbd_produce ( kbd, keycode, new->modifiers );

		/* Record as most recent keycode */
		kbd->keycode = keycode;

		/* Start auto-repeat hold-off timer */
		kbd->holdoff = USBKBD_HOLDOFF;
	}

	/* Insert auto-repeated keypress into keyboard buffer, if applicable */
	if ( kbd->keycode && ! kbd->holdoff )
		usbkbd_produce ( kbd, kbd->keycode, new->modifiers );

	/* Record report */
	memcpy ( old, new, sizeof ( *old ) );
}

/******************************************************************************
 *
 * Interrupt endpoint
 *
 ******************************************************************************
 */

/**
 * Complete interrupt transfer
 *
 * @v ep		USB endpoint
 * @v iobuf		I/O buffer
 * @v rc		Completion status code
 */
static void usbkbd_complete ( struct usb_endpoint *ep,
			      struct io_buffer *iobuf, int rc ) {
	struct usb_keyboard *kbd = container_of ( ep, struct usb_keyboard,
						  hid.in );
	struct usb_keyboard_report *report;

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

	/* Ignore packets with errors */
	if ( rc != 0 ) {
		DBGC ( kbd, "KBD %s interrupt IN failed: %s\n",
		       kbd->name, strerror ( rc ) );
		goto drop;
	}

	/* Ignore underlength packets */
	if ( iob_len ( iobuf ) < sizeof ( *report ) ) {
		DBGC ( kbd, "KBD %s underlength report:\n", kbd->name );
		DBGC_HDA ( kbd, 0, iobuf->data, iob_len ( iobuf ) );
		goto drop;
	}
	report = iobuf->data;

	/* Handle keyboard report */
	usbkbd_report ( kbd, report );

 drop:
	/* Recycle I/O buffer */
	usb_recycle ( &kbd->hid.in, iobuf );
}

/** Interrupt endpoint operations */
static struct usb_endpoint_driver_operations usbkbd_operations = {
	.complete = usbkbd_complete,
};

/******************************************************************************
 *
 * Keyboard LEDs
 *
 ******************************************************************************
 */

/**
 * Set keyboard LEDs
 *
 * @v kbd		USB keyboard
 * @ret rc		Return status code
 */
static int usbkbd_set_leds ( struct usb_keyboard *kbd ) {
	struct usb_function *func = kbd->hid.func;
	int rc;

	DBGC2 ( kbd, "KBD %s setting LEDs to %#02x\n", kbd->name, kbd->leds );

	/* Set keyboard LEDs */
	if ( ( rc = usbhid_set_report ( func->usb, func->interface[0],
					USBHID_REPORT_OUTPUT, 0, &kbd->leds,
					sizeof ( kbd->leds ) ) ) != 0 ) {
		DBGC ( kbd, "KBD %s could not set LEDs to %#02x: %s\n",
		       kbd->name, kbd->leds, strerror ( rc ) );
		return rc;
	}

	return 0;
}

/******************************************************************************
 *
 * USB interface
 *
 ******************************************************************************
 */

/**
 * Probe device
 *
 * @v func		USB function
 * @v config		Configuration descriptor
 * @ret rc		Return status code
 */
static int usbkbd_probe ( struct usb_function *func,
			  struct usb_configuration_descriptor *config ) {
	struct usb_device *usb = func->usb;
	struct usb_keyboard *kbd;
	int rc;

	/* Allocate and initialise structure */
	kbd = zalloc ( sizeof ( *kbd ) );
	if ( ! kbd ) {
		rc = -ENOMEM;
		goto err_alloc;
	}
	kbd->name = func->name;
	kbd->bus = usb->port->hub->bus;
	usbhid_init ( &kbd->hid, func, &usbkbd_operations, NULL );
	usb_refill_init ( &kbd->hid.in, 0, sizeof ( kbd->report ),
			  USBKBD_INTR_MAX_FILL );

	/* Describe USB human interface device */
	if ( ( rc = usbhid_describe ( &kbd->hid, config ) ) != 0 ) {
		DBGC ( kbd, "KBD %s could not describe: %s\n",
		       kbd->name, strerror ( rc ) );
		goto err_describe;
	}
	DBGC ( kbd, "KBD %s using %s (len %zd)\n",
	       kbd->name, usb_endpoint_name ( &kbd->hid.in ), kbd->hid.in.mtu );

	/* Set boot protocol */
	if ( ( rc = usbhid_set_protocol ( usb, func->interface[0],
					  USBHID_PROTOCOL_BOOT ) ) != 0 ) {
		DBGC ( kbd, "KBD %s could not set boot protocol: %s\n",
		       kbd->name, strerror ( rc ) );
		goto err_set_protocol;
	}

	/* Set idle time */
	if ( ( rc = usbhid_set_idle ( usb, func->interface[0], 0,
				      USBKBD_IDLE_DURATION ) ) != 0 ) {
		DBGC ( kbd, "KBD %s could not set idle time: %s\n",
		       kbd->name, strerror ( rc ) );
		goto err_set_idle;
	}

	/* Open USB human interface device */
	if ( ( rc = usbhid_open ( &kbd->hid ) ) != 0 ) {
		DBGC ( kbd, "KBD %s could not open: %s\n",
		       kbd->name, strerror ( rc ) );
		goto err_open;
	}

	/* Add to list of USB keyboards */
	list_add_tail ( &kbd->list, &usb_keyboards );

	/* Set initial LED state */
	usbkbd_set_leds ( kbd );

	usb_func_set_drvdata ( func, kbd );
	return 0;

	usbhid_close ( &kbd->hid );
 err_open:
 err_set_idle:
 err_set_protocol:
 err_describe:
	free ( kbd );
 err_alloc:
	return rc;
}

/**
 * Remove device
 *
 * @v func		USB function
 */
static void usbkbd_remove ( struct usb_function *func ) {
	struct usb_keyboard *kbd = usb_func_get_drvdata ( func );

	/* Remove from list of USB keyboards */
	list_del ( &kbd->list );

	/* Close USB human interface device */
	usbhid_close ( &kbd->hid );

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

/** USB keyboard device IDs */
static struct usb_device_id usbkbd_ids[] = {
	{
		.name = "kbd",
		.vendor = USB_ANY_ID,
		.product = USB_ANY_ID,
	},
};

/** USB keyboard driver */
struct usb_driver usbkbd_driver __usb_driver = {
	.ids = usbkbd_ids,
	.id_count = ( sizeof ( usbkbd_ids ) / sizeof ( usbkbd_ids[0] ) ),
	.class = USB_CLASS_ID ( USB_CLASS_HID, USB_SUBCLASS_HID_BOOT,
				USBKBD_PROTOCOL ),
	.score = USB_SCORE_NORMAL,
	.probe = usbkbd_probe,
	.remove = usbkbd_remove,
};

/******************************************************************************
 *
 * Console interface
 *
 ******************************************************************************
 */

/**
 * Read a character from the console
 *
 * @ret character	Character read
 */
static int usbkbd_getchar ( void ) {
	struct usb_keyboard *kbd;

	/* Consume first available key */
	list_for_each_entry ( kbd, &usb_keyboards, list ) {
		if ( usbkbd_fill ( kbd ) )
			return usbkbd_consume ( kbd );
	}

	return 0;
}

/**
 * Check for available input
 *
 * @ret is_available	Input is available
 */
static int usbkbd_iskey ( void ) {
	struct usb_keyboard *kbd;
	unsigned int fill;

	/* Poll USB keyboards, refill endpoints, and set LEDs if applicable */
	list_for_each_entry ( kbd, &usb_keyboards, list ) {

		/* Poll keyboard */
		usb_poll ( kbd->bus );

		/* Refill endpoints */
		usb_refill ( &kbd->hid.in );

		/* Update keyboard LEDs, if applicable */
		if ( kbd->leds_changed ) {
			usbkbd_set_leds ( kbd );
			kbd->leds_changed = 0;
		}
	}

	/* Check for a non-empty keyboard buffer */
	list_for_each_entry ( kbd, &usb_keyboards, list ) {
		fill = usbkbd_fill ( kbd );
		if ( fill )
			return fill;
	}

	return 0;
}

/** USB keyboard console */
struct console_driver usbkbd_console __console_driver = {
	.getchar = usbkbd_getchar,
	.iskey = usbkbd_iskey,
};
