/* sendkey.c - fake keystroke. */
/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 2009  Free Software Foundation, Inc.
 *
 *  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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <grub/types.h>
#include <grub/misc.h>
#include <grub/mm.h>
#include <grub/err.h>
#include <grub/dl.h>
#include <grub/extcmd.h>
#include <grub/cpu/io.h>
#include <grub/loader.h>

static char sendkey[0x20];
/* Length of sendkey.  */
static int keylen = 0;
static int noled = 0;
static const struct grub_arg_option options[] =
  {
    {"num", 'n', 0, "set numlock mode", "[on|off]", ARG_TYPE_STRING},
    {"caps", 'c', 0, "set capslock mode", "[on|off]", ARG_TYPE_STRING},
    {"scroll", 's', 0, "set scrolllock mode", "[on|off]", ARG_TYPE_STRING},
    {"insert", 0, 0, "set insert mode", "[on|off]", ARG_TYPE_STRING},
    {"pause", 0, 0, "set pause mode", "[on|off]", ARG_TYPE_STRING},
    {"left-shift", 0, 0, "press left shift", "[on|off]", ARG_TYPE_STRING},
    {"right-shift", 0, 0, "press right shift", "[on|off]", ARG_TYPE_STRING},
    {"sysrq", 0, 0, "press SysRq", "[on|off]", ARG_TYPE_STRING},
    {"numkey", 0, 0, "press NumLock key", "[on|off]", ARG_TYPE_STRING},
    {"capskey", 0, 0, "press CapsLock key", "[on|off]", ARG_TYPE_STRING},
    {"scrollkey", 0, 0, "press ScrollLock key", "[on|off]", ARG_TYPE_STRING},
    {"insertkey", 0, 0, "press Insert key", "[on|off]", ARG_TYPE_STRING},
    {"left-alt", 0, 0, "press left alt", "[on|off]", ARG_TYPE_STRING},
    {"right-alt", 0, 0, "press right alt", "[on|off]", ARG_TYPE_STRING},
    {"left-ctrl", 0, 0, "press left ctrl", "[on|off]", ARG_TYPE_STRING},
    {"right-ctrl", 0, 0, "press right ctrl", "[on|off]", ARG_TYPE_STRING},
    {"no-led", 0, 0, "don't update LED state", 0, 0},
    {0, 0, 0, 0, 0, 0}
  };
static int simple_flag_offsets[] 
= {5, 6, 4, 7, 11, 1, 0, 10, 13, 14, 12, 15, 9, 3, 8, 2};

static grub_uint32_t andmask = 0xffffffff, ormask = 0;

struct 
keysym
{
  char *unshifted_name;			/* the name in unshifted state */
  char *shifted_name;			/* the name in shifted state */
  unsigned char unshifted_ascii;	/* the ascii code in unshifted state */
  unsigned char shifted_ascii;		/* the ascii code in shifted state */
  unsigned char keycode;		/* keyboard scancode */
};

/* The table for key symbols. If the "shifted" member of an entry is
   NULL, the entry does not have shifted state. Copied from GRUB Legacy setkey fuction  */
