/* radare - LGPL - Copyright 2007-2013 - pancake */
/* dietline is a lightweight and portable library similar to GNU readline */

#include <r_cons.h>
#include <r_core.h>
#include <string.h>
#include <stdlib.h>

#if __WINDOWS__
#include <windows.h>
#define USE_UTF8 0
#else
#include <sys/ioctl.h>
#include <termios.h>
#include <signal.h>
#define USE_UTF8 1
#endif

static char *r_line_nullstr = "";

#define ONLY_VALID_CHARS 1

#if ONLY_VALID_CHARS
static inline int is_valid_char (unsigned char ch) {
	if (ch>=32 && ch<=127) return R_TRUE;
	switch (ch) {
	case 0: // wat
	case 1: // ^a
	case 4: // ^d
	case 5: // ^e
	case 8: // backspace
	case 9: // tab
	case 10: // newline
	case 13: // carriage return
	case 23: // ^w
	case 27: // arrow
		return R_TRUE;
	}
	return R_FALSE;
}
#endif

static int inithist() {
	ZERO_FILL (I.history);
	I.history.data = (char **)malloc ((I.history.size+1024)*sizeof(char *));
	if (I.history.data==NULL)
		return R_FALSE;
	I.history.size = R_LINE_HISTSIZE;
	memset (I.history.data, 0, I.history.size*sizeof(char *));
	return R_TRUE;
}

/* initialize history stuff */
R_API int r_line_dietline_init() {
	ZERO_FILL (I.completion);
	if (!inithist ())
		return R_FALSE;
	I.echo = R_TRUE;
	return R_TRUE;
}

#if USE_UTF8
/* read utf8 char into 's', return the length in bytes */
static int r_line_readchar_utf8(unsigned char *s, int slen) {
	// TODO: add support for w32
	int ret, len;
	for (len = 0; len+2<slen; len++) {
		ret = read (0, s+len, 1);
		if (ret==-1)
			return 0;
		if (s[len] < 28)
			return 1;
		if (ret == 1) {
			if (is_valid_char (s[len]))
				return 1;
			if ((s[len] & 0xc0) != 0x80) continue;
			if (len>0) break;
		} else return 0;
	}
	len++;
	s[len] = 0;
	return len;
}
#endif

static int r_line_readchar() {
	ut8 buf[2];
	*buf = '\0';
#if __WINDOWS__
	BOOL ret;
	DWORD mode, out;
	HANDLE h;
#endif

do_it_again:
#if __WINDOWS__
	h = GetStdHandle (STD_INPUT_HANDLE);
	GetConsoleMode (h, &mode);
	SetConsoleMode (h, 0); // RAW
	ret = ReadConsole (h, buf, 1, &out, NULL);
	// wine hack-around
	if (!ret && read (0, buf, 1) != 1)
		return -1;
	SetConsoleMode (h, mode);
#else
	do {
		int ret = read (0, buf, 1);
		if (ret == -1) return 0; // read no char
		if (ret == 0) return -1; // eof
		// TODO: add support for other invalid chars
		if (*buf==0x1a) { // ^Z
			kill (getpid (), SIGSTOP);
			buf[0] = 0;
		}
		if (*buf==0xc2 || *buf==0xc3) {
			read (0, buf+1, 1);
			*buf = '\0';
		}	
	} while (*buf == '\0');
#endif
#if ONLY_VALID_CHARS
	if (!is_valid_char (buf[0]))
		goto do_it_again;
#endif
	return buf[0];
}

R_API int r_line_hist_add(const char *line) {
	if (!I.history.data)
		inithist ();
	if (I.history.top>=I.history.size)
		I.history.top = I.history.index = 0; // workaround
	/* ignore dup */
	if (I.history.index>0 && !strcmp (line, I.history.data[I.history.index-1]))
		return R_FALSE;
	if (line && *line) { // && I.history.index < I.history.size) {
		I.history.data[I.history.top++] = strdup (line);
		I.history.index = I.history.top;
		return R_TRUE;
	}
	return R_FALSE;
}

