// we want strdup()
#define _XOPEN_SOURCE 600

#include "termkey.h"
#include "termkey-internal.h"

#ifdef HAVE_UNIBILIUM
# include <unibilium.h>
#else
# include <curses.h>
# include <term.h>

/* curses.h has just polluted our namespace. We want this back */
# undef buttons
#endif

#include <ctype.h>
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#ifndef _WIN32
# include <unistd.h>
#else
# include <io.h>
#endif
#include <sys/types.h>
#include <sys/stat.h>

#define streq(a,b) (!strcmp(a,b))

#define MAX_FUNCNAME 9

static struct {
  const char *funcname;
  TermKeyType type;
  TermKeySym sym;
  int mods;
} funcs[] =
{
  /* THIS LIST MUST REMAIN SORTED! */
  { "backspace", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_BACKSPACE, 0 },
  { "begin",     TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_BEGIN,     0 },
  { "beg",       TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_BEGIN,     0 },
  { "btab",      TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_TAB,       TERMKEY_KEYMOD_SHIFT },
  { "cancel",    TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_CANCEL,    0 },
  { "clear",     TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_CLEAR,     0 },
  { "close",     TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_CLOSE,     0 },
  { "command",   TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_COMMAND,   0 },
  { "copy",      TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_COPY,      0 },
  { "dc",        TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_DELETE,    0 },
  { "down",      TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_DOWN,      0 },
  { "end",       TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_END,       0 },
  { "enter",     TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_ENTER,     0 },
  { "exit",      TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_EXIT,      0 },
  { "find",      TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_FIND,      0 },
  { "help",      TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_HELP,      0 },
  { "home",      TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_HOME,      0 },
  { "ic",        TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_INSERT,    0 },
  { "left",      TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_LEFT,      0 },
  { "mark",      TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_MARK,      0 },
  { "message",   TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_MESSAGE,   0 },
  { "move",      TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_MOVE,      0 },
  { "next",      TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEDOWN,  0 }, // Not quite, but it's the best we can do
  { "npage",     TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEDOWN,  0 },
  { "open",      TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_OPEN,      0 },
  { "options",   TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_OPTIONS,   0 },
  { "ppage",     TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEUP,    0 },
  { "previous",  TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEUP,    0 }, // Not quite, but it's the best we can do
  { "print",     TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PRINT,     0 },
  { "redo",      TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_REDO,      0 },
  { "reference", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_REFERENCE, 0 },
  { "refresh",   TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_REFRESH,   0 },
  { "replace",   TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_REPLACE,   0 },
  { "restart",   TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_RESTART,   0 },
  { "resume",    TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_RESUME,    0 },
  { "right",     TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_RIGHT,     0 },
  { "save",      TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_SAVE,      0 },
  { "select",    TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_SELECT,    0 },
  { "suspend",   TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_SUSPEND,   0 },
  { "undo",      TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_UNDO,      0 },
  { "up",        TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_UP,        0 },
  { NULL },
};

#ifdef HAVE_UNIBILIUM
static enum unibi_string unibi_lookup_str(const char *name)
{
  for(enum unibi_string ret = unibi_string_begin_+1; ret < unibi_string_end_; ret++)
    if(streq(unibi_name_str(ret), name))
      return ret;

  return -1;
}

static const char *unibi_get_str_by_name(const unibi_term *ut, const char *name)
{
  enum unibi_string idx = unibi_lookup_str(name);
  if(idx == (enum unibi_string)-1)
    return NULL;

  return unibi_get_str(ut, idx);
}
#endif

/* To be efficient at lookups, we store the byte sequence => keyinfo mapping
 * in a trie. This avoids a slow linear search through a flat list of
 * sequences. Because it is likely most nodes will be very sparse, we optimise
 * vector to store an extent map after the database is loaded.
 */

typedef enum {
  TYPE_KEY,
  TYPE_ARR,
} trie_nodetype;

struct trie_node {
  trie_nodetype type;
};

struct trie_node_key {
  trie_nodetype type;
  struct keyinfo key;
};

struct trie_node_arr {
  trie_nodetype type;
  unsigned char min, max; /* INCLUSIVE endpoints of the extent range */
  struct trie_node *arr[]; /* dynamic size at allocation time */
};

typedef struct {
  TermKey *tk;

#ifdef HAVE_UNIBILIUM
  unibi_term *unibi;  /* only valid until first 'start' call */
#else
  char *term; /* only valid until first 'start' call */
#endif

  struct trie_node *root;

  char *start_string;
  char *stop_string;
} TermKeyTI;