static struct keysym keysym_table[] =
{
  {"escape",		0,		0x1b,	0,	0x01},
  {"1",			"exclam",	'1',	'!',	0x02},
  {"2",			"at",		'2',	'@',	0x03},
  {"3",			"numbersign",	'3',	'#',	0x04},
  {"4",			"dollar",	'4',	'$',	0x05},
  {"5",			"percent",	'5',	'%',	0x06},
  {"6",			"caret",	'6',	'^',	0x07},
  {"7",			"ampersand",	'7',	'&',	0x08},
  {"8",			"asterisk",	'8',	'*',	0x09},
  {"9",			"parenleft",	'9',	'(',	0x0a},
  {"0",			"parenright",	'0',	')',	0x0b},
  {"minus",		"underscore",	'-',	'_',	0x0c},
  {"equal",		"plus",		'=',	'+',	0x0d},
  {"backspace",		0,		'\b',	0,	0x0e},
  {"tab",		0,		'\t',	0,	0x0f},
  {"q",			"Q",		'q',	'Q',	0x10},
  {"w",			"W",		'w',	'W',	0x11},
  {"e",			"E",		'e',	'E',	0x12},
  {"r",			"R",		'r',	'R',	0x13},
  {"t",			"T",		't',	'T',	0x14},
  {"y",			"Y",		'y',	'Y',	0x15},
  {"u",			"U",		'u',	'U',	0x16},
  {"i",			"I",		'i',	'I',	0x17},
  {"o",			"O",		'o',	'O',	0x18},
  {"p",			"P",		'p',	'P',	0x19},
  {"bracketleft",	"braceleft",	'[',	'{',	0x1a},
  {"bracketright",	"braceright",	']',	'}',	0x1b},
  {"enter",		0,		'\r',	0,	0x1c},
  {"control",		0,		0,	0,	0x1d},
  {"a",			"A",		'a',	'A',	0x1e},
  {"s",			"S",		's',	'S',	0x1f},
  {"d",			"D",		'd',	'D',	0x20},
  {"f",			"F",		'f',	'F',	0x21},
  {"g",			"G",		'g',	'G',	0x22},
  {"h",			"H",		'h',	'H',	0x23},
  {"j",			"J",		'j',	'J',	0x24},
  {"k",			"K",		'k',	'K',	0x25},
  {"l",			"L",		'l',	'L',	0x26},
  {"semicolon",		"colon",	';',	':',	0x27},
  {"quote",		"doublequote",	'\'',	'"',	0x28},
  {"backquote",		"tilde",	'`',	'~',	0x29},
  {"shift",		0,		0,	0,	0x2a},
  {"backslash",		"bar",		'\\',	'|',	0x2b},
  {"z",			"Z",		'z',	'Z',	0x2c},
  {"x",			"X",		'x',	'X',	0x2d},
  {"c",			"C",		'c',	'C',	0x2e},
  {"v",			"V",		'v',	'V',	0x2f},
  {"b",			"B",		'b',	'B',	0x30},
  {"n",			"N",		'n',	'N',	0x31},
  {"m",			"M",		'm',	'M',	0x32},
  {"comma",		"less",		',',	'<',	0x33},
  {"period",		"greater",	'.',	'>',	0x34},
  {"slash",		"question",	'/',	'?',	0x35},
  {"rshift",		0,		0,	0,	0x36},
  {"numasterisk",		0,		'*',	0,	0x37},
  {"alt",		0,		0,	0,	0x38},
  {"space",		0,		' ',	0,	0x39},
  {"capslock",		0,		0,	0,	0x3a},
  {"F1",		0,		0,	0,	0x3b},
  {"F2",		0,		0,	0,	0x3c},
  {"F3",		0,		0,	0,	0x3d},
  {"F4",		0,		0,	0,	0x3e},
  {"F5",		0,		0,	0,	0x3f},
  {"F6",	 	0,		0,	0,	0x40},
  {"F7",		0,		0,	0,	0x41},
  {"F8",		0,		0,	0,	0x42},
  {"F9",		0,		0,	0,	0x43},
  {"F10",		0,		0,	0,	0x44},
  {"num7",		"numhome",		'7',	0,	0x47},
  {"num8",		"numup",		'8',	0,	0x48},
  {"num9",		"numpgup",		'9',	0,	0x49},
  {"numminus",		0,		'-',	0,	0x4a},
  {"num4",		"numleft",		'4',	0,	0x4b},
  {"num5",		"numcenter",		'5',	0,	0x4c},
  {"num6",		"numright",		'6',	0,	0x4d},
  {"numplus",		0,		'-',	0,	0x4e},
  {"num1",		"numend",		'1',	0,	0x4f},
  {"num2",		"numdown",		'2',	0,	0x50},
  {"num3",		"numpgdown",		'3',	0,	0x51},
  {"num0",		"numinsert",		'0',	0,	0x52},
  {"numperiod",	"numdelete", 0,	0x7f,		0x53},
  {"F11",		0,		0,	0,	0x57},
  {"F12",		0,		0,	0,	0x58},
  {"numenter",		0,		'\r',	0,	0xe0},
  {"numslash",		0,		'/',	0,	0xe0},
  {"delete",		0,		0x7f,	0,	0xe0},
  {"insert",		0,		0xe0,	0,	0x52},
  {"home",		0,		0xe0,	0,	0x47},
  {"end",		0,		0xe0,	0,	0x4f},
  {"pgdown",		0,		0xe0,	0,	0x51},
  {"pgup",		0,		0xe0,	0,	0x49},
  {"down",		0,		0xe0,	0,	0x50},
  {"up",		0,		0xe0,	0,	0x48},
  {"left",		0,		0xe0,	0,	0x4b},
  {"right",		0,		0xe0,	0,	0x4d}
};

/* Set a simple flag in flags variable  
   OUTOFFSET - offset of flag in FLAGS,
   OP - action id
*/
static void
grub_sendkey_set_simple_flag (int outoffset, int op)
{      
  if (op == 2)
    {
      andmask |= (1 << outoffset);
      ormask &= ~(1 << outoffset);
    }
  else
    {
      andmask &= (~(1 << outoffset));
      if (op == 1)
	ormask |= (1 << outoffset);
      else
	ormask &= ~(1 << outoffset);
    }
}

static int
grub_sendkey_parse_op (struct grub_arg_list state)
{
  if (! state.set)
    return 2;

  if (grub_strcmp (state.arg, "off") == 0 || grub_strcmp (state.arg, "0") == 0 
      || grub_strcmp (state.arg, "unpress") == 0)
    return 0;

  if (grub_strcmp (state.arg, "on")  == 0 || grub_strcmp (state.arg, "1")  == 0
      || grub_strcmp (state.arg, "press") == 0)
    return 1;

  return 2;
}

static grub_uint32_t oldflags;