static int r_line_hist_up() {
	if (I.hist_up)
		return I.hist_up (I.user);
	if (!I.history.data)
		inithist ();
	if (I.history.index>0) {
		strncpy (I.buffer.data, I.history.data[--I.history.index], R_LINE_BUFSIZE-1);
		I.buffer.index = I.buffer.length = strlen (I.buffer.data);
		return R_TRUE;
	}
	return R_FALSE;
}

static int r_line_hist_down() {
	if (I.hist_down)
		return I.hist_down (I.user);
	I.buffer.index = 0;
	if (!I.history.data)
		inithist ();
	if (I.history.index<I.history.size) {
		if (I.history.data[I.history.index+1] == NULL) {
			I.buffer.data[0]='\0';
			I.buffer.index = I.buffer.length = 0;
			return 0;
		}
		strncpy (I.buffer.data, I.history.data[++I.history.index], R_LINE_BUFSIZE-1);
		I.buffer.index = I.buffer.length = strlen (I.buffer.data);
		return R_TRUE;
	}
	return R_FALSE;
}

R_API const char *r_line_hist_get(int n) {
	int i = 0;
	if (!I.history.data)
		inithist ();
	if (I.history.data != NULL)
		for (i=0; i<I.history.size && I.history.data[i]; i++)
			if (n==i) return I.history.data[i];
	return NULL;
}

R_API int r_line_hist_list() {
	int i = 0;
	if (!I.history.data)
		inithist ();
	if (I.history.data != NULL)
		for (i=0; i<I.history.size && I.history.data[i]; i++)
			printf (" !%d  # %s\n", i, I.history.data[i]);
	return i;
}

R_API void r_line_hist_free() {
	int i;
	if (I.history.data != NULL)
	for (i=0; i<I.history.size; i++) {
		free (I.history.data[i]);
		I.history.data[i] = NULL;
	}
	free (I.history.data);
	I.history.data = NULL;
	I.history.index = 0;
}

/* load history from file. TODO: if file == NULL load from ~/.<prg>.history or so */
R_API int r_line_hist_load(const char *file) {
	FILE *fd;
	char buf[R_LINE_BUFSIZE],
		*path = r_str_home (file);
	if (path == NULL)
		return R_FALSE;
	if (!(fd = fopen (path, "r"))) {
		free (path);
		return R_FALSE;
	}
	while (fgets (buf, sizeof (buf), fd) != NULL) {
		buf[strlen (buf)-1] = 0;
		r_line_hist_add (buf);
	}
	fclose (fd);
	free (path);
	return R_TRUE;
}

R_API int r_line_hist_save(const char *file) {
	FILE *fd;
	int i, ret = R_FALSE;
	char *p, *path = r_str_home (file);
	if (path != NULL) {
		p = (char*)r_str_lastbut (path, R_SYS_DIR[0], NULL); // TODO: use fs
		if (p) {
			*p = 0;
			r_sys_rmkdir (path);
			*p = R_SYS_DIR[0];
		}
		fd = fopen (path, "w");
		if (fd != NULL && I.history.data) {
			for (i=0; i<I.history.index; i++) {
				fputs (I.history.data[i], fd);
				fputs ("\n", fd);
			}
			fclose (fd);
			ret = R_TRUE;
		}
	}
	free (path);
	return ret;
}

R_API int r_line_hist_chop(const char *file, int limit) {
	/* TODO */
	return 0;
}

