/*
 * Copyright 1999 Silicon Graphics, Inc. All rights reserved.
 */

/*
	Leaner command input module.
	Support basic command line editing, history mechanism and completion
	mechanism.

	History mechanism supported:

	A command name 'h' or 'history' will not be returned by the
	rl utilities and will generate a list of the current history.
	An optional parameter can be passed along with the 'h' or
	'history' command to set the number of command being remembered.

	
     !!        Refer to the previous command.  By itself, this substitution
               repeats the previous command.

     !n        Refer to command line n .

     !-n       Refer to the current command line minus n.

     !str      Refer to the most recent command starting with str.

	The standard ^ keys are supported:

	^W : delete to previous word
	^D : delete current character
	^A : goto start of line
	^E : goto end of line
	^F : forward one character
	^B : backward one character
	^H : delete previous character
	^N : down history
	^K : erase to eol (from cursor)
	^L : clear screen and redisplay prompt
	^P : up history
	^U : erase to beginning of line (from cursor)
	^R : redraw input line
     ESC-f : forward one word
     ESC-b : backward one word
     ESC-d : delete next word
   ESC-DEL : delete previous word


	Completion mechanism supported:

	When TAB is pressed while having inputted line, call the function 
	registered beforehand to complete line. 
*/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <ctype.h>
#include <curses.h>
#include <term.h>
#include <termio.h>
#include "rl.h"

#define ctrl(c) ((c) & 0x1f)
static int maxh=DEF_HIST;	/* number of buffered commands */
static int maxl=DEF_LENGTH; 	/* maximum command length */
static int in;			/* input tty handle */
static char *prompt=">";	/* prompt to be used */
static char *bol, *leftN, *rightN, *upN, *downN, *home;	/* cursor movement */
static char *kup, *kdown, *kleft, *kright, *kdel;	/* keyboard input sequences */
static char *cod;		/* display management */
static char *bip;		/* sound the bell */
static int notty=0;		/* no controling terminal or missing basic kbd functionality */
static int width=80;		/* width of the screen we are working from */
static int xenl=0;
static char *buf;
static struct termio tio, stio;
static char *kwb="\033\177";
static char *kwf="\033d";
static char *fw="\033f";
static char *bw="\033b";
static rl_complete_func_t	rl_complete_func; /* function for completing line */ 

/* 
	setup terminal characteristics and allocate initial stuff 
*/
int
rl_init(char *p, int bufsize, int histsize)
{
char *term;
int ret;

	/* set prompt if specified */
	if(p) prompt=p;
	if(bufsize) maxl=bufsize;
	if(histsize) maxh=histsize;

	if(!(hist_init(maxh, maxl))) return 0;

	/* allocate a new buffer */
	if(!(buf=malloc(maxl))) return 0;

	/* setup terminal. If we do not have a terminal
	we'll use the gets() call. If we have a terminal
	but no TERM, we use the "dumb" terminal */

	in=fileno(stdin);
	if(!isatty(in))
	{
		notty=1;
		return 1;
	}

	printf("\n");
	bip="\007";

	if(!(term = getenv ("TERM"))) term="dumb";
	else
	{
		/* XXX force iris-ansi-net to iris-ansi 
		Current Linux terminfo does'nt know iris-ansi-net */
		if(!strcmp(term,"iris-ansi-net")) term[9]='\0';
	}
	if(setupterm(term, in, &ret)!=ERR)
	{
		/* if any of these basics go back to fgets() */
		if(!(upN=tigetstr("cuu")) ||
		   !(downN=tigetstr("cud")) ||
		   !(leftN=tigetstr("cub")) ||
		   !(bol=tigetstr("cr")) ||
		   !(rightN=tigetstr("cuf")) ||
		   !(cod=tigetstr("ed"))) { notty=1; return 1; }

		xenl=tigetflag("xenl");
		home=tigetstr("clear");
		kup=tigetstr("kcuu1");
		kdown=tigetstr("kcud1");
		kleft=tigetstr("kcub1");
		kright=tigetstr("kcuf1");
		kdel=tigetstr("kdch1");

	} else 
	{ 
		if(!ret)
			printf("Unknown TERM : %s\n", term);
		notty=1; 
		return 1; 
	}
	/* get window size */
	{
	struct winsize w;

		if (ioctl (in, TIOCGWINSZ, &w) == 0)
		{
			width=w.ws_col;
		}
		else /* use ENV */
		{
		char *ewidth;

			if ((ewidth = getenv ("COLUMNS")))
				width = atoi (ewidth);

			/* use what's in terminfo */
			if (width <= 0)
				width = tigetnum ("co");

		}

		if (width <= 1) width = 80;

	}
	/* set ourselves in the proper mode */
	{
		if(ioctl(in, TCGETA, &tio)) { notty=1; return 1;}

		stio=tio;

		tio.c_lflag &= ~(ICANON | ECHO);
		tio.c_iflag &= ~(ICRNL  | INLCR);
		tio.c_cc[VMIN] = 1;
		tio.c_cc[VTIME] = 0;
	}
	
	return 1;
}