static grub_err_t
grub_sendkey_postboot (void)
{
  /* For convention: pointer to flags.  */
  grub_uint32_t *flags = (grub_uint32_t *) 0x417;

  *flags = oldflags;

  *((char *) 0x41a) = 0x1e;
  *((char *) 0x41c) = 0x1e;

  return GRUB_ERR_NONE;
}

/* Set keyboard buffer to our sendkey  */
static grub_err_t
grub_sendkey_preboot (int noret __attribute__ ((unused)))
{
  /* For convention: pointer to flags.  */
  grub_uint32_t *flags = (grub_uint32_t *) 0x417;

  oldflags = *flags;
  
  /* Set the sendkey.  */
  *((char *) 0x41a) = 0x1e;
  *((char *) 0x41c) = keylen + 0x1e;
  grub_memcpy ((char *) 0x41e, sendkey, 0x20);

  /* Transform "any ctrl" to "right ctrl" flag.  */
  if (*flags & (1 << 8))
    *flags &= ~(1 << 2);

  /* Transform "any alt" to "right alt" flag.  */
  if (*flags & (1 << 9))
    *flags &= ~(1 << 3);
  
  *flags = (*flags & andmask) | ormask;

  /* Transform "right ctrl" to "any ctrl" flag.  */
  if (*flags & (1 << 8))
    *flags |= (1 << 2);

  /* Transform "right alt" to "any alt" flag.  */
  if (*flags & (1 << 9))
    *flags |= (1 << 3);

  /* Write new LED state  */
  if (!noled)
    {
      int value = 0;
      int failed;
      /* Try 5 times  */
      for (failed = 0; failed < 5; failed++)
	{
	  value = 0;
	  /* Send command change LEDs  */
	  grub_outb (0xed, 0x60);

	  /* Wait */
	  do
	    value = grub_inb (0x60);
	  while ((value != 0xfa) && (value != 0xfe));

	  if (value == 0xfa)
	    {
	      /* Set new LEDs*/
	      grub_outb ((*flags >> 4) & 7, 0x60);
	      break;
	    }
	}
    }
  return GRUB_ERR_NONE;
}

static grub_err_t
grub_cmd_sendkey (grub_extcmd_context_t ctxt, int argc, char **args)
{
  struct grub_arg_list *state = ctxt->state;

  auto int find_key_code (char *key); 
  auto int find_ascii_code (char *key);

  int find_key_code (char *key)
    {
      unsigned i;

      for (i = 0; i < sizeof (keysym_table) / sizeof (keysym_table[0]); i++)
	{
	  if (keysym_table[i].unshifted_name 
	      && grub_strcmp (key, keysym_table[i].unshifted_name) == 0)
	    return keysym_table[i].keycode;
	  else if (keysym_table[i].shifted_name 
		   && grub_strcmp (key, keysym_table[i].shifted_name) == 0)
	    return keysym_table[i].keycode;
	}

      return 0;
    }

  int find_ascii_code (char *key)
    {
      unsigned i;

      for (i = 0; i < sizeof (keysym_table) / sizeof (keysym_table[0]); i++)
	{
	  if (keysym_table[i].unshifted_name 
	      && grub_strcmp (key, keysym_table[i].unshifted_name) == 0)
	    return keysym_table[i].unshifted_ascii;
	  else if (keysym_table[i].shifted_name 
		   && grub_strcmp (key, keysym_table[i].shifted_name) == 0)
	    return keysym_table[i].shifted_ascii;
	}

      return 0;
    }

  andmask = 0xffffffff;
  ormask = 0;

  {
    int i;

    keylen = 0;

    for (i = 0; i < argc && keylen < 0x20; i++)
      {
	int key_code;
	
	key_code = find_key_code (args[i]);
	if (key_code)
	  {
	    sendkey[keylen++] = find_ascii_code (args[i]);
	    sendkey[keylen++] = key_code;
	  }
      }
  }

  {
    unsigned i;
    for (i = 0; i < sizeof (simple_flag_offsets) 
	   / sizeof (simple_flag_offsets[0]); i++)
      grub_sendkey_set_simple_flag (simple_flag_offsets[i], 
				    grub_sendkey_parse_op(state[i]));
  }

  /* Set noled. */
  noled = (state[sizeof (simple_flag_offsets) 
		 / sizeof (simple_flag_offsets[0])].set);

  return GRUB_ERR_NONE;
}

static grub_extcmd_t cmd;
static void *preboot_hook;

GRUB_MOD_INIT (sendkey)
{
  cmd = grub_register_extcmd ("sendkey", grub_cmd_sendkey, 
			      GRUB_COMMAND_FLAG_BOTH,
			      "sendkey [KEYSTROKE1] [KEYSTROKE2] ...",
			      "Emulate a keystroke", options);

  preboot_hook 
    = grub_loader_register_preboot_hook (grub_sendkey_preboot, 
					 grub_sendkey_postboot,
					 GRUB_LOADER_PREBOOT_HOOK_PRIO_CONSOLE);
}

GRUB_MOD_FINI (sendkey)
{
  grub_unregister_extcmd (cmd);
  grub_loader_unregister_preboot_hook (preboot_hook);
}