/* ARISA - Curses functions for arisa-sh
 * Copyright (C) 2003, 2004 Carl Ritson
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
 */

#define HISTORY_SIZE 20

/* Enums and Typedefs **/
enum {
	C_DEFAULT	= 1,
	C_RED,
	C_BLUE,
	C_GREEN,
	C_YELLOW,
	C_MAGENTA,
	C_PURPLE,
	C_CYAN,
	C_WHITE,
	C_BLACK
};

enum {
	RET_NOP		= 0,
	RET_RESIZE	= -1,
	RET_PAGEUP	= -11,
	RET_PAGEDOWN	= -12
};

typedef struct inputbuf_t {
	WINDOW	*win;
	
	char 	*buffer;
	size_t 	bufsize;
	
	int	width;
	int 	used;
	int	shift;
	int	cursor;

	char	**history;
	int	no_history;
	int	history_next;
	int	history_current;
} inputbuf_t;


/** Global Variables **/

static WINDOW		*termwin, *lines, *mid, *bot;
static inputbuf_t 	*inputbuf;
static volatile int	need_resize = 0;
static int		x_size = 0, y_size = 0;
static int		no_lines = 500, line_shift = 0;

/** Functions **/

/* Input Buffer */

static void inputbuf_clear(inputbuf_t *ib); // pre-declaration

static inputbuf_t *inputbuf_alloc(WINDOW *win, size_t size) {
	inputbuf_t *ib; 
	int x,y;
	
	if(size < 1)
		return NULL;
	
	getmaxyx(win,y,x);
	
	ib		= malloc(sizeof(inputbuf_t));
	ib->win		= win;
	ib->buffer	= malloc(size);
	ib->bufsize	= size;
	ib->width	= x;
	ib->used	= 0;
	
	ib->no_history		= HISTORY_SIZE;
	ib->history		= malloc(sizeof(char *) * ib->no_history);
	ib->history_next	= 0;
	ib->history_current	= 0;

	inputbuf_clear(ib);

	return ib;
}	

static void inputbuf_free(inputbuf_t *ib) {
	int i;
	
	free(ib->buffer);
	for(i = 0; i < ib->no_history; ++i) {
		if(ib->history[i] != NULL)
			free(ib->history[i]);
	}
	free(ib->history);
	free(ib);
}

static int cursor_off_screen(inputbuf_t *ib) {
	if((ib->cursor - ib->shift) > (ib->width - 1))
		return 1;
	else if((ib->cursor - ib->shift) < 0)
		return 1;
	else
		return 0;
}

static void inputbuf_redraw_cursor(inputbuf_t *ib) {
	wmove(ib->win,0,ib->cursor - ib->shift);
	wnoutrefresh(ib->win);
}

static void inputbuf_redraw(inputbuf_t *ib) {
	int tmp;
	if(cursor_off_screen(ib)) {
		tmp = ib->cursor / (ib->width / 2);
		if(ib->cursor < ib->shift)
			ib->shift = tmp * (ib->width / 2);
		else
			ib->shift = (tmp - 1) * (ib->width / 2);
	}
	werase(ib->win);
	waddnstr(ib->win, ib->buffer + ib->shift, ib->width);
	inputbuf_redraw_cursor(ib);
}

static void inputbuf_change_width(inputbuf_t *ib, int width) {
	if(width > 1) {
		ib->width = width;
		inputbuf_redraw(ib);
	}
}

static void inputbuf_clear(inputbuf_t *ib) {
	ib->buffer[0]   = '\0';
	ib->cursor      = 0;
	ib->used        = 0;
	ib->shift       = 0;

	werase(ib->win);
}

static void inputbuf_loadstr(inputbuf_t *ib, const char *str) {
	strncpy(ib->buffer,str,ib->bufsize - 1);
	ib->buffer[ib->bufsize - 1] = '\0';
	ib->used	= strlen(ib->buffer);
	ib->cursor	= ib->used;
	ib->shift	= 0;
	inputbuf_redraw(ib);
}

static void inputbuf_removechar(inputbuf_t *ib) {
	memmove(ib->buffer + ib->cursor,
		ib->buffer + ib->cursor + 1,
		ib->used - ib->cursor);
	ib->buffer[--ib->used] = '\0';
	wdelch(ib->win);
	if((ib->used - ib->shift) >= ib->width) {
		mvwaddch(ib->win,0,ib->width - 1, 
				ib->buffer[ib->shift + ib->width - 1]);
	}
}

