/*
 * Key parser
 *
 * Copyright (C) 2009 Jorge Arellano Cid <jcid@dillo.org>
 * Copyright (C) 2024 Rodrigo Arias Mallo <rodarima@gmail.com>
 *
 * 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 3 of the License, or
 * (at your option) any later version.
 */

#include <FL/Fl.H>
#include <stdio.h>
#include <stdlib.h>        /* strtol */
#include <string.h>
#include <ctype.h>

#include "dlib/dlib.h"
#include "keys.hh"
#include "utf8.hh"
#include "msg.h"

/*
 *  Local data types
 */
typedef struct {
   const char *name;
   KeysCommand_t cmd;
   int modifier, key;
} KeyBinding_t;

typedef struct {
   const char *name;
   const int value;
} Mapping_t;


/*
 *  Local data
 */
static const Mapping_t keyNames[] = {
   { "Backspace",   FL_BackSpace },
   { "Delete",      FL_Delete    },
   { "Down",        FL_Down      },
   { "End",         FL_End       },
   { "Esc",         FL_Escape    },
   { "F1",          FL_F + 1     },
   { "F2",          FL_F + 2     },
   { "F3",          FL_F + 3     },
   { "F4",          FL_F + 4     },
   { "F5",          FL_F + 5     },
   { "F6",          FL_F + 6     },
   { "F7",          FL_F + 7     },
   { "F8",          FL_F + 8     },
   { "F9",          FL_F + 9     },
   { "F10",         FL_F + 10    },
   { "F11",         FL_F + 11    },
   { "F12",         FL_F + 12    },
   { "Home",        FL_Home      },
   { "Insert",      FL_Insert    },
   { "Left",        FL_Left      },
   { "Menu",        FL_Menu      },
   { "PageDown",    FL_Page_Down },
   { "PageUp",      FL_Page_Up   },
   { "Print",       FL_Print     },
   { "Return",      FL_Enter     },
   { "Right",       FL_Right     },
   { "Space",       ' '          },
   { "Tab",         FL_Tab       },
   { "Up",          FL_Up        },
   /* multimedia keys */
   { "Back",        FL_Back        },
   { "Favorites",   FL_Favorites   },
   { "Forward",     FL_Forward     },
   { "HomePage",    FL_Home_Page   },
   { "Mail",        FL_Mail        },
   { "MediaNext",   FL_Media_Next  },
   { "MediaPlay",   FL_Media_Play  },
   { "MediaPrev",   FL_Media_Prev  },
   { "MediaStop",   FL_Media_Stop  },
   { "Refresh",     FL_Refresh     },
   { "Search",      FL_Search      },
   { "Sleep",       FL_Sleep       },
   { "Stop",        FL_Stop        },
   { "VolumeDown",  FL_Volume_Down },
   { "VolumeMute",  FL_Volume_Mute },
   { "VolumeUp",    FL_Volume_Up   },
};

static const Mapping_t modifierNames[] = {
   { "Shift",   FL_SHIFT   },
   { "Ctrl",    FL_CTRL    },
   { "Alt",     FL_ALT     },
   { "Meta",    FL_META    },
   { "Button1", FL_BUTTON1 },
   { "Button2", FL_BUTTON2 },
   { "Button3", FL_BUTTON3 }
};

