#include <limits.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <assert.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "term.h"
#include "myc_mem.h"
#include "myc_str.h"
#include "sys_select.h"
#include "uselect.h"

#define ESC_TMO 1000
#define REF_TMO 50
#define HISTORY_LINES 256

#define L_ARROW 0200
#define R_ARROW 0201
#define U_ARROW 0202
#define D_ARROW 0203
#define EVT_CHAR '\0'
#define DEL_CHAR '\b'
#define RET_CHAR '\n'

static char esc_key='\0';

static fd_set regMask;
static int minReg=0, maxReg=0;
static char filehead[256]="";

static char *func[256]={NULL};

void term_reg_socket(int socket) {
  FD_SET(socket, &regMask);
  if (socket > maxReg) {
    if (maxReg == 0) minReg = socket;
    maxReg = socket;
  } else if (socket < minReg) {
    minReg = socket;
  }
}

void term_unr_socket(int socket) {
  FD_CLR(socket, &regMask);
}

int term_raw_key(int *key, int msecTmo) {
  fd_set mask;

  mask=regMask;
  return(sys_select_or_key(&mask, msecTmo, key));
}

int term_wait_socket(int socket, int msecTmo) {
  fd_set mask;
  struct timeval tmo;
  int i;

  if (socket == 0) {
    mask = regMask;
  } else {
    FD_ZERO(&mask);
    FD_SET(socket, &mask);
  }
  FD_SET(STDIN_FILENO, &mask);
  tmo.tv_sec=msecTmo / 1000;
  tmo.tv_usec=(msecTmo % 1000) * 1000;
  i=uselect(FD_SETSIZE, &mask, NULL, NULL, &tmo);
  if (FD_ISSET(STDIN_FILENO, &mask)) {
    return 0;
  }
  if (i<0) return i;
  if (i) {
    if (socket && FD_ISSET(socket, &mask)) {
      return socket;
    }
    for (i=minReg; i<=maxReg; i++) {
      if (FD_ISSET(i, &mask)) {
        return i;
      };
    }
    return -1;
  }
  return 0;
}

int term_get_key(int *key, int msecTmo) {
  int iret;
  int k;
  int kode;

  fflush(stdout);
  if (esc_key==0) {
    iret=term_raw_key(&k, msecTmo);
  } else {
    k=esc_key;
  }
  while (k==27) { /* esc */
    iret=term_raw_key(&k, ESC_TMO);
    switch (k) {
    case 'O': k=0217; break; /* ss3 */
    case '[': k=0233; break; /* csi */
    default: break;
    }
  }      
  if (iret!=STDIN_FILENO) {
    esc_key=k;
    *key='\0';
    return(iret);
  }
  switch (k) {
  case 0233: /* csi */
    iret=term_raw_key(&k, ESC_TMO);
    kode=0;
    while (k>='0' && k <='9') {
      kode=kode*10+(k-'0');
      iret=term_raw_key(&k, ESC_TMO);
    }
    if (iret!=STDIN_FILENO) {
      esc_key=k;
      *key='\0';
      return(iret);
    }
    switch (k) { /* L,R,U,D arrows */
    case 'D': k=L_ARROW; break;
    case 'C': k=R_ARROW; break;
    case 'A': k=U_ARROW; break;
    case 'B': k=D_ARROW; break;
    default:
      if (k=='~') {
        k = 128 + kode;
      } else {
        k += 128;
      }
      if (k>255) k='?';
    }
    break;
  case 0217: /* ss3 */
    iret=term_raw_key(&k, ESC_TMO);
    if (iret!=STDIN_FILENO) {
      esc_key=k;
      *key='\0';
      return(iret);
    }
    if (k=='M') { /* enter */
      k=RET_CHAR;
    } else if (k >= 'l' && k <= 'y') {
      k=k-64;
    } else {
      switch (k) { /* L,R,U,D arrows */
      case 'D': k=L_ARROW; break;
      case 'C': k=R_ARROW; break;
      case 'A': k=U_ARROW; break;
      case 'B': k=D_ARROW; break;
      default: k += 128; if (k>255) k='?';
      }
    }
    break;
  case 0177: /* del */
  case '\b': /* bs */
    k=DEL_CHAR;
    break;
  case '\r': /* cr */
  case '\n': /* lf */
    k=RET_CHAR;
    break;
  case EVT_CHAR: /* time out*/
    break;
  default:
    if (k>0176) {
      k='?';
    }
  }
  *key=k;
  esc_key='\0';
  return (STDIN_FILENO);
}

static char *history[HISTORY_LINES]={NULL};
/* history array: it's a cyclic buffer, when it is full, the oldest values
   are overwritten.
*/

static int hist_pos=0; /* position when scrolling through the history */
static int hist_end=0; /* end of history. Always history[hist_end]==NULL */