static void inputbuf_addchar(inputbuf_t *ib, const int c) {
	if(ib->cursor == ib->used) {
		ib->buffer[ib->cursor + 1] = '\0';
		waddch(ib->win,c);
	} else {
		memmove(ib->buffer + ib->cursor,
			ib->buffer + ib->cursor - 1,
			ib->used - (ib->cursor - 1));
		winsch(ib->win,c);
	}
	ib->buffer[ib->cursor] = (char) c;
	ib->cursor++;
	ib->used++;
}

static void history_add(inputbuf_t *ib, const char *str) {
	if(str == NULL)
		return;
	if(strlen(str) == 0)
		return;
	
	if(ib->history[ib->history_next] != NULL)
		free(ib->history[ib->history_next]);
	ib->history[ib->history_next] = strdup(str);

	ib->history_next++;
	if(ib->history_next >= ib->no_history)
		ib->history_next = 0;
	ib->history_current = ib->history_next;
}

static void history_load(inputbuf_t *ib, int shift) {
	int pos = ib->history_current + shift;
	
	pos = pos >= 0 ? pos % ib->no_history : 
		ib->no_history - ((-pos) % ib->no_history);
	
	if(shift == 1 && pos == ib->history_next)
		inputbuf_loadstr(ib,"");
	else if(ib->history[pos] != NULL) {
		inputbuf_loadstr(ib,ib->history[pos]);
		ib->history_current = pos;
	}
}

static int inputbuf_readchars(inputbuf_t *ib, 
		char *buffer, size_t bufsize) {
	int c, cread = -1, ret = RET_NOP;
	
	do {
		cread++;
		c = wgetch(ib->win);
		switch (c) {
			case KEY_RESIZE:
				ret = RET_RESIZE;
			case ERR:
				break;
			case KEY_LEFT:
				if(ib->cursor > 0)
					ib->cursor--;
				break;
			case KEY_RIGHT:
				if(ib->cursor < ib->used)
					ib->cursor++;
				break;
			case KEY_UP:
				history_load(ib,-1);
				break;
			case KEY_DOWN:
				history_load(ib,+1);
				break;
			case KEY_PPAGE:
				ret = RET_PAGEUP;
				break;
			case KEY_NPAGE:
				ret = RET_PAGEDOWN;
				break;
			case KEY_DC:
				if(ib->used > 0 && ib->cursor < ib->used)
					inputbuf_removechar(ib);
				break;
			case 0177:
			case KEY_BACKSPACE:
				if(ib->cursor > 0) {
					ib->cursor--;
					inputbuf_redraw_cursor(ib);
					inputbuf_removechar(ib);
				}
				break;
			case KEY_HOME:
				ib->cursor = 0;
				break;
			case KEY_END:
				ib->cursor = ib->used;
				break;
			case KEY_ENTER:
			case '\n':
				ib->buffer[ib->used] = '\0';
				history_add(ib,ib->buffer);
				snprintf(buffer,bufsize,"%s",ib->buffer);
				ret = strlen(buffer);
				
				inputbuf_clear(ib);
				break;
			default:
				if(!iscntrl(c) && ib->used < ib->bufsize - 1)
					inputbuf_addchar(ib,c);
				break;
		}
		
		if(cursor_off_screen(ib))
			inputbuf_redraw(ib);
		inputbuf_redraw_cursor(ib);
	} while(ret == RET_NOP && c != ERR);

	if(cread > 0)
		wnoutrefresh(ib->win);
	
	return ret;
}


/* The Rest */

static void cleanup(void) {
	endwin();
	inputbuf_free(inputbuf);
}

static void refresh_lines(WINDOW *win) {
	int pminrow = no_lines - ((y_size - 2) + line_shift);
	if(pminrow < 0)
		pminrow = 0;
	prefresh(win,pminrow,0,0,0,y_size - 3,x_size);
}