R_API void r_line_autocomplete() {
	int argc = 0;
	char *p;
	const char **argv = NULL;
	int i, j, opt, plen, len = 0;
	int cols = r_cons_get_size (NULL)*0.82;

	/* prepare argc and argv */
	if (I.completion.run != NULL) {
		I.completion.run (&I);
		opt = argc = I.completion.argc;
		argv = I.completion.argv;
	} else opt = 0;

	p = (char *)r_str_lchr (I.buffer.data, ' ');
	if (!p)
		p = (char *)r_str_lchr (I.buffer.data, '@'); // HACK FOR r2
	if (p) {
		p++;
		plen = sizeof (I.buffer.data)-(int)(size_t)(p-I.buffer.data);
	} else {
		p = I.buffer.data; // XXX: removes current buffer 
		plen = sizeof (I.buffer.data);
	}
	/* autocomplete */
	if (argc==1) {
		int largv0 = strlen (argv[0]);
		if (largv0+3 < plen) {
			memcpy (p, argv[0], largv0);
			memcpy (p+largv0, " ", 2);
			I.buffer.length = I.buffer.index = strlen (I.buffer.data);
		}
	} else
	if (argc>0) {
		if (*p) {
			// TODO: avoid overflow
			const char *root = argv[0];
			int min_common_len = strlen(root);

			// try to autocomplete argument
			for (i=0; i<argc; i++) {
				j = 0;
				while (argv[i][j]==root[j] && root[j] != '\0') j++;
				if (j < min_common_len)
					min_common_len = j;
				root = argv[i];
			}
			strcpy (p, root);
			if (min_common_len<strlen (root))
				p[min_common_len] = 0;
			I.buffer.index = I.buffer.length = strlen (I.buffer.data);
		}
	}

	/* show options */
	if (opt>1 && I.echo) {
		const int sep = 3;
		int col = 10;
		int slen;
		printf ("%s%s\n", I.prompt, I.buffer.data);
		for (i=0; i<argc && argv[i]; i++) {
			int l = strlen (argv[i]);
			if ((sep+l)>col)
				col = sep+l;
			if (col>(cols>>1)) {
				col = (cols>>1);
				break;
			}
		}
		for (len=i=0; i<argc && argv[i]; i++) {
			slen = strlen (argv[i]);
			len += (slen>col)? (slen+sep): col+sep;
			if (len+col>cols) {
				printf ("\n");
				len = 0;
			}
			printf ("%-*s   ", col-sep, argv[i]);
		}
		printf ("\n");
	}
	fflush (stdout);
}

R_API char *r_line_readline() {
	return r_line_readline_cb (NULL, NULL);
}