FILE *term_open_pref(int temp, char *head, char *mode) {
  char buf[PATH_MAX+1], old[PATH_MAX+1], wd[PATH_MAX];
  char *usr, *home, *p;
  int l;
  FILE *fil;
  mode_t oldmask;

  usr = getenv("USER");
  if (temp && usr==NULL) return NULL;
  if (usr != NULL && *usr != '\0' && strstr(usr, "lnsg") != 0) {
    if (! getcwd(wd, sizeof wd)) return NULL;
    home=getenv("HOME");
    if (!home) return NULL;
    realpath(home, buf);
    l = strlen(buf);
    buf[l] = '/'; l++;
    buf[l]= '\0';
    if (wd[l] != '\0' && strncmp(wd, buf, l) == 0) { /* wd starts with $HOME */
      p = strchr(wd+l, '/');
      if (p) *p='\0'; /* determine 1st subdirectory of $HOME */
      if (temp) {
        str_append(wd, "_lnsg");
        usr = wd+l;
      } else {
        home = wd;
      }
    } else {
      home = buf;
    }
  } else {
    home=getenv("HOME");
    if (!home) return NULL;
  }
  if (temp) {
    /* old file name */
    str_copy(old, "/tmp/");
    str_append(old, head);
    str_append(old, ".");
    str_append(old, usr);
    /* new file name */
    str_copy(buf, "/tmp/six/");
    oldmask = umask(0);
    mkdir(buf, S_IRWXU+S_IRWXG+S_IRWXO);
    umask(oldmask);
    str_append(buf, head);
    str_append(buf, ".");
    str_append(buf, usr); /* usr is the username, or lnsg_<subdirectory> */
  } else {
    /* old file name */
    str_copy(old, home);
    str_append(old, "/.");
    str_append(old, head);
    /* new file name */
    str_copy(buf, home);
    str_append(buf, "/.six/");
    mkdir(buf, S_IRWXU+S_IRGRP+S_IXGRP+S_IROTH+S_IXOTH);
    str_append(buf, head);
  }
  rename(old, buf); /* if old file exists, rename it to new */
  if (*mode == 'd') {
    unlink(buf);
    return NULL;
  } else {
    fil = fopen(buf, mode);
    return fil;
  }
}

char *term_fgets(char *buf, int size, FILE *fil) {
  char *p, *ret;
  char skipbuf[256];

  buf[0]='\0';
  ret=fgets(buf, size, fil);
  if (ret==NULL) {
    return NULL;
  }
  p=strchr(buf,'\n');
  if (p==NULL) {
    while (p==NULL) { /* skip rest of line */
      ret=fgets(skipbuf, sizeof(skipbuf), fil);
      if (ret==NULL) {
        return NULL;
      }
      p=strchr(skipbuf,'\n');
    }
  } else {
    *p='\0';
  }
  return ret;
}
  
void term_save_hist(int trimlast) {
  FILE *fil;
  int i,n;
  if (filehead[0]=='\0') return;
  fil=term_open_pref(1, filehead, "w");
  if (fil==NULL) return;
  n=HISTORY_LINES-1;
  if (trimlast) {
    n--;
  }
  i=hist_end;
  while (n>0) {
    n--;
    i++; if (i>=HISTORY_LINES) i=0;
    if (history[i]!=NULL) {
      fputs(history[i], fil); fputs("\n", fil);
    }
  }
  fclose(fil);
}

void term_read_hist(char *id, char *instr) {
  FILE *fil;
  int i;
  char buf[1024], *lin;

  str_copy(filehead, id);
  str_append(filehead, "_hist_");
  str_append(filehead, instr);
  fil=term_open_pref(1, filehead, "r");
  if (fil==NULL) return;
  hist_end=0;
  while (hist_end<HISTORY_LINES-1 && !feof(fil)) {
    term_fgets(buf, sizeof(buf), fil);
    i=strlen(buf);
    if (i>0) {
      lin=MALLOC(i+1);
      strncpy(lin, buf, i+1);
      history[hist_end]=lin;
      hist_end++;
    }
  }
  fclose(fil);
  history[hist_end]=NULL;
  hist_pos=hist_end;
}

static int dirty=0; /* line is to be cleared through a call of term_clear */

void term_clear(void) {
  if (dirty) {
    fputs("\r\033[K\033[0m", stdout); /* clear to end of line, clear colors */
    dirty=0;
  }
}

void term_off(void) {
  dirty=1;
  term_clear();
  sys_keys_off();
}