static void put_line(WINDOW *win, const char *line) {
	attr_t attr = A_NORMAL;
	int i;
	
	waddch(win,'\n');
	
	for(i = 0; line[i] != '\0'; ++i) {
		if(line[i] == '%') {
			i++;
			switch(line[i]) {
				case 'n':
					attr = A_NORMAL;
					wcolor_set(win,C_DEFAULT,NULL);
					break;
				case '_':
				case '9':
					attr = (attr & A_BOLD) ? 
						attr & ~A_BOLD : attr | A_BOLD;
					break;
				case 'U':
					attr = (attr & A_UNDERLINE) ?
						attr & ~A_UNDERLINE :
						attr | A_UNDERLINE;
					break;
				case 'B':
					attr |= A_BOLD;
				case 'b':
					wcolor_set(win,C_BLUE,NULL);
					break;
				case 'R':
					attr |= A_BOLD;
				case 'r':
					wcolor_set(win,C_RED,NULL);
					break;
				case 'G':
					attr |= A_BOLD;
				case 'g':
					wcolor_set(win,C_GREEN,NULL);
					break;
				case 'Y':
					attr |= A_BOLD;
				case 'y':
					wcolor_set(win,C_YELLOW,NULL);
					break;
				case 'M':
					attr |= A_BOLD;
				case 'm':
					wcolor_set(win,C_MAGENTA,NULL);
					break;
				case 'P':
					attr |= A_BOLD;
				case 'p':
					wcolor_set(win,C_PURPLE,NULL);
					break;
				case 'C':
					attr |= A_BOLD;
				case 'c':
					wcolor_set(win,C_CYAN,NULL);
					break;
				case 'W':
					attr |= A_BOLD;
				case 'w':
					wcolor_set(win,C_WHITE,NULL);
					break;
				case 'K':
					attr |= A_BOLD;
				case 'k':
					wcolor_set(win,C_BLACK,NULL);
					break;
				default:
					waddch(win,line[i-1] | attr);
					if(line[i] != '\0' && line[i] != '%')
						waddch(win,line[i] | attr);
					break;
			}
		} else
			waddch(win,line[i] | attr);
	}
	wcolor_set(win,C_DEFAULT,NULL);
	refresh_lines(win);
}

static void curses_print(WINDOW *win, const char *line) {
	if(timestamp) {
		struct tm	timeval;
		char		tsbuf[32], buffer[600];
		time_t		now = time(NULL);

		localtime_r(&now,&timeval);
		if(timestamp == 2)
			strftime(tsbuf,sizeof(tsbuf),"%T",&timeval);
		else
			strftime(tsbuf,sizeof(tsbuf),"%R",&timeval);
		snprintf(buffer,sizeof(buffer),"%%K[%%n%s%%K]%%n %s",
				tsbuf,line);
		put_line(win,buffer);
	} else
		put_line(win,line);
}

static void draw_prompt(WINDOW *win, int x) {
	int i,len;
	werase(win);
	wprintw(win,"%s",prompt);
	len = strlen(prompt);
	for(i = 0; i < (x-len); ++i)
		wprintw(win,"-");
	wnoutrefresh(win);
}

static int get_term_size(int *width, int *height) {
#ifdef TIOCGWINSZ
	struct winsize ws;
	
	if (ioctl(0, TIOCGWINSZ, &ws) < 0)
		return -1;
	if (ws.ws_row == 0 && ws.ws_col == 0)
		return -1;
	
	*width = ws.ws_col;
	*height = ws.ws_row;

	if (*width < 1)
		*width = 1;
	if (*height < 1)
		*height = 1;
	
	return 0;
#else
	return -1;
#endif /* TIOCGWINSZ */
}

static void resize(void) {
	char buffer[64];
	int x,y;
	
	if(get_term_size(&x,&y) != 0)
		return;
	
	if(x != x_size || y != y_size) {
		x_size = x; y_size = y;
		resizeterm(y_size,x_size);
		
		wresize(termwin,y_size,x_size);
		
		wresize(lines,no_lines,x_size);
		
		wresize(mid,1,x_size);
		mvwin(mid,y_size-2,0);
		
		wresize(bot,1,x_size);
		mvwin(bot,y_size-1,0);
		
		snprintf(buffer,sizeof(buffer),
			"%%b* Terminal Size Changed To: %dx%d",
			x_size,y_size);
		put_line(lines,buffer);
		
		draw_prompt(mid,x_size);
		
		inputbuf_change_width(inputbuf,x);

		doupdate();
	}

	need_resize = 0;
}

static void sigwinch(int sig) {
	need_resize = 1;
}

static void pageup(void) {
	line_shift += (y_size-2) / 2;
	if(line_shift > (no_lines - ((y_size-2) / 2)))
		line_shift = (no_lines - ((y_size-2) / 2));
	refresh_lines(lines);
	inputbuf_redraw_cursor(inputbuf);
}

static void pagedown(void) {
	line_shift -= (y_size-2) / 2;
	if(line_shift < 0)
		line_shift = 0;
	refresh_lines(lines);
	inputbuf_redraw_cursor(inputbuf);
}

