/*
 * readline.c - Simple command line editor, with external support for
 *              completion
 *
 * Written 2005 by Werner Almesberger
 */

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <termios.h>


#define BUF_SIZE 256
#define DEFAULT_LINES 10


enum edit_result {
    edit_error,		/* a system call failed */
    edit_enter,		/* [Enter] */
    edit_quit,		/* ^C, EOF, timeout */
    edit_tab,		/* [Tab] */
};

static char *buffer;
static int buf_len; /* strlen(buffer) */
static char *cursor;
static char *history;	/* beginning of history buffer */
static char *history_last; /* last history line (initial buffer) */
static char *history_end; /* end of buffer */
static char *history_curr = NULL; /* currently saved history line */
static char history_saved[BUF_SIZE];
static int history_lines = DEFAULT_LINES; /* number of lines to keep */
static int timeout = 0; /* input timeout; 0 is forever */
static int delay  = 0; /* first input timeout; 0 is "timeout" */
static int unify_whitespace = 0;
static const char *cursor_marker = NULL;


/* delete backwards in buffer */

static void delete_to(char *to)
{
    int i,tail;

    buf_len -= cursor-to;
    tail = buf_len-(to-buffer);
    i = cursor-to;
    while (i < tail) {
	putc(cursor[i],stderr);
	i++;
    }
    while (i > tail) {
	putc('\b',stderr);
	i--;
    }
    for (i = 0; i != cursor-to; i++)
	putc(' ',stderr);
    for (i = 0; to[i]; i++)
	putc('\b',stderr);
    memmove(to,cursor,tail+1);
    for (i = 0; i != cursor-to && to[i]; i++)
	putc(to[i],stderr);
    while (i--)
	putc('\b',stderr);
    cursor = to;
}


/* copy history line "next" to the buffer */

static void swap_history(char *next)
{
    int next_len = strlen(next);
    int i;

    for (i = cursor-buffer; i != buf_len; i++)
	putc(' ',stderr);
    while (i--)
	putc('\b',stderr);
    for (i = 0; next[i]; i++)
	putc(next[i],stderr);
    while (i < cursor-buffer) {
	putc(' ',stderr);
	i++;
    }
    while (i-- > next_len)
	putc('\b',stderr);
    history_curr = next;
    strcpy(history_saved,next);
    buffer = next;
    buf_len = next_len;
    cursor = buffer+buf_len;
}


/*
 * Returns:
 * 0 on enter (save line in history)
 * 1 on special termination (timeout, ^C, etc.)
 * 2 on tab
 * -1 on error
 */