static int insert_seq(TermKeyTI *ti, const char *seq, struct trie_node *node);

static struct trie_node *new_node_key(TermKeyType type, TermKeySym sym, int modmask, int modset)
{
  struct trie_node_key *n = malloc(sizeof(*n));
  if(!n)
    return NULL;

  n->type = TYPE_KEY;

  n->key.type = type;
  n->key.sym  = sym;
  n->key.modifier_mask = modmask;
  n->key.modifier_set  = modset;

  return (struct trie_node*)n;
}

static struct trie_node *new_node_arr(unsigned char min, unsigned char max)
{
  struct trie_node_arr *n = malloc(sizeof(*n) + ((int)max-min+1) * sizeof(n->arr[0]));
  if(!n)
    return NULL;

  n->type = TYPE_ARR;
  n->min = min; n->max = max;

  int i;
  for(i = min; i <= max; i++)
    n->arr[i-min] = NULL;

  return (struct trie_node*)n;
}

static struct trie_node *lookup_next(struct trie_node *n, unsigned char b)
{
  switch(n->type) {
  case TYPE_KEY:
    fprintf(stderr, "ABORT: lookup_next within a TYPE_KEY node\n");
    abort();
  case TYPE_ARR:
    {
      struct trie_node_arr *nar = (struct trie_node_arr*)n;
      if(b < nar->min || b > nar->max)
        return NULL;
      return nar->arr[b - nar->min];
    }
  }

  return NULL; // Never reached but keeps compiler happy
}

static void free_trie(struct trie_node *n)
{
  switch(n->type) {
  case TYPE_KEY:
    break;
  case TYPE_ARR:
    {
      struct trie_node_arr *nar = (struct trie_node_arr*)n;
      int i;
      for(i = nar->min; i <= nar->max; i++)
        if(nar->arr[i - nar->min])
          free_trie(nar->arr[i - nar->min]);
      break;
    }
  }

  free(n);
}

static struct trie_node *compress_trie(struct trie_node *n)
{
  if(!n)
    return NULL;

  switch(n->type) {
  case TYPE_KEY:
    return n;
  case TYPE_ARR:
    {
      struct trie_node_arr *nar = (struct trie_node_arr*)n;
      unsigned char min, max;
      // Find the real bounds
      for(min = 0; !nar->arr[min]; min++)
        if(min == 255 && !nar->arr[min]) {
          free(nar);
          return new_node_arr(1, 0);
        }

      for(max = 0xff; !nar->arr[max]; max--)
        ;

      struct trie_node_arr *new = (struct trie_node_arr*)new_node_arr(min, max);
      int i;
      for(i = min; i <= max; i++)
        new->arr[i - min] = compress_trie(nar->arr[i]);

      free(nar);
      return (struct trie_node*)new;
    }
  }

  return n;
}

static bool try_load_terminfo_key(TermKeyTI *ti, const char *name, struct keyinfo *info)
{
  const char *value = NULL;

#ifdef HAVE_UNIBILIUM
  if(ti->unibi)
    value = unibi_get_str_by_name(ti->unibi, name);
#else
  if(ti->term)
    value = tigetstr(name);
#endif

  if(ti->tk->ti_getstr_hook)
    value = (ti->tk->ti_getstr_hook)(name, value, ti->tk->ti_getstr_hook_data);

  if(!value || value == (char*)-1 || !value[0])
    return false;

  struct trie_node *node = new_node_key(info->type, info->sym, info->modifier_mask, info->modifier_set);
  insert_seq(ti, value, node);

  return true;
}