#define UP_HISTORY 	1001
#define DOWN_HISTORY	1002
#define CURSOR_LEFT	1003
#define CURSOR_RIGHT	1004
#define DELETE		1005
#define BACKSPACE	1006
#define KILLLINE	1007
#define LINEDONE	1008
#define KILLWORD	1009
#define KILLTOBOL	1010
#define KILLTOEOL	1011
#define GOTOBOL		1012
#define GOTOEOL		1013
#define CLRSCR		1014
#define REDRAW		1015
#define KILL_WORD_FORWARD	1016
#define WORD_BACKWARD		1017
#define WORD_FORWARD		1018
#define COMPLETE_LINE	1019 /* for completing line */

#define NCTRL	16
static int ctrls[NCTRL][2]=
{
	{UP_HISTORY,	ctrl('P')},
	{DOWN_HISTORY,	ctrl('N')},
	{CURSOR_LEFT,	ctrl('B')},
	{CURSOR_RIGHT,	ctrl('F')},
	{DELETE	,	ctrl('D')},
	{BACKSPACE,	ctrl('H')},
	{LINEDONE,	ctrl('J')},
	{KILLWORD,	ctrl('W')},
	{KILLTOBOL,	ctrl('U')},
	{KILLTOEOL,	ctrl('K')},
	{GOTOBOL,	ctrl('A')},
	{GOTOEOL,	ctrl('E')},
	{CLRSCR,	ctrl('L')},
	{REDRAW,	ctrl('R')},
	{LINEDONE,	'\r'},
};
#define NBIND (sizeof(codes)/sizeof(codes[0]))
static int codes[]={
	UP_HISTORY,DOWN_HISTORY,CURSOR_LEFT,CURSOR_RIGHT,DELETE,
	KILLWORD,KILL_WORD_FORWARD,WORD_BACKWARD,WORD_FORWARD,
};
static char **seqs[]={
	&kup,&kdown,&kleft,&kright,&kdel,
	&kwb,&kwf,&bw,&fw
};

#define buz() putp(bip)

static int
getinput(void)
{
int idx=0;
char curseq[10];

	while(1)
	{
	int c=getc(stdin);
	int i;
	int found=0;

		if (c == '\t') {
			/* completing line */
			return COMPLETE_LINE;
		}
		/* check the control characters */
		for(i=0;i<NCTRL;i++)
			if(ctrls[i][1]==c) return ctrls[i][0];

		/* check the keyboard sequences */
		curseq[idx++]=c;
		for(i=0;i<NBIND;i++)
		{
		int j;

			if(!*(seqs[i])) continue;
			for(j=0;j<idx;j++)
			{
				if((*seqs[i])[j]==curseq[j])
				{
					/* set found if we match the entire current input */
					if(j==idx-1) found=1;
					if((*seqs[i])[j+1]=='\0') return codes[i];
				}
			}
		}
		if(!found) {
			if(isprint(c)) 
				return c;
			else { 
				buz(); idx=0;
			}
		}
	}
}

static int curpos;      	/* position of cursor inside the input string */
static int maxpos=0;      	/* current length of the input string */

static void curboth(int n)
{
int curx=curpos%width;
int cury=curpos/width;
int newx=(curpos+n)%width;
int newy=(curpos+n)/width;

	if(newy > cury) putp(tparm(downN, newy-cury));
	else if(cury > newy) putp(tparm(upN, cury-newy));
	if(newx > curx) putp(tparm(rightN, newx-curx));
	else if(curx > newx) putp(tparm(leftN, curx-newx));
	curpos+=n;
}

static void curleft(int n) { curboth(-n); }
static void curright(int n) { curboth(n); }