R_API char *r_line_readline_cb(RLineReadCallback cb, void *user) {
	int columns = r_cons_get_size (NULL)-2;
	const char *gcomp_line = "";
	static int gcomp_idx = 0;
	static int gcomp = 0;
	signed char buf[10];
#if USE_UTF8
	int utflen;
#endif
	int ch, i; /* grep completion */
	char *tmp_ed_cmd, prev;

	I.buffer.index = I.buffer.length = 0;
	if (I.contents) {
		// XXX. control overflow
		strcpy (I.buffer.data, I.contents);
		I.buffer.index = I.buffer.length = strlen (I.contents);
	} else {
		I.buffer.data[0] = '\0';
	}
	if (I.disable) {
		I.buffer.data[0]='\0';
		if (!fgets (I.buffer.data, R_LINE_BUFSIZE-1, stdin))
			return NULL;
		I.buffer.data[strlen (I.buffer.data)] = '\0';
		return (*I.buffer.data)? I.buffer.data : r_line_nullstr;
	}

	memset (&buf, 0, sizeof buf);
	r_cons_set_raw (1);

	if (I.echo) {
		r_cons_clear_line ();
		printf ("\x1b[0K\r%s%s", I.prompt, I.buffer.data);
		fflush (stdout);
	}
	for (;;) {
#if 0
		if (I.echo) {
			printf("  (");
			for(i=1;i<argc;i++) {
				if (I.buffer.length==0||!strncmp(argv[i], I.buffer.data, I.buffer.length)) {
					len+=strlen(argv[i])+1;
					if (len+I.buffer.length+4 >= columns) break;
					printf("%s ", argv[i]);
				}
			}
			printf(")");
			fflush(stdout);
		}
#endif
		I.buffer.data[I.buffer.length]='\0';
		if (cb && !cb (user, I.buffer.data)) {
			I.buffer.data[0] = 0;
			I.buffer.length = 0;
		}
#if USE_UTF8
		utflen = r_line_readchar_utf8 (
			(ut8*)buf, sizeof (buf));
		if (utflen <1) return NULL;
		buf[utflen] = 0;
#else
		ch = r_line_readchar ();
		if (ch == -1) return NULL;
		buf[0] = ch;
#endif
		if (I.echo)
			r_cons_clear_line();
		columns = r_cons_get_size (NULL)-2;
		if (columns<1)
			columns = 40;
#if __WINDOWS__
		if (I.echo)
			printf ("\r%*c\r", columns, ' ');
#else
		if (I.echo)
			printf ("\r\x1b[2K\r"); //%*c\r", columns, ' ');
#endif
		switch (*buf) {
		//case -1: // ^D
		//	return NULL;
		case 0: // control-space
			/* ignore atm */
			break;
		case 1: // ^A
			I.buffer.index = 0;
			break;
		case 5: // ^E
		        if (prev == 24) { // ^X = 0x18
				I.buffer.data[I.buffer.length] = 0; // probably unnecessary
				tmp_ed_cmd = I.editor_cb (I.user, I.buffer.data);
				if (tmp_ed_cmd) {
					/* copied from yank (case 25) */ 
					I.buffer.length = strlen (tmp_ed_cmd);
					if (I.buffer.length < R_LINE_BUFSIZE) {
						I.buffer.index = I.buffer.length;
						// XXX may overflow here
						strcpy (I.buffer.data, tmp_ed_cmd);
					} else I.buffer.length -= strlen (tmp_ed_cmd);
					free (tmp_ed_cmd);
				}
			} else I.buffer.index = I.buffer.length;  
			break;
		case 3: // ^C 
			if (I.echo)
				eprintf ("^C\n");
			I.buffer.index = I.buffer.length = 0;
			*I.buffer.data = '\0';
			goto _end;
		case 4: // ^D
			if (!I.buffer.data[0]) { /* eof */
				if (I.echo)
					printf ("^D\n");
				r_cons_set_raw (R_FALSE);
				return NULL;
			}
			if (I.buffer.index<I.buffer.length)
				memmove (I.buffer.data+I.buffer.index,
					I.buffer.data+I.buffer.index+1,
					strlen (I.buffer.data+I.buffer.index+1)+1);
			break;
		case 10: // ^J -- ignore
			return I.buffer.data;
		case 11: // ^K -- ignore
			break;
		case 12: // ^L -- right
			I.buffer.index = (I.buffer.index<I.buffer.length)?
				I.buffer.index+1 : I.buffer.length;
			if (I.echo)
				eprintf ("\x1b[2J\x1b[0;0H");
			fflush (stdout);
			break;
		case 18: // ^R -- autocompletion
			gcomp = 1;
			break;
		case 19: // ^S -- backspace
			if (gcomp) gcomp--;
			else {
#if USE_UTF8
				if (I.buffer.index>0) {
					char *s;
					do {
						I.buffer.index--;
						s = I.buffer.data+I.buffer.index;
					} while ((*s & 0xc0) == 0x80);
				}
#else
				I.buffer.index = I.buffer.index? I.buffer.index-1: 0;
#endif
			}
			break;
		case 21: // ^U - cut
			free (I.clipboard);
			I.clipboard = strdup (I.buffer.data);
			I.buffer.data[0] = '\0';
			I.buffer.length = 0;
			I.buffer.index = 0;
			break;
		case 23: // ^W ^w
			if (I.buffer.index>0) {
				for (i=I.buffer.index-1; i>0&&I.buffer.data[i]==' '; i--);
				for (; i&&I.buffer.data[i]!=' '; i--);
				if (!i) for (; i>0&&I.buffer.data[i]==' '; i--);
				if (i>0) i++; else if (i<0) i=0;
				strcpy (I.buffer.data+i, I.buffer.data+I.buffer.index);
				I.buffer.length = strlen (I.buffer.data);
				I.buffer.index = i;
			}
			break;
		case 24: // ^X -- do nothing but store in prev = *buf
			break;
		case 25: // ^Y - paste
			if (I.clipboard != NULL) {
				I.buffer.length += strlen(I.clipboard);
				// TODO: support endless strings
				if (I.buffer.length < R_LINE_BUFSIZE) {
					I.buffer.index = I.buffer.length;
					strcat (I.buffer.data, I.clipboard);
				} else I.buffer.length -= strlen (I.clipboard);
			}
			break;
		case 14: // ^n
			if (gcomp) {
				if (gcomp_idx>0)
					gcomp_idx--;
			} else r_line_hist_down ();
			break;
		case 16: // ^p
			if (gcomp) {
				gcomp_idx++;
			} else r_line_hist_up ();
			break;
		case 27: //esc-5b-41-00-00
			buf[0] = r_line_readchar();
			if (buf[0] == -1)
				return NULL;
			buf[1] = r_line_readchar();
			if (buf[1] == -1)
				return NULL;
			if (buf[0]==0x5b) { // [
				switch (buf[1]) {
				case 0x33: // supr
					if (I.buffer.index<I.buffer.length)
						memmove (I.buffer.data+I.buffer.index,
							I.buffer.data+I.buffer.index+1,
							strlen (I.buffer.data+I.buffer.index+1)+1);
					buf[1] = r_line_readchar ();
					if (buf[1] == -1)
						return NULL;
					break;
				/* arrows */
				case 0x41:
					if (gcomp) gcomp_idx++;
					else if (r_line_hist_up ()==-1)
						return NULL;
					break;
				case 0x42:
					if (gcomp) {
						if (gcomp_idx>0)
							gcomp_idx--;
					} else if (r_line_hist_down ()==-1)
						return NULL;
					break;
				case 0x43: // C --> right arrow
#if USE_UTF8
					{
						char *s = I.buffer.data+I.buffer.index+1;
						utflen = 1;
						while ((*s & 0xc0) == 0x80) {
							utflen++;
							s++;
						}
						I.buffer.index = I.buffer.index<I.buffer.length?
							I.buffer.index+utflen: I.buffer.length;
					}
#else
					I.buffer.index = I.buffer.index<I.buffer.length?
						I.buffer.index+1: I.buffer.length;
#endif
					break;
				case 0x44: // D --> left arrow
#if USE_UTF8
					{
						char *s = I.buffer.data+I.buffer.index-1;
						utflen = 1;
						while (s>I.buffer.data && (*s & 0xc0) == 0x80) {
							utflen++;
							s--;
						}
					}
					I.buffer.index = I.buffer.index? I.buffer.index-utflen: 0;
#else
					I.buffer.index = I.buffer.index? I.buffer.index-1: 0;
#endif
					break;
				case 0x31: // control + arrow
					r_cons_readchar ();
					r_cons_readchar ();
					ch = r_cons_readchar ();
					switch (ch) {
					case 0x41:
						//first
						I.buffer.index = 0;
						break;
					case 0x44:
						// previous word
						for (i=I.buffer.index; i>0; i--) {
							if (I.buffer.data[i] == ' ') {
								I.buffer.index = i-1;
								break;
							}
						}
						if (I.buffer.data[i] != ' ')
							I.buffer.index = 0;
						break;
					case 0x42:
						//end
						I.buffer.index = I.buffer.length;
						break;
					case 0x43:
						// next word
						for (i=I.buffer.index; i<I.buffer.length; i++) {
							if (I.buffer.data[i] == ' ') {
								I.buffer.index = i+1;
								break;
							}
						}
						if (I.buffer.data[i] != ' ')
							I.buffer.index = I.buffer.length;
						break;
					}
					r_cons_set_raw (1);
					break;
				case 0x37: // HOME xrvt-unicode
					r_cons_readchar ();
				case 0x48: // HOME
					I.buffer.index = 0;
					break;
				case 0x34: // END
				case 0x38: // END xrvt-unicode
					r_cons_readchar ();
				case 0x46: // END
					I.buffer.index = I.buffer.length;
					break;
				}
			}
			break;
		case 8:
		case 127:
			if (I.buffer.index < I.buffer.length) {
				if (I.buffer.index>0) {
					int len = 0;
					// TODO: WIP
#if USE_UTF8
					char *s;
					do {
						I.buffer.index--;
						s = I.buffer.data+I.buffer.index;
						len++;
					} while ((*s &0xc0)==0x80);
#else
					len = 1;
					I.buffer.index--;
#endif
					memmove (I.buffer.data+I.buffer.index,
						I.buffer.data+I.buffer.index+len,
						strlen (I.buffer.data+I.buffer.index));
					I.buffer.length -= len;
					I.buffer.data[I.buffer.length] = 0;
				}
			} else {
// OK
#if USE_UTF8
				char *s;
				// utf8 backward size
				do {
					I.buffer.length--;
					s = I.buffer.data+I.buffer.length;
					i++;
				} while ((*s &0xc0)==0x80);
				I.buffer.index = I.buffer.length;
#else
				I.buffer.index = --I.buffer.length;
#endif
				if (I.buffer.length<0) I.buffer.length=0;
				I.buffer.data[I.buffer.length]='\0';
			}
			if (I.buffer.index<0)
				I.buffer.index = 0;
			break;
		case 9: // tab
			r_line_autocomplete ();
			break;
		case 13:
			if (gcomp && I.buffer.length>0) {
				// XXX overflow
				strcpy (I.buffer.data, gcomp_line);
				I.buffer.length = strlen (gcomp_line);
			}
			gcomp_idx = gcomp = 0;
			goto _end;
#if 0
			// force command fit
			for(i=1;i<argc;i++) {
				if (I.buffer.length==0 || !strncmp(argv[i], I.buffer.data, I.buffer.length)) {
					printf("%*c", columns, ' ');
					printf("\r");
					printf("\n\n(%s)\n\n", I.buffer.data);
					r_cons_set_raw(0);
					return I.buffer.data;
				}
			}
#endif
		default:
			if (gcomp)
				gcomp++;
			if (I.buffer.index<I.buffer.length) {
#if USE_UTF8
				I.buffer.length += utflen;
				for (i = I.buffer.length; i>I.buffer.index; i--)
					I.buffer.data[i] = I.buffer.data[i-utflen];
				memcpy (I.buffer.data+I.buffer.index, buf, utflen);
#else
				for (i = ++I.buffer.length; i>I.buffer.index; i--)
					I.buffer.data[i] = I.buffer.data[i-1];
				I.buffer.data[I.buffer.index] = buf[0];
#endif
			} else {
#if USE_UTF8
				memcpy (I.buffer.data+I.buffer.length, buf, utflen);
				I.buffer.length+=utflen;
#if 0
				if (I.buffer.length>(R_LINE_BUFSIZE-1))
					I.buffer.length--;
#endif
				I.buffer.data[I.buffer.length]='\0';
#else
				I.buffer.data[I.buffer.length]=buf[0];
				I.buffer.length++;
				if (I.buffer.length>(R_LINE_BUFSIZE-1))
					I.buffer.length--;
				I.buffer.data[I.buffer.length]='\0';
#endif
			}
#if USE_UTF8
			I.buffer.index += utflen;
#else
			I.buffer.index++;
#endif
			break;
		}
		prev = buf[0];
		if (I.echo) {
			if (gcomp) {
				gcomp_line = "";
				//if (I.buffer.length == 0)
				//	gcomp = 0;
				if (I.history.data != NULL)
				for (i=0; i<I.history.size; i++) {
					if (I.history.data[i] == NULL)
						break;
					if (strstr (I.history.data[i], I.buffer.data)) {
						gcomp_line = I.history.data[i];
						if (!gcomp_idx--)
							break;
					}
				}
				printf ("\r (reverse-i-search (%s)): %s\r", I.buffer.data, gcomp_line);
			} else {
				int chars = R_MAX (1, strlen (I.buffer.data)); // wtf?
				int cols = R_MAX (1, columns - r_str_ansi_len (I.prompt)-2);
				/* print line */
				printf ("\r%s", I.prompt);
				fwrite (I.buffer.data, 1, R_MIN (cols, chars), stdout);
				/* place cursor */
				printf ("\r%s", I.prompt);
				if (I.buffer.index>cols) {
					printf ("< ");
					i = I.buffer.index-cols;
				} else i=0;
				fwrite (I.buffer.data+i, 1, I.buffer.index, stdout);
			}
			fflush (stdout);
		}
	}
_end:
	r_cons_set_raw (0);
	if (I.echo) {
		printf ("\r%s%s\n", I.prompt, I.buffer.data);
		fflush (stdout);
	}

	// should be here or not?
	if (!memcmp (I.buffer.data, "!history", 8)) {
	//if (I.buffer.data[0]=='!' && I.buffer.data[1]=='\0') {
		r_line_hist_list ();
		return r_line_nullstr;
	}
	return I.buffer.data? I.buffer.data : r_line_nullstr;
}
