#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#ifdef USE_CURSES
#include <curses.h>
#else
#ifndef WIN32
#include <termios.h>
#else
#include <windows.h>
#include <conio.h>
#endif
#endif

#ifdef USE_NLLIBC
#include <nllibc.h>
#endif

#include <nlline.h>
#include "history.h"

rl_completion_func_t *rl_attempted_completion_function = NULL;

#ifdef USE_CURSES
static int termios_init()
{
  filter();
  initscr();
  noecho();
  cbreak();
  clear();
  return 0;
}

static int termios_done()
{
  refresh();
  nocbreak();
  echo();
  endwin();
  return 0;
}
#else
#ifndef WIN32
static struct termios save;

static int termios_init()
{
  struct termios sets;

  tcgetattr(0, &sets);

  save = sets;
  sets.c_lflag &= ~ECHO;
  sets.c_lflag &= ~ICANON;
  sets.c_cc[VTIME] = 0;
  sets.c_cc[VMIN]  = 1;
  tcsetattr(0, TCSANOW, &sets);

  return 0;
}

static int termios_done()
{
  tcsetattr(0, TCSANOW, &save);
  return 0;
}
#else
static DWORD save;

static int termios_init()
{
  HANDLE handle = (HANDLE)_get_osfhandle(STDOUT_FILENO);
  DWORD mode = 0;

  if (!GetConsoleMode(handle, &mode))
    return -1;

  save = mode;

#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
#endif

  if (!SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING))
    return -1;

  return 0;
}

static int termios_done()
{
  HANDLE handle = (HANDLE)_get_osfhandle(STDOUT_FILENO);

  if (!SetConsoleMode(handle, save))
    return -1;

  return 0;
}
#endif
#endif

static int init()
{
  termios_init();
  return 0;
}

static int done()
{
  termios_done();
  return 0;
}

typedef enum {
  ESCAPE_NONE   = 0,
  ESCAPE_START  = 1,
  ESCAPE_CURSOR = 2,
} escape_t;

static char *expand_buffer(char *line, int *line_size_p, int need_size, int used_size)
{
  int new_line_size = *line_size_p;
  char *p;

  while (need_size > new_line_size) {
    if (new_line_size == 0)
      new_line_size = 1024;
    else
      new_line_size *= 2;
  }

  if (new_line_size > *line_size_p) {
    p = malloc(new_line_size);
    if (p == NULL) {
      if (line != NULL)
	free(line);
      return NULL;
    }
    if (line != NULL) {
      memcpy(p, line, used_size);
      free(line);
    } else {
      memset(p, 0, new_line_size);
    }
    *line_size_p = new_line_size;
    line = p;
  }

  return line;
}

#ifndef USE_CURSES
static void outesc(int fd, char *start, int num, char *end)
{
  char esc[32];
  sprintf(esc, "\x1b%s%d%s", start, num, end);
  write(fd, esc, strlen(esc));
}
#endif

static void _backward(int size)
{
#ifdef USE_CURSES
  int y = 0, x = 0;
  getsyx(y, x); x -= size; move(y, x);
  refresh();
#else
  outesc(1, "[", size, "D");
#endif
}

static void _forward(int size)
{
#ifdef USE_CURSES
  int y = 0, x = 0;
  getsyx(y, x); x += size; move(y, x);
  refresh();
#else
  outesc(1, "[", size, "C");
#endif
}

static void _delete(int size)
{
#ifdef USE_CURSES
  for (; size > 0; size--) delch();
  refresh();
#else
  outesc(1, "[", size, "P");
#endif
}

static void _insert(int size, char *str)
{
#ifdef USE_CURSES
  insnstr(str, size);
  _forward(size);
  refresh();
#else
  outesc(1, "[", size, "@");
  write(1, str, size);
#endif
}

static void _clear(void)
{
#ifdef USE_CURSES
  clear();
  refresh();
#else
  outesc(1, "[", 2, "J");
  outesc(1, "[", 0, ";0H");
#endif
}