/* 
	This function clears the screen button. Displays the current buffer and
	sets the cursor at the proper position.
*/
static void
showbuf(int repos)
{
int max, i;
int pos=curpos;

	/* clear to end of display from where we are now */
	putp(cod);

	/* display the current buffer */
	for(i=curpos; i<maxpos; i+=max) {

		int c;

		max=width-(i%width);
		if(i+max > maxpos) max=maxpos-i;
		c=buf[i+max];
		buf[i+max]='\0';
		putp(buf+i);
		buf[i+max]=c;
		if(!((i+max)%width) && xenl) {

			putp("\n"); 
			putp(bol);
		}
	}
	curpos=maxpos;
	if(repos) curleft(maxpos-pos);
}

static char *
getline(void)
{
int plen=strlen(prompt);
int histoff=0;

	/* show the prompt */
	strcpy(buf, prompt);
	maxpos=plen;
	curpos=0;
	showbuf(0);
	curpos=plen;

	ioctl(in, TCSETA, &tio);

	{ 
		struct winsize w;
		int nw=0; 
		if (ioctl (in, TIOCGWINSZ, &w) == 0) nw=w.ws_col; 
		if(nw>1) width=nw;
	}

	while(1) {

		int i;

		switch((i=getinput())) { 

		case UP_HISTORY: case DOWN_HISTORY:
		{
		int inc=(i==UP_HISTORY?1:-1);
		char *p;

			if((p=hist_getcmd(histoff+inc)))
			{
				curleft(curpos-plen);
				strcpy(buf+plen, p);
				maxpos=strlen(buf);
				showbuf(0);
				curpos=maxpos;
				histoff+=inc;

			}else buz();
		}
		break;
		case BACKSPACE: case CURSOR_LEFT:
		{
			if(curpos==plen) buz();
			else
			{
				curleft(1);
				/* we need to reprint if backspace */
				if(i==BACKSPACE)
				{
					memmove(buf+curpos, buf+curpos+1, maxl-curpos);
					maxpos--;
					showbuf(1);
				}
			}
		}
		break;
		case DELETE:
		{
			if(curpos==maxpos) buz();
			else
			{
				memmove(buf+curpos, buf+curpos+1, maxl-curpos);
				maxpos--;
				showbuf(1);
			}
		}
		break;
		case CURSOR_RIGHT:
		{
			if(curpos==maxpos) buz();
			else { curright(1); }
		}
		break;
		case LINEDONE:
		{
			/* we're about to return, so set the cursor position */
			curright(maxpos-curpos);
			putp("\r\n");
			ioctl(in, TCSETA, &stio);
			return buf+plen;
			
		}
		/* erase entire line . Currently not linked to any keys */
		case KILLLINE:
		{
			curleft(curpos-plen);
			maxpos=plen;
			buf[plen]='\0';
			showbuf(1);
		}
		/* erase the current word */
		case KILLWORD:
		{
			/* if we are at the start of the line , bip */
			if(curpos==plen) buz();
			else
			{
			int i=curpos-1;

				/* if the cursor sits on a white character already 
				   find the first non white one */
				while(!isalnum(buf[i])&&i>plen) i--;
				/* skip back untill beginning of line or white again */
				while(isalnum(buf[i])&&i>plen) i--;
				if(i<maxpos && !isalnum(buf[i])) i++;
				/* move every backward */
				memmove(buf+i, buf+curpos, maxl-curpos);
				curleft(curpos-i);
				maxpos=strlen(buf);
				showbuf(1);
			}
		}
		break;
		case KILLTOBOL:
		{
			memmove(buf+plen, buf+curpos, maxl-curpos);
			curleft(curpos-plen);
			maxpos=strlen(buf);
			showbuf(1);
		}
		break;
		case KILLTOEOL:
		{
			buf[curpos]='\0';
			maxpos=strlen(buf);
			showbuf(1);
		}
		break;
		case GOTOBOL: { curleft(curpos-plen); } break;
		case GOTOEOL: { curright(maxpos-curpos); } break;
		case CLRSCR: 
		{ 
			if(home) {

				int i=curpos;

				putp(home); 
				curpos=0;
				showbuf(0);
				curpos=maxpos;
				curleft(maxpos-i);

			} else buz(); 

		} break;
				
		case REDRAW: { } break;  /* do nothing */
		case WORD_FORWARD:
			/* if we are at the start of the line , bip */
			if(curpos==maxpos) buz();
			else
			{
			int i=curpos;

				/* if the cursor sits on a white character already 
		   		find the first non white one */
				while(!isalnum(buf[i])&&i<maxpos) i++;
				/* scip back untill beginning of line or white again */
				while(isalnum(buf[i])&&i<maxpos) i++;
				curright(i-curpos);
			}
		break;

		case WORD_BACKWARD:
			/* if we are at the start of the line , bip */
			if(curpos==plen) buz();
			else
			{
			int i=curpos;

				/* if the cursor sits on a white character already 
		   		   find the first non white one */
				if(i>plen) i--;
				while(!isalnum(buf[i])&&i>plen) i--;
				/* scip back untill beginning of line or white again */
				while(isalnum(buf[i])&&i>plen) i--;
				while(!isalnum(buf[i])&&i>plen) i++;
				curleft(curpos-i);
			}
		break;

		case KILL_WORD_FORWARD:
			/* if we are at the start of the line , bip */
			if(curpos==maxpos) buz();
			else
			{
			int i=curpos;

				/* if the cursor sits on a white character already 
		   		find the first non white one */
				while(!isalnum(buf[i])&&i<maxpos) i++;
				/* scip back untill beginning of line or white again */
				while(isalnum(buf[i])&&i<maxpos) i++;
				while(!isalnum(buf[i])&&i<maxpos) i++;
				/* keep one space */
				if(i<maxpos && isalnum(buf[i])) i--;
				/* move every backward */
				memmove(buf+curpos, buf+i, maxl-i);
				maxpos=strlen(buf);
				showbuf(1);
			}
		break;

		case COMPLETE_LINE: /* complete line */
		{
			char *ret;
			int	retstr_len;
			int save_curpos;

			/* if rl_complete_func is not registered, bip */
			if (!rl_complete_func) {
				buz();
			} else {
				/* save current cursor position */
				save_curpos = curpos;
				/* since command list may be printed in rl_complete_func(), 
				move cursor to max position */
				curright(maxpos - curpos);
				/* call line completion function */
				ret = rl_complete_func(buf+plen, save_curpos-plen);
				if (ret == DRAW_NEW_ENTIRE_LINE) { 
					/* draw new entire line */
					curpos = 0;
					showbuf(0);
					/* resume cursor position */
					curleft(maxpos - save_curpos);
				} else if (ret == PRINT_BEEP) { 
					/* resume cursor position */
					curleft(maxpos - save_curpos);
					/* print beep character */
					buz();
				} else if (ret > 0) { 
					/* insert string that returned before position of cursor */ 
					retstr_len = strlen(ret);
					/* resume cursor position */
					curleft(maxpos - save_curpos);
					if (maxpos+retstr_len>maxl) {
						/* if we exceed maximum command length, bip */
						buz();
					} else {
						/* insert string that returned */
						memmove(buf+curpos+retstr_len, buf+curpos, maxl-curpos-retstr_len);
						strncpy(buf+curpos, ret, retstr_len);
						maxpos+=retstr_len;
						showbuf(1);
						curright(retstr_len);
					}
				}
			}
		}
		break;

		default: 
		{
			if(maxpos==maxl) buz();
			else
			{
				memmove(buf+curpos+1, buf+curpos, maxl-curpos-1);
				buf[curpos]=i;
				maxpos++;
				showbuf(1);
				curright(1);
			}
		}
		break;
		}
	}
}
char *
rl_getline(void)
{
char *p;
char *command;

	while(1)
	{
		if(notty)
		{
			printf("%s", prompt);
			if(!(command=fgets(buf, maxl, stdin))) return NULL;
			command[strlen(command)-1]='\0';
		}
		else if(!(command=getline())) return NULL;

		for(p=command;*p==' '||*p=='\t';p++);

		/* if not empty, pass it thought history */
		if(*p && (command=hist_cmd(command)))
		{
			/* hist_cmd() return a pointer to the actual history
			   entry, so make a copy to buf and return to user */
			strcpy(buf,command);
			return buf;
		}
	}
}

/* register function which complete line */
void
rl_register_complete_func(rl_complete_func_t complete_func)
{
	rl_complete_func = complete_func;
}



#ifdef MAIN
main()
{
	if(!rl_init(">> ", 1024, 100)) exit(0);

	printf("notty=%d\n", notty);

	while(1) printf("command=(%s)\n", rl_getline());
}
#endif