static const KeyBinding_t default_keys[] = {
   { "nop"          , KEYS_NOP          , 0         , 0               },
   { "open"         , KEYS_OPEN         , FL_CTRL   , 'o'             },
   { "new-window"   , KEYS_NEW_WINDOW   , FL_CTRL   , 'n'             },
   { "new-tab"      , KEYS_NEW_TAB      , FL_CTRL   , 't'             },
   { "left-tab"     , KEYS_LEFT_TAB     , FL_CTRL |
                                          FL_SHIFT  , FL_Tab          },
   { "left-tab"     , KEYS_LEFT_TAB     , FL_CTRL   , FL_Page_Up      },
   { "right-tab"    , KEYS_RIGHT_TAB    , FL_CTRL   , FL_Tab          },
   { "right-tab"    , KEYS_RIGHT_TAB    , FL_CTRL   , FL_Page_Down    },
   { "close-tab"    , KEYS_CLOSE_TAB    , FL_CTRL   , 'w'             },
   { "find"         , KEYS_FIND         , FL_CTRL   , 'f'             },
   { "websearch"    , KEYS_WEBSEARCH    , FL_CTRL   , 's'             },
   { "bookmarks"    , KEYS_BOOKMARKS    , FL_CTRL   , 'b'             },
   { "reload"       , KEYS_RELOAD       , FL_CTRL   , 'r'             },
   { "stop"         , KEYS_STOP         , 0         , 0               },
   { "save"         , KEYS_SAVE         , 0         , 0               },
   { "hide-panels"  , KEYS_HIDE_PANELS  , 0         , FL_Escape       },
   { "file-menu"    , KEYS_FILE_MENU    , FL_ALT    , 'f'             },
   { "close-all"    , KEYS_CLOSE_ALL    , FL_CTRL   , 'q'             },
   { "back"         , KEYS_BACK         , 0         , FL_BackSpace    },
   { "back"         , KEYS_BACK         , 0         , ','             },
   { "forward"      , KEYS_FORWARD      , FL_SHIFT  , FL_BackSpace    },
   { "forward"      , KEYS_FORWARD      , 0         , '.'             },
   { "goto"         , KEYS_GOTO         , FL_CTRL   , 'l'             },
   { "home"         , KEYS_HOME         , FL_CTRL   , 'h'             },
   { "view-source"  , KEYS_VIEW_SOURCE  , FL_CTRL   , 'u'             },
   { "screen-up"    , KEYS_SCREEN_UP    , 0         , FL_Page_Up      },
   { "screen-up"    , KEYS_SCREEN_UP    , 0         , 'b'             },
   { "screen-down"  , KEYS_SCREEN_DOWN  , 0         , FL_Page_Down    },
   { "screen-down"  , KEYS_SCREEN_DOWN  , 0         , ' '             },
   { "screen-left"  , KEYS_SCREEN_LEFT  , 0         , 0               },
   { "screen-right" , KEYS_SCREEN_RIGHT , 0         , 0               },
   { "line-up"      , KEYS_LINE_UP      , 0         , FL_Up           },
   { "line-down"    , KEYS_LINE_DOWN    , 0         , FL_Down         },
   { "left"         , KEYS_LEFT         , 0         , FL_Left         },
   { "right"        , KEYS_RIGHT        , 0         , FL_Right        },
   { "top"          , KEYS_TOP          , 0         , FL_Home         },
   { "bottom"       , KEYS_BOTTOM       , 0         , FL_End          },
   { "zoom-in"      , KEYS_ZOOM_IN      , FL_CTRL   , '+'             },
   { "zoom-in"      , KEYS_ZOOM_IN      , FL_CTRL   , '=' /* US + */  },
   { "zoom-out"     , KEYS_ZOOM_OUT     , FL_CTRL   , '-'             },
   { "zoom-reset"   , KEYS_ZOOM_RESET   , FL_CTRL   , '0'             },
};

static Dlist *bindings;



/**
 * Initialize the bindings list
 */
void Keys::init()
{
   KeyBinding_t *node;

   // Fill our key bindings list
   bindings = dList_new(32);
   for (uint_t i = 0; i < sizeof(default_keys) / sizeof(default_keys[0]); i++) {
      if (default_keys[i].key) {
         node = dNew(KeyBinding_t, 1);
         node->name = dStrdup(default_keys[i].name);
         node->cmd = default_keys[i].cmd;
         node->modifier = default_keys[i].modifier;
         node->key = default_keys[i].key;
         dList_insert_sorted(bindings, node, nodeByKeyCmp);
      }
   }
}