static int _getc(void)
{
  unsigned char c;
  int r;
#ifdef USE_CURSES
  while (1) {
    r = getch();
    if (r == ERR)
      return -1;
    switch (r) {
    case KEY_UP:        c = 0x10; break;
    case KEY_DOWN:      c = 0x0e; break;
    case KEY_LEFT:      c = 0x02; break;
    case KEY_RIGHT:     c = 0x06; break;
    case KEY_BACKSPACE: c = 0x08; break;
    case KEY_DC:        c = 0x7f; break;
    case KEY_ENTER:     c = '\n'; break;
    default:
      if (r > 0xff)
	continue;
      c = r;
      break;
    }
    break;
  }
#else
#ifndef WIN32
  r = read(0, &c, 1);
#else
  c = _getch();
  r = 1;
  switch (c) {
  case 0x0d: c = '\n'; break;
  default: break;
  }
#endif
  if (r <= 0)
    return -1;
#endif
  return c;
}

static void _puts(int size, const char *str)
{
#ifdef USE_CURSES
  addnstr(str, size);
  refresh();
#else
  write(1, str, size);
#endif
}

static int backward(int curpos, int used_size, int size)
{
  if (size > curpos)
    size = curpos;

  if (size > 0) {
    _backward(size);
    curpos -= size;
  }

  return curpos;
}

static int forward(int curpos, int used_size, int size)
{
  if (size > used_size - curpos)
    size = used_size - curpos;

  if (size > 0) {
    _forward(size);
    curpos += size;
  }

  return curpos;
}

static int move_to_start(int curpos, int used_size)
{
  return backward(curpos, used_size, curpos);
}

static int move_to_end(int curpos, int used_size)
{
  return forward(curpos, used_size, used_size - curpos);
}

static int delete(char *line, int curpos, int *used_size_p, int size)
{
  if (size > *used_size_p - curpos)
    size = *used_size_p - curpos;

  if (size > 0) {
    memmove(line + curpos, line + curpos + size, *used_size_p - curpos);
    _delete(size);
    *used_size_p -= size;
  }

  return curpos;
}

static int insert(char *line, int curpos, int *used_size_p, char *str, int size)
{
  if (size > 0) {
    memmove(line + curpos + size, line + curpos, *used_size_p - curpos);
    _insert(size, str);
    *used_size_p += size;
    memcpy(line + curpos, str, size);
    curpos += size;
  }

  return curpos;
}

static int clear_to_end(char *line, int curpos, int *used_size_p)
{
  return delete(line, curpos, used_size_p, *used_size_p - curpos);
}

static int clear_line(char *line, int curpos, int *used_size_p)
{
  return clear_to_end(line, move_to_start(curpos, *used_size_p), used_size_p);
}

static void puts_files(char *files[])
{
  int i, b = 0, pos = 0, len;

  _puts(1, "\n");
  for (i = 0; files[i]; i++) {
    if (i == 0)
      continue;
    if (b) {
      _puts(1, "\t");
      pos = ((pos / 8) + 1) * 8;
    }
    len = strlen(files[i]);
    _puts(len, files[i]);
    pos += len;
    b = 1;
    if (pos >= 64) {
      _puts(1, "\n");
      b = 0;
      pos = 0;
    }
  }
  if (b)
    _puts(1, "\n");

  return;
}

static char *call_complete_func(char *line, int curpos, int nocomp)
{
  int num, start, i, len;
  char *pathname = NULL, *filename, **files = NULL;
  static char *complete;

  complete = NULL;

  if (rl_attempted_completion_function == NULL)
    goto ret;

  for (start = curpos; start > 0; start--) {
    if (isspace(line[start - 1]))
      break;
  }

  files = rl_attempted_completion_function(line + start, start, curpos);
  if (files == NULL)
    goto ret;

  num = 0;
  for (i = 0; files[i]; i++)
    num++;

  len = curpos - start;

  pathname = malloc(len + 1);
  strncpy(pathname, line + start, len);
  pathname[len] = '\0';

  filename = strrchr(pathname, '/');
  filename = (filename == NULL) ? pathname : (filename + 1);

  if (files[0] == NULL)
    goto ret;

  if (nocomp) {
    if (!strcmp(files[0], pathname)) {
      puts_files(files);
    } else {
      puts_files(files + 1);
    }
    complete = strdup("\n");
  } else if (!strcmp(files[0], pathname)) {
    if ((num == 1) || ((num == 2) && !strcmp(files[1], filename))) {
      complete = strdup(" ");
    } else {
      puts_files(files);
      complete = strdup("\n");
    }
  } else {
    complete = strdup(files[0] + len);
  }

ret:
  if (files != NULL) {
    for (i = 0; files[i]; i++) {
      free(files[i]);
    }
    free(files);
  }

  if (pathname != NULL)
    free(pathname);

  return complete;
}

