/* Support for the HID Boot Protocol.  */
/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 2008, 2009  Free Software Foundation, Inc.
 *
 *  GRUB 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 3 of the License, or
 *  (at your option) any later version.
 *
 *  GRUB 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 GRUB.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <grub/term.h>
#include <grub/machine/machine.h>
#include <grub/machine/console.h>
#include <grub/time.h>
#include <grub/cpu/io.h>
#include <grub/misc.h>
#include <grub/term.h>
#include <grub/usb.h>
#include <grub/dl.h>
#include <grub/time.h>


static char keyboard_map[128] =
  {
    '\0', '\0', '\0', '\0', 'a', 'b', 'c', 'd',
    'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
    'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
    'u', 'v', 'w', 'x', 'y', 'z', '1', '2',
    '3', '4', '5', '6', '7', '8', '9', '0',
    '\n', GRUB_TERM_ESC, GRUB_TERM_BACKSPACE, GRUB_TERM_TAB, ' ', '-', '=', '[',
    ']', '\\', '#', ';', '\'', '`', ',', '.',
    '/', '\0', '\0', '\0', '\0', '\0', '\0', '\0',
    '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0',
    '\0', '\0', GRUB_TERM_HOME, GRUB_TERM_PPAGE, GRUB_TERM_DC, GRUB_TERM_END, GRUB_TERM_NPAGE, GRUB_TERM_RIGHT,
    GRUB_TERM_LEFT, GRUB_TERM_DOWN, GRUB_TERM_UP
  };

static char keyboard_map_shift[128] =
  {
    '\0', '\0', '\0', '\0', 'A', 'B', 'C', 'D',
    'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
    'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
    'U', 'V', 'W', 'X', 'Y', 'Z', '!', '@',
    '#', '$', '%', '^', '&', '*', '(', ')',
    '\n', '\0', '\0', '\0', ' ', '_', '+', '{',
    '}', '|', '#', ':', '"', '`', '<', '>',
    '?'
  };

static grub_usb_device_t usbdev;

static void
grub_usb_hid (void)
{
  struct grub_usb_desc_device *descdev;

  auto int usb_iterate (grub_usb_device_t dev);
  int usb_iterate (grub_usb_device_t dev)
    {
      descdev = &dev->descdev;

      grub_dprintf ("usb_keyboard", "%x %x %x\n",
		   descdev->class, descdev->subclass, descdev->protocol);

#if 0
      if (descdev->class != 0x09
	  || descdev->subclass == 0x01
	  || descdev->protocol != 0x02)
	return 0;
#endif

      if (descdev->class != 0 || descdev->subclass != 0 || descdev->protocol != 0)
	return 0;

      grub_printf ("HID found!\n");

      usbdev = dev;

      return 1;
    }
  grub_usb_iterate (usb_iterate);

  /* Place the device in boot mode.  */
  grub_usb_control_msg (usbdev, 0x21, 0x0B, 0, 0, 0, 0);

  /* Reports every time an event occurs and not more often than that.  */
  grub_usb_control_msg (usbdev, 0x21, 0x0A, 0<<8, 0, 0, 0);
}

static grub_err_t
grub_usb_keyboard_getreport (grub_usb_device_t dev, unsigned char *report)
{
  return grub_usb_control_msg (dev, (1 << 7) | (1 << 5) | 1, 0x01, 0, 0,
			       8, (char *) report);
}



static int
grub_usb_keyboard_checkkey (void)
{
  unsigned char data[8];
  int key;
  int i;
  grub_err_t err;

  data[2] = 0;
  for (i = 0; i < 50; i++)
    {
      /* Get_Report.  */
      err = grub_usb_keyboard_getreport (usbdev, data);

      if (! err && data[2])
	break;
    }

  if (err || !data[2])
    return -1;

  grub_dprintf ("usb_keyboard",
		"report: 0x%02x 0x%02x 0x%02x 0x%02x"
		" 0x%02x 0x%02x 0x%02x 0x%02x\n",
		data[0], data[1], data[2], data[3],
		data[4], data[5], data[6], data[7]);

  /* Check if the Control or Shift key was pressed.  */
  if (data[0] & 0x01 || data[0] & 0x10)
    key = keyboard_map[data[2]] - 'a' + 1;
  else if (data[0] & 0x02 || data[0] & 0x20)
    key = keyboard_map_shift[data[2]];
  else
    key = keyboard_map[data[2]];

  if (key == 0)
    grub_printf ("Unknown key 0x%x detected\n", data[2]);

#if 0
  /* Wait until the key is released.  */
  while (!err && data[2])
    {
      err = grub_usb_control_msg (usbdev, (1 << 7) | (1 << 5) | 1, 0x01, 0, 0,
				  sizeof (data), (char *) data);
      grub_dprintf ("usb_keyboard",
		    "report2: 0x%02x 0x%02x 0x%02x 0x%02x"
		    " 0x%02x 0x%02x 0x%02x 0x%02x\n",
		    data[0], data[1], data[2], data[3],
		    data[4], data[5], data[6], data[7]);
    }
#endif

  grub_errno = GRUB_ERR_NONE;

  return key;
}

typedef enum
{
  GRUB_HIDBOOT_REPEAT_NONE,
  GRUB_HIDBOOT_REPEAT_FIRST,
  GRUB_HIDBOOT_REPEAT
} grub_usb_keyboard_repeat_t;

static int
grub_usb_keyboard_getkey (void)
{
  int key;
  grub_err_t err;
  unsigned char data[8];
  grub_uint64_t currtime;
  int timeout;
  static grub_usb_keyboard_repeat_t repeat = GRUB_HIDBOOT_REPEAT_NONE;

 again:

  do
    {
      key = grub_usb_keyboard_checkkey ();
    } while (key == -1);

  data[2] = !0; /* Or whatever.  */
  err = 0;

  switch (repeat)
    {
    case GRUB_HIDBOOT_REPEAT_FIRST:
      timeout = 500;
      break;
    case GRUB_HIDBOOT_REPEAT:
      timeout = 50;
      break;
    default:
      timeout = 100;
      break;
    }

  /* Wait until the key is released.  */
  currtime = grub_get_time_ms ();
  while (!err && data[2])
    {
      /* Implement a timeout.  */
      if (grub_get_time_ms () > currtime + timeout)
	{
	  if (repeat == 0)
	    repeat = 1;
	  else
	    repeat = 2;

	  grub_errno = GRUB_ERR_NONE;
	  return key;
	}

      err = grub_usb_keyboard_getreport (usbdev, data);
    }

  if (repeat)
    {
      repeat = 0;
      goto again;
    }

  repeat = 0;

  grub_errno = GRUB_ERR_NONE;

  return key;
}

static struct grub_term_input grub_usb_keyboard_term =
  {
    .name = "usb_keyboard",
    .checkkey = grub_usb_keyboard_checkkey,
    .getkey = grub_usb_keyboard_getkey,
    .next = 0
  };

GRUB_MOD_INIT(usb_keyboard)
{
  grub_usb_hid ();
  grub_term_register_input ("usb_keyboard", &grub_usb_keyboard_term);
}

GRUB_MOD_FINI(usb_keyboard)
{
  grub_term_unregister_input (&grub_usb_keyboard_term);
}