/**
 * Free data
 */
void Keys::free()
{
   KeyBinding_t *node;

   while ((node = (KeyBinding_t*)dList_nth_data(bindings, 0))) {
      dFree((char*)node->name);
      dList_remove_fast(bindings, node);
      dFree(node);
   }
   dList_free(bindings);
}

/**
 * Compare function by {key,modifier} pairs.
 */
int Keys::nodeByKeyCmp(const void *node, const void *key)
{
   KeyBinding_t *n = (KeyBinding_t*)node, *k = (KeyBinding_t*)key;
   _MSG("Keys::nodeByKeyCmp modifier=%d\n", k->modifier);
   return (n->key != k->key) ? (n->key - k->key) : (n->modifier - k->modifier);
}

/**
 * Look if the just pressed key is bound to a command.
 * Return value: The command if found, KEYS_NOP otherwise.
 */
KeysCommand_t Keys::getKeyCmd()
{
   KeysCommand_t ret = KEYS_NOP;
   KeyBinding_t keyNode;

   keyNode.modifier = Fl::event_state() & (FL_SHIFT | FL_CTRL |FL_ALT|FL_META);
   if (iscntrl(Fl::event_text()[0])) {
      keyNode.key = Fl::event_key();
   } else {
      const char *beyond = Fl::event_text() + Fl::event_length();
      keyNode.key = a_Utf8_decode(Fl::event_text(), beyond, NULL);

      /* BUG: The idea is to drop the modifiers if their use results in a
       * different character (e.g., if shift-8 gives '*', drop the shift,
       * but if ctrl-6 gives '6', keep the ctrl), but we have to compare a
       * keysym with a Unicode codepoint, which only works for characters
       * below U+0100 (those known to latin-1).
       */
      if (keyNode.key != Fl::event_key())
         keyNode.modifier = 0;
   }
   _MSG("getKeyCmd: evkey=0x%x evtext=\'%s\' key=0x%x, mod=0x%x\n",
        Fl::event_key(), Fl::event_text(), keyNode.key, keyNode.modifier);
   void *data = dList_find_sorted(bindings, &keyNode, nodeByKeyCmp);
   if (data)
      ret = ((KeyBinding_t*)data)->cmd;
   return ret;
}

/**
 * Remove a key binding from the table.
 */
void Keys::delKeyCmd(int key, int mod)
{
   KeyBinding_t keyNode, *node;
   keyNode.key = key;
   keyNode.modifier = mod;

   node = (KeyBinding_t*) dList_find_sorted(bindings, &keyNode, nodeByKeyCmp);
   if (node) {
      dList_remove(bindings, node);
      dFree((char*)node->name);
      dFree(node);
   }
}

/**
 * Takes a key name and looks it up in the mapping table. If
 * found, its key code is returned. Otherwise -1 is given
 * back.
 */
int Keys::getKeyCode(char *keyName)
{
   uint_t i;
   for (i = 0; i < sizeof(keyNames) / sizeof(keyNames[0]); i++) {
      if (!dStrAsciiCasecmp(keyNames[i].name, keyName)) {
         return keyNames[i].value;
      }
   }

   return -1;
}

/**
 * Takes a command name and searches it in the mapping table.
 * Return value: command code if found, -1 otherwise
 */
KeysCommand_t Keys::getCmdCode(const char *commandName)
{
   uint_t i;

   for (i = 0; i < sizeof(default_keys) / sizeof(KeyBinding_t); i++) {
      if (!dStrAsciiCasecmp(default_keys[i].name, commandName))
         return default_keys[i].cmd;
   }
   return KEYS_INVALID;
}

/**
 * Takes a modifier name and looks it up in the mapping table. If
 * found, its key code is returned. Otherwise -1 is given back.
 */