static int edit(void)
{
    int esc_state = 0;
    char *in,*out;

    buf_len = strlen(buffer);
    cursor = buffer+buf_len;

    /*
     * if there are two adjacent spaces in unified whitespace mode, put the
     * cursor there, and remove them.
     */
    if (!cursor_marker)
	fputs(buffer,stderr);
    else {
	for (in = out = buffer; *in; in++) {
	    if (!strncmp(in,cursor_marker,strlen(cursor_marker))) {
		cursor = in;
		in += strlen(cursor_marker)-1;
	    }
	    else {
		*out++ = *in;
		fputc(*in,stderr);
	    }
	}
	*out = 0;
	while (out-- != cursor)
	    fputc('\b',stderr);
    }

    while (1) {
	fd_set set;
	struct timeval to;
	int res,got,i;
	unsigned char in;

	fflush(stdout);
	FD_ZERO(&set);
	FD_SET(0,&set);
	to.tv_sec = delay ? delay : timeout;
	to.tv_usec = 0;
	delay = 0;
	res = select(1,&set,NULL,NULL,to.tv_sec ? &to : NULL);
	if (res < 0) {
	    perror("select");
	    return edit_error;
	}
	if (!res) {
	    printf("TIMEOUT");
	    break;
	}
	got = read(0,&in,1);
	if (got < 0) {
	    perror("read");
	    return edit_error;
	}
	if (!got) {
	    printf("EOF");
	    break;
	}
	switch (esc_state) {
	    case 1:
		/* skip over numeric/application keypad mode */
		esc_state = in == '[' || in == 'O' ? 2 : 0;
		continue;
	    case 2:
		esc_state = 0;
		switch (in) {
		    case 'A':
			in = 'P'-64;
			break;
		    case 'B':
			in = 'N'-64;
			break;
		    case 'C':
			in = 'F'-64;
			break;
		    case 'D':
			in = 'B'-64;
			break;
		    case 'H':
			in = 'A'-64;
			break;
		    case 'F':
			in = 'E'-64;
			break;
		    default:
			continue;
		}
		/* fall through */
	}
	if (in == '\e') {
	    esc_state = 1;
	    continue;
	}
	switch (in) {
	    char *word;

	    /* end input */
	    case '\r':
	    case '\n':
		printf("ENTER");
		return edit_enter;
	    case '\t':
		for (i = 0; i != cursor-buffer; i++)
		    putc('\b',stderr);
		for (i = 0; i != buf_len; i++)
		    putc(' ',stderr);
		while (i--)
		    putc('\b',stderr);
		printf("TAB");
		return edit_tab;
	    case 'C'-64:
		printf("INTR");
		return edit_quit;
	    case 'D'-64:
		if (cursor != buffer)
		    continue;
		printf("EOF");
		return edit_quit;

	    /* move inside the line */
	    case 'A'-64:
		for (i = 0; i != cursor-buffer; i++)
		    putc('\b',stderr);
		cursor = buffer;
		continue;
	    case 'E'-64:
		while (*cursor) {
		    putc(*cursor,stderr);
		    cursor++;
		}
		continue;
	    case 'B'-64:
		if (cursor != buffer) {
		    putc('\b',stderr);
		    cursor--;
		}
		continue;
	    case 'F'-64:
		if (*cursor) {
		    putc(*cursor,stderr);
		    cursor++;
		}
		continue;

	    /* deletion */
	    case 'U'-64:
		delete_to(buffer);
		continue;
	    case 'W'-64:
		for (word = cursor; word != buffer && isspace(word[-1]);
		  word--);
		while (word != buffer && !isspace(word[-1]))
		    word--;
		delete_to(word);
		continue;
	    case 'H'-64:
	    case 127:
		if (cursor == buffer)
		    continue;
		delete_to(cursor-1);
		continue;

	    /* history access */
	    case 'P'-64:
		if (buffer != history)
		    swap_history(buffer-BUF_SIZE);
		continue;
	    case 'N'-64:
		if (buffer != history_last)
		    swap_history(buffer+BUF_SIZE);
		continue;
	}
	if (in < 32)
	    continue;
	if (unify_whitespace && in == ' ' &&
	  (buffer == cursor || cursor[-1] == ' '))
	    continue;
	if (buf_len == BUF_SIZE-1)
	    continue;
	memmove(cursor+1,cursor,buf_len-(cursor-buffer)+1);
	*cursor++ = in;
	for (i = -1; cursor[i]; i++)
	    putc(cursor[i],stderr);
	while (i--)
	    putc('\b',stderr);
	buf_len++;
    }
    return edit_quit;
}


/*
 * -n N  allocate space for that many lines of history (default is 10)
 * -f F  file containing the history
 * -i S  default input string
 * -d N  initial timeout, in whole seconds
 * -t N  timeout, in whole seconds
 * -m S  string marking the cursor position
 * -w    compact whitespace
 */

static void usage(const char *name)
{
    fprintf(stderr,
"usage: %s [-n history_lines] [-f history_file] [-i default_input]\n"
"       %8s[-d delay_seconds] [-t timeout_seconds] [-m cursor_marker] [-w]\n",
      name,"");
    exit(1);
}