static int load_terminfo(TermKeyTI *ti)
{
  int i;

#ifdef HAVE_UNIBILIUM
  unibi_term *unibi = ti->unibi;
#else
  {
    int err;

    /* Have to cast away the const. But it's OK - we know terminfo won't really
    * modify term */
    if(setupterm((char*)ti->term, 1, &err) != OK)
      return 0;
  }
#endif

  ti->root = new_node_arr(0, 0xff);
  if(!ti->root)
    return 0;

  /* First the regular key strings
   */
  for(i = 0; funcs[i].funcname; i++) {
    char name[MAX_FUNCNAME + 5 + 1];

    sprintf(name, "key_%s", funcs[i].funcname);
    if(!try_load_terminfo_key(ti, name, &(struct keyinfo){
          .type = funcs[i].type,
          .sym  = funcs[i].sym,
          .modifier_mask = funcs[i].mods,
          .modifier_set  = funcs[i].mods,
      }))
      continue;

    /* Maybe it has a shifted version */
    sprintf(name, "key_s%s", funcs[i].funcname);
    try_load_terminfo_key(ti, name, &(struct keyinfo){
        .type = funcs[i].type,
        .sym  = funcs[i].sym,
        .modifier_mask = funcs[i].mods | TERMKEY_KEYMOD_SHIFT,
        .modifier_set  = funcs[i].mods | TERMKEY_KEYMOD_SHIFT,
    });
  }

  /* Now the F<digit> keys
   */
  for(i = 1; i < 255; i++) {
    char name[9];
    sprintf(name, "key_f%d", i);
    if(!try_load_terminfo_key(ti, name, &(struct keyinfo){
          .type = TERMKEY_TYPE_FUNCTION,
          .sym  = i,
          .modifier_mask = 0,
          .modifier_set  = 0,
      }))
      break;
  }

  /* Finally mouse mode */
  {
    const char *value = NULL;

#ifdef HAVE_UNIBILIUM
    if(ti->unibi)
      value = unibi_get_str_by_name(ti->unibi, "key_mouse");
#else
    if(ti->term)
      value = tigetstr("key_mouse");
#endif

    if(ti->tk->ti_getstr_hook)
      value = (ti->tk->ti_getstr_hook)("key_mouse", value, ti->tk->ti_getstr_hook_data);

    /* Some terminfos (e.g. xterm-1006) claim a different key_mouse that won't
     * give X10 encoding. We'll only accept this if it's exactly "\e[M"
     */
    if(value && streq(value, "\x1b[M")) {
      struct trie_node *node = new_node_key(TERMKEY_TYPE_MOUSE, 0, 0, 0);
      insert_seq(ti, value, node);
    }
  }

  /* Take copies of these terminfo strings, in case we build multiple termkey
   * instances for multiple different termtypes, and it's different by the
   * time we want to use it
   */
#ifdef HAVE_UNIBILIUM
  const char *keypad_xmit = unibi ?
    unibi_get_str(unibi, unibi_keypad_xmit) :
    NULL;
#endif

  if(keypad_xmit)
    ti->start_string = strdup(keypad_xmit);
  else
    ti->start_string = NULL;

#ifdef HAVE_UNIBILIUM
  const char *keypad_local = unibi ?
    unibi_get_str(unibi, unibi_keypad_local) :
    NULL;
#endif

  if(keypad_local)
    ti->stop_string = strdup(keypad_local);
  else
    ti->stop_string = NULL;

#ifdef HAVE_UNIBILIUM
  if(unibi)
    unibi_destroy(unibi);

  ti->unibi = NULL;
#else
  if(ti->term)
    free(ti->term);

  ti->term = NULL;
#endif

  ti->root = compress_trie(ti->root);

  return 1;
}

static void *new_driver(TermKey *tk, const char *term)
{
  TermKeyTI *ti = malloc(sizeof *ti);
  if(!ti)
    return NULL;

  ti->tk = tk;
  ti->root = NULL;
  ti->start_string = NULL;
  ti->stop_string = NULL;

#ifdef HAVE_UNIBILIUM
  ti->unibi = unibi_from_term(term);
  int saved_errno = errno;
  if(!ti->unibi && saved_errno != ENOENT) {
    free(ti);
    return NULL;
  }
  /* ti->unibi may be NULL if errno == ENOENT. That means the terminal wasn't
   * known. Lets keep going because if we get getstr hook that might invent
   * new strings for us
   */
#else
  {
    int err;

    ti->term = NULL;

    /* Have to cast away the const. But it's OK - we know terminfo won't really
    * modify term */
    if(setupterm((char*)term, 1, &err) == OK)
      ti->term = strdup(term);
  }
#endif

  return ti;
}

static int start_driver(TermKey *tk, void *info)
{
  TermKeyTI *ti = info;
  struct stat statbuf;
  char *start_string;
  size_t len;

  if(!ti->root)
    load_terminfo(ti);

  start_string = ti->start_string;

  if(tk->fd == -1 || !start_string)
    return 1;

  /* The terminfo database will contain keys in application cursor key mode.
   * We may need to enable that mode
   */

  /* There's no point trying to write() to a pipe */
  if(fstat(tk->fd, &statbuf) == -1)
    return 0;

#ifndef _WIN32
  if(S_ISFIFO(statbuf.st_mode))
    return 1;
#endif

  // Can't call putp or tputs because they suck and don't give us fd control
  len = strlen(start_string);
  while(len) {
    size_t written = write(tk->fd, start_string, len);
    if(written == -1)
      return 0;
    start_string += written;
    len -= written;
  }
  return 1;
}