int Keys::getModifier(char *modifierName)
{
   uint_t i;
   for (i = 0; i < sizeof(modifierNames) / sizeof(modifierNames[0]); i++) {
      if (!dStrAsciiCasecmp(modifierNames[i].name, modifierName)) {
         return modifierNames[i].value;
      }
   }

   return -1;
}

/**
 * Given a keys command, return a shortcut for it, or 0 if there is none
 * (e.g., for KEYS_NEW_WINDOW, return CTRL+'n').
 */
int Keys::getShortcut(KeysCommand_t cmd)
{
   int len = dList_length(bindings);

   for (int i = 0; i < len; i++) {
      KeyBinding_t *node = (KeyBinding_t*)dList_nth_data(bindings, i);
      if (cmd == node->cmd)
         return node->modifier + node->key;
   }
   return 0;
}

/**
 * Parse a key-combination/command-name pair, and
 * insert it into the bindings list.
 */
void Keys::parseKey(char *key, char *commandName)
{
   char *p, *modstr, *keystr;
   KeysCommand_t symcode;
   int st, keymod = 0, keycode = 0;

   _MSG("Keys::parseKey key='%s' commandName='%s'\n", key, commandName);

   // Get command code
   if ((symcode = getCmdCode(commandName)) == KEYS_INVALID) {
      MSG("Keys::parseKey: Invalid command name: '%s'\n", commandName);
      return;
   }

   // Skip space
   for (  ; isspace(*key); ++key) ;
   // Get modifiers
   while(*key == '<' && (p = strchr(key, '>'))) {
      ++key;
      modstr = dStrndup(key, p - key);
      if ((st = getModifier(modstr)) == -1) {
         MSG("Keys::parseKey unknown modifier: %s\n", modstr);
      } else {
         keymod |= st;
      }
      dFree(modstr);
      key = p + 1;
   }
   // Allow trailing space after keyname
   keystr = (*key && (p = strchr(key + 1, ' '))) ? dStrndup(key, p - key - 1) :
                                                   dStrdup(key);
   // Get key code
   if (!key[1]) {
      keycode = *key;
   } else if (a_Utf8_char_count(keystr, strlen(keystr)) == 1) {
      const char *beyond = keystr + strlen(keystr);
      keycode = a_Utf8_decode(keystr, beyond, NULL);
   } else if (key[0] == '0' && key[1] == 'x') {
      /* keysym */
      keycode = strtol(key, NULL, 0x10);
   } else if ((st = getKeyCode(keystr)) == -1) {
      MSG("Keys::parseKey unknown keyname: %s\n", keystr);
   } else {
      keycode = st;
   }
   dFree(keystr);

   // Set binding
   if (keycode) {
      delKeyCmd(keycode, keymod);
      if (symcode != KEYS_NOP) {
         KeyBinding_t *node = dNew(KeyBinding_t, 1);
         node->name = dStrdup(commandName);
         node->cmd = symcode;
         node->modifier = keymod;
         node->key = keycode;
         dList_insert_sorted(bindings, node, nodeByKeyCmp);
         _MSG("parseKey: Adding key=%d, mod=%d\n", node->key, node->modifier);
      }
   }
}

/**
 * Parse the keysrc.
 */
void Keys::parse(FILE *fp)
{
   char *line, *keycomb, *command;
   int st, lineno = 1;

   // scan the file line by line
   while ((line = dGetline(fp)) != NULL) {
      st = dParser_parse_rc_line(&line, &keycomb, &command);

      if (st == 0) {
         _MSG("Keys::parse: keycomb=%s, command=%s\n", keycomb, command);
         parseKey(keycomb, command);
      } else if (st < 0) {
         MSG("Keys::parse: Syntax error in keysrc line %d: "
             "keycomb=\"%s\" command=\"%s\"\n", lineno, keycomb, command);
      }

      dFree(line);
      ++lineno;
   }
   fclose(fp);
}