int main(int argc,const char **argv)
{
    const char *history_file = NULL;
    int i;
    enum edit_result res;
    struct termios old_termios,new_termios;
    char nul = 0;
    const char *default_input = &nul;
    char *end,*walk;

    for (i = 1; i < argc; i++) {
	if (*argv[i] != '-' || argv[i][2])
	    usage(argv[0]);
	switch (argv[i][1]) {
	    case 'w':
		unify_whitespace = 1;
		continue;
	}
	if (++i == argc)
	    usage(argv[0]);
	switch (argv[i-1][1]) {
	    char *end;

	    case 'n':
		history_lines = strtoul(argv[i],&end,10);
		if (*end)
		    usage(argv[0]);
		break;
	    case 'f':
		history_file = argv[i];
		break;
	    case 'i':
		if (strlen(argv[i]) >= BUF_SIZE)
		    abort();
		default_input = argv[i];
		break;
	    case 'd':
		delay = strtoul(argv[i],&end,10);
		if (*end)
		    usage(argv[0]);
		break;
	    case 't':
		timeout = strtoul(argv[i],&end,10);
		if (*end)
		    usage(argv[0]);
		break;
	    case 'm':
		cursor_marker = argv[i];
		break;
	    default:
		usage(argv[0]);
	}
    }

    /* set up history and editing buffer */
    history = malloc((history_lines+1)*BUF_SIZE);
    if (!history) {
	perror("calloc");
	return 1;
    }
    history_last = history;
    history_end = history+(history_lines+1)*BUF_SIZE;

    /* load history, if any */
    if (history_file) {
	FILE *file;

	file = fopen(history_file,"r");
	if (!file) {
	    perror(history_file);
	    return 1;
	}
	while (fgets(history_last,BUF_SIZE-1,file)) {
	    char *nl;

	    nl = strchr(history_last,'\n');
	    if (nl)
		*nl = 0;
	    if (history_last+BUF_SIZE == history_end)
		memmove(history,history+BUF_SIZE,history_lines*BUF_SIZE);
	    else
		history_last += BUF_SIZE;
	}
	fclose(file);
    }

    /* load the default input */
    buffer = history_last;
    strcpy(buffer,default_input);

    /* edit */
    if (tcgetattr(0,&old_termios) < 0) {
	perror("tcgetattr");
	return 1;
    }
    new_termios = old_termios;
    cfmakeraw(&new_termios);
    if (tcsetattr(0,TCSADRAIN,&new_termios) < 0) {
	perror("tcsetattr");
	return 1;
    }
    res = edit();
    if (tcsetattr(0,TCSADRAIN,&old_termios) < 0) {
	perror("tcsetattr");
	return 1;
    }
    if (res != edit_tab) /* completion is special */
	putc('\n',stderr);

    /* remove leading and trailing whitespace */
    while (*buffer && isspace(*buffer))
	buffer++;
    for (end = walk = buffer; *walk; walk++)
	if (!isspace(*walk))
	    end = walk;
    end[1] = 0;

    /* write the history if we had input */
    if (res == edit_enter && *buffer && history_file) {
	FILE *file;
	const char *p;

	/* rewrite the history, so that it rotates */
	file = fopen(history_file,"w");
	if (!file) {
	    perror(history_file);
	    goto done;
	}
	for (p = history; p != history_last; p += BUF_SIZE)
	    if (fprintf(file,"%s\n",p == history_curr ? history_saved : p)
	      < 0) {
		perror(history_file);
		goto done;
	    }
	if (fprintf(file,"%s\n",buffer) < 0) {
	    perror(history_file);
	    goto done;
	}
	if (fclose(file) < 0)
	    perror(history_file);
    }

    /* print input if there's any */
done:
    if (res == edit_error)
	return 1;

    if (res != edit_tab || !cursor_marker)
	printf(" %s\n",buffer);
    else {
	const char *p;

	putchar(' ');
	for (p = buffer; *p && p != cursor; p++)
	    putchar(*p);
	fputs(cursor_marker,stdout);
	while (*p) {
	    putchar(*p);
	    p++;
	}
	putchar('\n');
    }
    return 0;
}