static int stop_driver(TermKey *tk, void *info)
{
  TermKeyTI *ti = info;
  struct stat statbuf;
  char *stop_string = ti->stop_string;
  size_t len;

  if(tk->fd == -1 || !stop_string)
    return 1;

  /* There's no point trying to write() to a pipe */
  if(fstat(tk->fd, &statbuf) == -1)
    return 0;

#ifndef _WIN32
  if(S_ISFIFO(statbuf.st_mode))
    return 1;
#endif

  /* The terminfo database will contain keys in application cursor key mode.
   * We may need to enable that mode
   */

  // Can't call putp or tputs because they suck and don't give us fd control
  len = strlen(stop_string);
  while(len) {
    size_t written = write(tk->fd, stop_string, len);
    if(written == -1)
      return 0;
    stop_string += written;
    len -= written;
  }
  return 1;
}

static void free_driver(void *info)
{
  TermKeyTI *ti = info;

  free_trie(ti->root);

  if(ti->start_string)
    free(ti->start_string);

  if(ti->stop_string)
    free(ti->stop_string);

#ifdef HAVE_UNIBILIUM
  if(ti->unibi)
    unibi_destroy(ti->unibi);
#else
  if(ti->term)
    free(ti->term);
#endif

  free(ti);
}

#define CHARAT(i) (tk->buffer[tk->buffstart + (i)])

static TermKeyResult peekkey(TermKey *tk, void *info, TermKeyKey *key, int force, size_t *nbytep)
{
  TermKeyTI *ti = info;

  if(tk->buffcount == 0)
    return tk->is_closed ? TERMKEY_RES_EOF : TERMKEY_RES_NONE;

  struct trie_node *p = ti->root;

  unsigned int pos = 0;
  while(pos < tk->buffcount) {
    p = lookup_next(p, CHARAT(pos));
    if(!p)
      break;

    pos++;

    if(p->type != TYPE_KEY)
      continue;

    struct trie_node_key *nk = (struct trie_node_key*)p;
    if(nk->key.type == TERMKEY_TYPE_MOUSE) {
      tk->buffstart += pos;
      tk->buffcount -= pos;

      TermKeyResult mouse_result = (*tk->method.peekkey_mouse)(tk, key, nbytep);

      tk->buffstart -= pos;
      tk->buffcount += pos;

      if(mouse_result == TERMKEY_RES_KEY)
        *nbytep += pos;

      return mouse_result;
    }

    key->type      = nk->key.type;
    key->code.sym  = nk->key.sym;
    key->modifiers = nk->key.modifier_set;
    *nbytep = pos;
    return TERMKEY_RES_KEY;
  }

  // If p is not NULL then we hadn't walked off the end yet, so we have a
  // partial match
  if(p && !force)
    return TERMKEY_RES_AGAIN;

  return TERMKEY_RES_NONE;
}

static int insert_seq(TermKeyTI *ti, const char *seq, struct trie_node *node)
{
  int pos = 0;
  struct trie_node *p = ti->root;

  // Unsigned because we'll be using it as an array subscript
  unsigned char b;

  while((b = seq[pos])) {
    struct trie_node *next = lookup_next(p, b);
    if(!next)
      break;
    p = next;
    pos++;
  }

  while((b = seq[pos])) {
    struct trie_node *next;
    if(seq[pos+1])
      // Intermediate node
      next = new_node_arr(0, 0xff);
    else
      // Final key node
      next = node;

    if(!next)
      return 0;

    switch(p->type) {
    case TYPE_ARR:
      {
        struct trie_node_arr *nar = (struct trie_node_arr*)p;
        if(b < nar->min || b > nar->max) {
          fprintf(stderr, "ASSERT FAIL: Trie insert at 0x%02x is outside of extent bounds (0x%02x..0x%02x)\n",
              b, nar->min, nar->max);
          abort();
        }
        nar->arr[b - nar->min] = next;
        p = next;
        break;
      }
    case TYPE_KEY:
      fprintf(stderr, "ASSERT FAIL: Tried to insert child node in TYPE_KEY\n");
      abort();
    }

    pos++;
  }

  return 1;
}

struct TermKeyDriver termkey_driver_ti = {
  .name        = "terminfo",

  .new_driver  = new_driver,
  .free_driver = free_driver,

  .start_driver = start_driver,
  .stop_driver  = stop_driver,

  .peekkey = peekkey,
};