char *readline(const char *prompt)
{
  char *line = NULL, *cut = NULL, *history_line, *complete;
  unsigned char c;
  int r, len, line_size = 0, cut_size = 0, size = 0, curpos = 0, pushed = 0;
  escape_t escape;

  init();

  if (prompt) {
    _puts(strlen(prompt), prompt);
  }

  escape = ESCAPE_NONE;

  line = expand_buffer(line, &line_size, 1, 0);
  line[0] = '\0';
  cut = expand_buffer(cut, &cut_size, 1, 0);
  cut[0] = '\0';

  nlline_history_current_init();

  while (1) {
    c = 0;
    r = _getc();
    if (r < 0) {
      c = '\n';
      size = 0;
      break;
    }
    c = r;

    switch (escape) {
    case ESCAPE_NONE:
      switch (c) {
#ifndef WIN32
      case 0x1b: escape = ESCAPE_START; continue;
#else
      case 0xe0: escape = ESCAPE_CURSOR; continue;
#endif
      default: break;
      }
      break;
    case ESCAPE_START:
      switch (c) {
      case 0x5b: escape = ESCAPE_CURSOR; continue;
      default: break;
      }
      break;
    case ESCAPE_CURSOR:
      switch (c) {
#ifndef WIN32
      case 0x41: /* up */
#else
      case 0x48: /* up */
#endif
	c = 0x10; /* Ctrl+p */
	escape = ESCAPE_NONE;
	break;
#ifndef WIN32
      case 0x42: /* down */
#else
      case 0x50: /* down */
#endif
	c = 0x0e; /* Ctrl+n */
	escape = ESCAPE_NONE;
	break;
#ifndef WIN32
      case 0x44: /* left */
#else
      case 0x4b: /* left */
#endif
	c = 0x02; /* Ctrl+b */
	escape = ESCAPE_NONE;
	break;
#ifndef WIN32
      case 0x43: /* right */
#else
      case 0x4d: /* right */
#endif
	c = 0x06; /* Ctrl+f */
	escape = ESCAPE_NONE;
	break;
      default:
	break;
      }
      break;
    }

    switch (c) {
    case 0x00: /* Ctrl+Space */
      continue;
    case 0x01: /* Ctrl+a */
      curpos = move_to_start(curpos, size);
      continue;
    case 0x02: /* Ctrl+b */
      curpos = backward(curpos, size, 1);
      continue;
    case 0x03: /* Ctrl+c */
      c = '\n';
      size = 0;
      break;
    case 0x04: /* Ctrl+d */
      if (curpos < size)
	curpos = delete(line, curpos, &size, 1);
      else if (!curpos && !size)
	goto err;
      else {
	complete = call_complete_func(line, curpos, 1);
	if (complete == NULL)
	  continue;
	if (!strcmp(complete, "\n")) {
	  if (prompt) {
	    _puts(strlen(prompt), prompt);
	    _puts(size, line);
	    curpos = backward(size, size, size - curpos);
	  }
	}
	free(complete);
      }
      continue;
    case 0x05: /* Ctrl+e */
      curpos = move_to_end(curpos, size);
      continue;
    case 0x06: /* Ctrl+f */
      curpos = forward(curpos, size, 1);
      continue;
    case 0x07: /* Ctrl+g */
      continue;
    case 0x08: /* Ctrl+h or Backspace */
      if (curpos > 0) {
	curpos = backward(curpos, size, 1);
	curpos = delete(line, curpos, &size, 1);
      }
      continue;
    case 0x09: /* Ctrl+i or Tab */
      complete = call_complete_func(line, curpos, 0);
      if (complete == NULL)
	continue;
      if (!strcmp(complete, "\n")) {
	if (prompt) {
	  _puts(strlen(prompt), prompt);
	  _puts(size, line);
	  curpos = backward(size, size, size - curpos);
	}
      } else {
	len = strlen(complete);
	line = expand_buffer(line, &line_size, size + strlen(complete), size);
	if (line == NULL) {
	  free(complete);
	  goto err;
	}
	curpos = insert(line, curpos, &size, complete, strlen(complete));
      }
      free(complete);
      continue;
    case 0x0a: /* Ctrl+j or Enter */
      c = '\n';
      break;
    case 0x0b: /* Ctrl+k */
      cut = expand_buffer(cut, &cut_size, size - curpos + 1, strlen(cut) + 1);
      if (cut == NULL)
	goto err;
      strcpy(cut, line + curpos);
      curpos = clear_to_end(line, curpos, &size);
      continue;
    case 0x0c: /* Ctrl+l */
      _clear();
      if (prompt) {
	_puts(strlen(prompt), prompt);
      }
      _puts(size, line);
      curpos = backward(size, size, size - curpos);
      continue;
    case 0x0d: /* Ctrl+m */
      c = '\n';
      break;
    case 0x0e: /* Ctrl+n */
      if (nlline_history_is_current_end() != 0)
	continue;
      nlline_history_current_prev();
      history_line = nlline_history_get_current();
      if (history_line == NULL)
	continue;
      len = strlen(history_line);
      curpos = clear_line(line, curpos, &size);
      line = expand_buffer(line, &line_size, len + 1, size);
      if (line == NULL)
	goto err;
      curpos = insert(line, curpos, &size, history_line, len);
      if (pushed && (nlline_history_is_current_end() > 0)) {
	nlline_history_pop();
	nlline_history_current_init();
	pushed = 0;
      }
      continue;
    case 0x0f: /* Ctrl+o */
      continue;
    case 0x10: /* Ctrl+p */
      if (nlline_history_is_current_start() > 0)
	continue;
      nlline_history_current_next();
      history_line = nlline_history_get_current();
      if (history_line == NULL)
	continue;
      if (!pushed) {
	nlline_history_push(line);
	pushed = 1;
      }
      len = strlen(history_line);
      curpos = clear_line(line, curpos, &size);
      line = expand_buffer(line, &line_size, len + 1, size);
      if (line == NULL)
	goto err;
      curpos = insert(line, curpos, &size, history_line, len);
      continue;
    case 0x11: /* Ctrl+q */
    case 0x12: /* Ctrl+r */
    case 0x13: /* Ctrl+s */
      continue;
    case 0x14: /* Ctrl+t */
      if (curpos > 0) {
	if (curpos == size)
	  curpos = backward(curpos, size, 1);
	if (curpos > 0) {
	  c = line[curpos];
	  curpos = delete(line, curpos, &size, 1);
	  curpos = backward(curpos, size, 1);
	  curpos = insert(line, curpos, &size, (char *)&c, 1);
	}
	curpos = forward(curpos, size, 1);
      }
      continue;
    case 0x15: /* Ctrl+u */
      while (curpos > 0) {
	curpos = backward(curpos, size, 1);
	curpos = delete(line, curpos, &size, 1);
      }
      continue;
    case 0x16: /* Ctrl+v */
      continue;
    case 0x17: /* Ctrl+w */
      while ((curpos > 0) && isspace(line[curpos - 1])) {
	curpos = backward(curpos, size, 1);
	curpos = delete(line, curpos, &size, 1);
      }
      while ((curpos > 0) && !isspace(line[curpos - 1])) {
	curpos = backward(curpos, size, 1);
	curpos = delete(line, curpos, &size, 1);
      }
      continue;
    case 0x18: /* Ctrl+x */
      continue;
    case 0x19: /* Ctrl+y */
      line = expand_buffer(line, &line_size, size + strlen(cut), size);
      if (line == NULL)
	goto err;
      curpos = insert(line, curpos, &size, cut, strlen(cut));
      continue;
    case 0x1a: /* Ctrl+z */
      continue;
    case 0x7f: /* DEL */
      if (curpos < size)
	curpos = delete(line, curpos, &size, 1);
      continue;
    default:
      break;
    }

    if (c == '\n')
      break;

    line = expand_buffer(line, &line_size, size + 1, size);
    if (line == NULL)
      goto err;

    curpos = insert(line, curpos, &size, (char *)&c, 1);
  }

  line = expand_buffer(line, &line_size, size + 1, size);
  if (line == NULL)
    goto err;

  line[size] = '\0';

  if (pushed)
    nlline_history_pop();

  goto ret;

err:
  if (line) free(line);
  line = NULL;

ret:
  if (cut) free(cut);

  done();

  if (c == '\n')
    write(1, "\n", 1);

  return line;
}

void add_history(const char *line)
{
  nlline_history_push(line);
}

void clear_history(void)
{
  nlline_history_clear_all();
}