int term_get_line(char *buf, int size, int *pos, char *prompt, fd_set *mask) {
  char *lin;
  int key;
  int i,l,iret,buflen;
  char tmp[1024];
  static char back[512]="";

  if (back[0] == '\0') {
    memset(back, '\b', sizeof back);
  }
  buf[size-1]='\0'; /* make sure buf is null terminated */
  l=strlen(buf);
  if (*pos>l) {
    *pos=l;
  } else if (*pos<0) {
    *pos=0;
  }
  iret=term_get_key(&key, 0);
  while (1) {
    if (iret==-1 || key == RET_CHAR || key==EVT_CHAR) { /* refresh after a short timeout */
      snprintf(tmp, sizeof tmp, "%s%s%s%s%s%.*s",
        "\r\033[0m\033[1m",  /* no color, bold */
        prompt, "\033[34m", /* blue */
        buf, "\033[K\033[0m", /* clear to end of line, clear colors */
        l - *pos, back);
      fputs(tmp, stdout);
      if (iret==-1) {
        iret=term_get_key(&key, -1); /* no timeout */
      }
    }
    switch (key) {
    case EVT_CHAR:
      /* caller must clear line before next write with term_clear() */
      dirty=1;
      return(iret);

      /* interrupted EXIT */

    case RET_CHAR:
      i=hist_end-1; if (i<0) i=HISTORY_LINES-1;
      if ((history[i]==NULL || 0!=strcmp(history[i], buf)) && buf[0]!='\0') { /* do not save equal and empty lines */
        buflen=strlen(buf)+1;
	lin=MALLOC(buflen);
        strncpy(lin, buf, buflen);
        history[hist_end]=lin;
        hist_end++; if (hist_end>=HISTORY_LINES) hist_end=0;
        if (history[hist_end]!=NULL) {
          FREE(history[hist_end]); /* clear line at end of history */
        }
        history[hist_end] = NULL;
      }
      hist_pos=hist_end;
      term_save_hist(0);
      return(STDIN_FILENO);

      /* normal EXIT */

    case DEL_CHAR:
      if (*pos>0) {
        for (i=*pos; buf[i]!='\0'; i++) {
          buf[i-1] = buf [i];
        }
        buf[i-1]='\0';
        (*pos)--;
        l--;
      }
      break;
    case L_ARROW:
      if (*pos>0) (*pos)--;
      break;
    case R_ARROW:
      if (buf[*pos]!='\0') (*pos)++;
      break;
    case U_ARROW:
    case D_ARROW:
      if (key==U_ARROW) {
        i=hist_pos-1; if (i<0) i=HISTORY_LINES-1;
      } else {
        i=hist_pos+1; if (i>=HISTORY_LINES) i=0;
      }
      if (history[i]!=NULL) {
        strncpy(buf, history[i], size-1);
        buf[size-1]='\0';
        hist_pos=i;
        l=strlen(buf);
      } else {
        buf[0]='\0';
        l=0;  
        if (history[hist_pos]!=NULL) {
          hist_pos=i;
        }
      }
      *pos=l;
      break;
    case 1: /* ctrl-A: jump to begin of line */
      *pos = 0;
      break;
    case 5: /* ctrl-E: jump to end of line */
      *pos = strlen(buf);
      break;
    default:
      if (key <' ' || key > 0176) { /* untreated special key */
        if (func[key] != NULL) {
          snprintf(buf, size, "%s", func[key]);
          key = RET_CHAR;
          continue;
        }
        key = '?';
      }
      if (l<size-1) {
        for (i=l; i>*pos; i--) {
          buf[i]=buf[i-1];
        }
        (*pos)++; l++;
        buf[i]=key;
        buf[l]='\0';
      }
      break;
    }
    iret=term_get_key(&key, REF_TMO);
  }
}

void term_define_key(char *cmd, int keyArg) {
  int iret;
  int key;
  
  if (keyArg) {
    key = keyArg;
  } else {
    if (*cmd) {
      printf("\n# press key for command '%s' or return", cmd);
    } else {
      printf("\n# press key for which to remove command");
    }
    iret = term_get_key(&key, 5000);
  }
  switch (key) {
  case EVT_CHAR:
  case DEL_CHAR:
  case L_ARROW:
  case R_ARROW:
  case U_ARROW:
  case D_ARROW:
    break;
  case RET_CHAR:
    printf("\n# canceled\n");
  default:
    if (key <' ' || key > 0176) { /* special key */
      if (*cmd) {
        if (func[key]) free(func[key]);
        func[key] = strdup(cmd);
        if (!keyArg) {
          printf("\n# programmed key %d\n", key);
        }
      } else {
        if (func[key]) {
          if (!keyArg) {
            printf("\n# removed command '%s' from key %d\n", func[key], key);
          }
          free(func[key]);
        } else {
          if (!keyArg) {
            printf("\n# no command on key %d\n", key);
          }
        }
        func[key] = NULL;
      }
      return;
    }
  }
  printf("\n# key %d is not programmable\n", key);
}

void term_save_keys(FILE *fil, void (*out)(char *)) {
  int key;
  char buf[128];
  
  for (key=0; key<256; key++) {
    if (func[key]) {
      fprintf(fil, "%d %s\n", key, func[key]);
      if (out) {
        snprintf(buf, sizeof buf, "  key %d defined as '%s'\n", key, func[key]);
        out(buf);
      }
    }
  }
}

void term_load_keys(FILE *fil, void (*out)(char *)) {
  char line[136], buf[128];
  int key, l;
  
  while (term_fgets(line, sizeof line, fil)) {
    key = 0;
    sscanf(line, "%d %n", &key, &l);
    if (key != 0) {
      term_define_key(line+l, key);
      if (out) {
        snprintf(buf, sizeof buf, "  key %d defined as '%s'\n", key, line+l);
        out(buf);
      }
    }
  }
}