static int ncurses_main(void) {
#define KBD 0
#define SOK 1
	char 		buffer[512], recvbuf[512];
	struct pollfd	pfd[2];
	int x,y,ret = 0;
	
	/* root window */
	termwin = initscr();

	/* init colours */
	start_color();
#ifdef NCURSES_VERSION
#define BG_COLOR (-1)
	use_default_colors();
	init_pair(C_DEFAULT,	-1,		-1);
#else
#define BG_COLOR COLOR_BLACK
	init_pair(C_DEFAULT,	COLOR_WHITE,	COLOR_BLACK);
#endif
	init_pair(C_RED,	COLOR_RED,	BG_COLOR);
	init_pair(C_BLUE,	COLOR_BLUE,	BG_COLOR);
	init_pair(C_GREEN,	COLOR_GREEN,	BG_COLOR);
	init_pair(C_YELLOW,	COLOR_YELLOW,	BG_COLOR);
	init_pair(C_MAGENTA,	COLOR_MAGENTA,	BG_COLOR);
	init_pair(C_CYAN,	COLOR_CYAN,	BG_COLOR);
	init_pair(C_WHITE,	COLOR_WHITE,	BG_COLOR);
	init_pair(C_BLACK,	COLOR_BLACK,	BG_COLOR);

	/* single characters with no echo */
	noecho();
	cbreak();

#ifdef SIGWINCH
	signal(SIGWINCH,sigwinch);
#endif

	atexit(cleanup);
		
	/* allocate windows */
	getmaxyx(termwin,y,x);
	x_size = x;
	y_size = y;
	lines = newpad(no_lines,x);
	mid = newwin(1,x,y-2,0);
	bot = newwin(1,x,y-1,0);
	
	/* prompt window */
	draw_prompt(mid,x_size);
	
	/* input window */
	keypad(bot,TRUE);
	nodelay(bot,TRUE);
	inputbuf = inputbuf_alloc(bot,510);
	
	/* output window */
	scrollok(lines,TRUE);
	wmove(lines,no_lines-1,0);
	refresh_lines(lines);
	
	inputbuf_redraw_cursor(inputbuf);
	
	doupdate();
	
	pfd[KBD].fd	= fileno(stdin);
	pfd[KBD].events	= POLLIN;
	pfd[SOK].fd	= sok;
	pfd[SOK].events	= POLLIN;
	
	/* tell arisa to send colour codes */
	send(sok,"colour int\r\n",12,0);
	for(;;) {
		pfd[KBD].revents = 0;
		pfd[SOK].revents = 0;
		
		ret = poll(pfd,2,-1);
		x = errno;
		
		if(need_resize)
			resize();
		
		if(ret == 0 || (ret == -1 && x == EINTR)) {
			continue;
		} else if(ret == -1) {
			// FIXME: message
			exit(1);
		}
		
		if(pfd[KBD].revents == POLLIN) {
			ret = inputbuf_readchars(inputbuf,
					recvbuf,sizeof(recvbuf));
			if(ret > RET_NOP) {
				ret = snprintf(buffer,sizeof(buffer),
						"%s\r\n",recvbuf);
				ret = send(sok,buffer,ret,0);
				if(ret == -1) {
					snprintf(buffer,sizeof(buffer),
						"%%r* Send Error: %s",
						strerror(errno));
					curses_print(lines,buffer);
					doupdate();
					exit(1);
				}
				snprintf(buffer,sizeof(buffer),"%%b> %s",
						recvbuf);
				curses_print(lines,buffer);
				inputbuf_redraw_cursor(inputbuf);
			} else if(ret == RET_RESIZE) {
				resize();
			} else if(ret == RET_PAGEDOWN) {
				pagedown();
			} else if(ret == RET_PAGEUP) {
				pageup();
			}
		} else if(pfd[KBD].revents != 0)
			exit(1); // terminal probably hung up
		
		if(pfd[SOK].revents == POLLIN) {
			do {
				ret = recv_line(recvbuf,sizeof(recvbuf));
				if(ret >= 0) {
					if(recvbuf[0] == '[' && 
						recvbuf[strlen(recvbuf)-1] == ']') {
						if(prompt != NULL)
							free(prompt);
						prompt = strdup(recvbuf);
						draw_prompt(mid,x_size);
					} else 
						curses_print(lines,recvbuf);
					inputbuf_redraw_cursor(inputbuf);
				}
			} while(has_pending_data(sok) > 0 && ret >= 0);
			if(ret == -1) {
				snprintf(buffer,sizeof(buffer),
					"%%r* Socket Error: %s",
					strerror(errno));
				curses_print(lines,buffer);
				doupdate();
				exit(1);
			}
		} else if(pfd[SOK].revents != 0) {
			socklen_t len;
			int val;
			len = sizeof(val);
			ret = getsockopt(sok,SOL_SOCKET,SO_ERROR,&val,&len);
			snprintf(buffer,sizeof(buffer),
					"%%r* Socket Error: %s",
					strerror(ret != -1 ? val : errno));
			curses_print(lines,buffer);
			doupdate();
			exit(1);
		}
		doupdate();
	}
	
	return 0;
#undef KBD
#undef SOK
}

