/* rdline.c
 *
 * Copyright (c) 1992-2016 by Mike Gleason.
 * All rights reserved.
 *
 * Note: It should still be simple to backport the old GNU Readline
 * support in here.  Feel free to do that if you hate NcFTP's built-in
 * implementation.
 * 
 */

#include "syshdrs.h"
#ifdef PRAGMA_HDRSTOP
#	pragma hdrstop
#endif

#include "shell.h"
#include "util.h"
#include "bookmark.h"
#include "cmds.h"
#include "pref.h"
#include "ls.h"
#include "readln.h"
#include "gl_getline.h"

const char *tcap_normal = "";
const char *tcap_boldface = "";
const char *tcap_underline = "";
const char *tcap_reverse = "";
const char *gTerm;
int gXterm;
int gXtermTitle;	/* Idea by forsberg@lysator.liu.se */
char gCurXtermTitleStr[256];

#if ( (defined(WIN32) || defined(_WINDOWS)) && !defined(__CYGWIN__) ) && defined(_CONSOLE)
	char gSavedConsoleTitle[64];
	static WORD GetConsoleTextAttribute(HANDLE Console)
	{
		CONSOLE_SCREEN_BUFFER_INFO	ConsoleInfo;
		GetConsoleScreenBufferInfo(Console, &ConsoleInfo);
		return ConsoleInfo.wAttributes;
	}
#endif

extern int gMaySetXtermTitle;
extern LsCacheItem gLsCache[kLsCacheSize];
extern FTPConnectionInfo gConn;
extern char gRemoteCWD[512];
extern char gOurDirectoryPath[];
extern const char gVersion[];
extern int gNumBookmarks;
extern BookmarkPtr gBookmarkTable;
extern PrefOpt gPrefOpts[];
extern int gNumPrefOpts;
extern int gScreenColumns;
extern int gIsTTYr;
extern int gUid;
extern int gServerUsesMSDOSPaths;




void
GetScreenColumns(void)
{
#if (defined(WIN32) || defined(_WINDOWS)) && !defined(__CYGWIN__)
	CONSOLE_SCREEN_BUFFER_INFO csbi;

	if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi)) {
		gScreenColumns = (int) csbi.dwSize.X;
		if (gScreenColumns < 80)
			gScreenColumns = 80;
	}
#else	/* Unix */
#ifdef BINDIR
	char ncftpbookmarks[256];
	FILE *infp;
	sigproc_t osigpipe;
	int columns;
#endif	/* BINDIR */
	char *cp;

	if ((cp = (char *) getenv("COLUMNS")) == NULL) {
		gScreenColumns = 80;
	} else {
		gScreenColumns = atoi(cp);
		return;
	}

#ifdef TIOCGWINSZ
	{
		struct winsize felix;

		memset(&felix, 0, sizeof(felix));
		if (ioctl(0, TIOCGWINSZ, &felix) == 0) {
			columns = felix.ws_col;
			if ((columns > 0) && (columns < GL_BUF_SIZE))
				gScreenColumns = columns;
			else
				gScreenColumns = 80;
			return;
		}
	}
#endif

#ifdef BINDIR
	/* Don't run things as root unless really necessary. */
	if (gUid == 0)
		return;

	/* This is a brutal hack where we've hacked a
	 * special command line option into ncftp_bookmarks
	 * (which is linked with curses) so that it computes
	 * the screen size and prints it to stdout.
	 *
	 * This function runs ncftp_bookmarks and gets
	 * that information.  The reason we do this is that
	 * we may or may not have a sane installation of
	 * curses/termcap, and we don't want to increase
	 * NcFTP's complexity by the curses junk just to
	 * get the screen size.  Instead, we delegate this
	 * to ncftp_bookmarks which already deals with the
	 * ugliness of curses.
	 */

	STRNCPY(ncftpbookmarks, BINDIR);
	STRNCAT(ncftpbookmarks, "/");
	STRNCAT(ncftpbookmarks, "ncftpbookmarks");

	if (access(ncftpbookmarks, X_OK) < 0)
		return;

	STRNCAT(ncftpbookmarks, " --dimensions-terse");

	osigpipe = NcSignal(SIGPIPE, SIG_IGN);
	infp = popen(ncftpbookmarks, "r");
	if (infp != NULL) {
		if (fscanf(infp, "%d", &columns) != 1)
			columns = 0;
		while (getc(infp) != EOF) {}
		(void) pclose(infp);

		if ((columns > 0) && (columns < GL_BUF_SIZE))
			gScreenColumns = columns;
	}
	(void) NcSignal(SIGPIPE, (sigproc_t) osigpipe);
#endif	/* BINDIR */
#endif	/* Windows */
}	/* GetScreenColumns */



/* For a few selected terminal types, we'll print in boldface, etc.
 * This isn't too important, though.
 */
void
InitTermcap(void)
{
#if ( (defined(WIN32) || defined(_WINDOWS)) && !defined(__CYGWIN__) ) && defined(_CONSOLE)
	gXterm = gXtermTitle = 0;
	gCurXtermTitleStr[0] = '\0';

	tcap_normal = "\033[0m";       /* Default ANSI escapes */
	tcap_boldface = "\033[1m";
	tcap_underline = "";
	tcap_reverse = "";

	gTerm = "MS-DOS Prompt";
	ZeroMemory(gSavedConsoleTitle, (DWORD) sizeof(gSavedConsoleTitle));
	GetConsoleTitle(gSavedConsoleTitle, (DWORD) sizeof(gSavedConsoleTitle) - 1);
	SetConsoleTitle("NcFTP");
	gXterm = gXtermTitle = 1;
#else
	const char *term;
	
	gXterm = gXtermTitle = 0;
	gCurXtermTitleStr[0] = '\0';

	if ((gTerm = getenv("TERM")) == NULL) {
		tcap_normal = "";
		tcap_boldface = "";
		tcap_underline = "";
		tcap_reverse = "";
		return;
	}

	term = gTerm;
	if (	(strstr(term, "xterm") != NULL) ||
		(strstr(term, "rxvt") != NULL) ||
		(strstr(term, "dtterm") != NULL) ||
		(ISTRCMP(term, "scoterm") == 0)
	) {
		gXterm = gXtermTitle = 1;
	}

	if (	(gXterm != 0) ||
		(strcmp(term, "vt100") == 0) ||
		(strcmp(term, "linux") == 0) ||
		(strcmp(term, "vt220") == 0) ||
		(strcmp(term, "cons25") == 0) ||
		(strcmp(term, "vt102") == 0)
	) {
		tcap_normal = "\033[0m";       /* Default ANSI escapes */
		tcap_boldface = "\033[1m";
		tcap_underline = "\033[4m";
		tcap_reverse = "\033[7m";
	} else {
		tcap_normal = "";
		tcap_boldface = "";
		tcap_underline = "";
		tcap_reverse = "";
	}
#endif
}	/* InitTermcap */




static char *
FindStartOfCurrentCommand(void)
{
	char *scp;
	char *start;
	int qc;

	for (scp = gl_buf;;) {
		start = scp;
		for (;;) {
			if (*scp == '\0')
				goto done;
			if (!isspace((int) *scp))
				break;
			scp++;
		}
		start = scp;

		for (;;) {
			if (*scp == '\0') {
				goto done;
			} else if ((*scp == '"') || (*scp == '\'')) {
				qc = *scp++;

				for (;;) {
					if (*scp == '\0') {
						goto done;
					} else if (*scp == '\\') {
						scp++;
						if (*scp == '\0')
							goto done;
						scp++;
					} else if (*scp == qc) {
						scp++;
						break;
					} else {
						scp++;
					}
				}
			} else if (*scp == '\\') {
				scp++;
				if (*scp == '\0')
					goto done;
				scp++;
			} else if ((*scp == ';') || (*scp == '\n')) {
				/* command ended */
				scp++;
				if (*scp == '\0')
					goto done;
				break;
			} else {
				scp++;
			}
		}
	}
done:
	return (start);
}	/* FindStartOfCurrentCommand */



static FTPFileInfoListPtr
GetLsCacheFileList(const char *const item)
{
	int ci;
	int sortBy;
	int sortOrder;
	FTPFileInfoListPtr filp;

	ci = LsCacheLookup(item);
	if (ci < 0) {
		/* This dir was not in the
		 * cache -- go get it.
		 */
		Ls(item, 'l', "", NULL);
		ci = LsCacheLookup(item);
		if (ci < 0)
			return NULL;
	}

	sortBy = 'n';		/* Sort by filename. */
	sortOrder = 'a';	/* Sort in ascending order. */
	filp = &gLsCache[ci].fil;
	SortFileInfoList(filp, sortBy, sortOrder);
	return filp;
}	/* GetLsCacheFileList */




static char *
RemoteCompletionFunction(const char *text, int state, int fTypeFilter)
{
	char rpath[256];
	char *cp;
	char *cp2;
	const char *textbasename;
	int fType;
	FTPFileInfoPtr diritemp;
	FTPFileInfoListPtr filp;
	int textdirlen;
	size_t tbnlen;
	size_t flen, mlen;
	static FTPFileInfoVec diritemv;
	static int i;

	textbasename = strrchr(text, '/');
	if (textbasename == NULL) {
		textbasename = text;
		textdirlen = -1;
	} else {
		textdirlen = (int) (textbasename - text);
		textbasename++;
	}
	tbnlen = strlen(textbasename);

	if (state == 0) {
		if (text[0] == '\0') {
			/* Special case when they do "get <TAB><TAB> " */
			STRNCPY(rpath, gRemoteCWD);
		} else {
			PathCat(rpath, sizeof(rpath), gRemoteCWD, text, gServerUsesMSDOSPaths);
			if (text[strlen(text) - 1] == '/') {
				/* Special case when they do "get /dir1/dir2/<TAB><TAB>" */
				STRNCAT(rpath, "/");
			}
			cp2 = strrchr(rpath, '/');
			if (cp2 == NULL) {
				return NULL;
			} else if (cp2 == rpath) {
				/* Item in root directory. */
				cp2++;
			}
			*cp2 = '\0';
		}

		filp = GetLsCacheFileList(rpath);
		if (filp == NULL)
			return NULL;

		diritemv = filp->vec;
		if (diritemv == NULL)
			return NULL;

		i = 0;
	}

	for ( ; ; ) {
		diritemp = diritemv[i];
		if (diritemp == NULL)
			break;

		i++;
		fType = (int) diritemp->type;
		if ((fTypeFilter == 0) || (fType == fTypeFilter) || (fType == /* symlink */ 'l')) {
			if (strncmp(textbasename, diritemp->relname, tbnlen) == 0) {
				flen = strlen(diritemp->relname);
				if (textdirlen < 0) {
					mlen = flen + 2;
					cp = (char *) malloc(mlen);
					if (cp == NULL)
						return (NULL);
					(void) memcpy(cp, diritemp->relname, mlen);
				} else {
					mlen = (size_t) textdirlen + 1 + flen + 2;
					cp = (char *) malloc(mlen);
					if (cp == NULL)
						return (NULL);
					(void) memcpy(cp, text, (size_t) textdirlen);
					cp[textdirlen] = '/';
					(void) strcpy(cp + textdirlen + 1, diritemp->relname);
				}
				if (fType == 'd') {
					gl_completion_exact_match_extra_char = '/';
				} else {
					gl_completion_exact_match_extra_char = ' ';
				}
				return cp;
			}
		}
	}
	return NULL;
}	/* RemoteCompletionFunction */




static char *
RemoteFileCompletionFunction(const char *text, int state)
{
	char *cp;

	cp = RemoteCompletionFunction(text, state, 0);
	return cp;
}	/* RemoteFileCompletionFunction */




static char *
RemoteDirCompletionFunction(const char *text, int state)
{
	char *cp;

	cp = RemoteCompletionFunction(text, state, 'd');
	return cp;
}	/* RemoteDirCompletionFunction */




static char *
BookmarkCompletionFunction(const char *text, int state)
{
	char *cp;
	size_t textlen;
	int i, matches;

	if ((gBookmarkTable == NULL) || (state >= gNumBookmarks))
		return (NULL);

	textlen = strlen(text);
	if (textlen == 0) {
		cp = StrDup(gBookmarkTable[state].bookmarkName);
	} else {
		cp = NULL;
		for (i=0, matches=0; i<gNumBookmarks; i++) {
			if (ISTRNCMP(gBookmarkTable[i].bookmarkName, text, textlen) == 0) {
				if (matches >= state) {
					cp = StrDup(gBookmarkTable[i].bookmarkName);
					break;
				}
				matches++;
			}
		}
	}
	return cp;
}	/* BookmarkCompletionFunction */




static char *
CommandCompletionFunction(const char *text, int state)
{
	char *cp;
	size_t textlen;
	int i, matches;
	CommandPtr cmdp;

	textlen = strlen(text);
	if (textlen == 0) {
		cp = NULL;
	} else {
		cp = NULL;
		for (i=0, matches=0; ; i++) {
			cmdp = GetCommandByIndex(i);
			if (cmdp == kNoCommand)
				break;
			if (ISTRNCMP(cmdp->name, text, textlen) == 0) {
				if (matches >= state) {
					cp = StrDup(cmdp->name);
					break;
				}
				matches++;
			}
		}
	}
	return cp;
}	/* CommandCompletionFunction */




static char *
PrefOptCompletionFunction(const char *text, int state)
{
	char *cp;
	size_t textlen;
	int i, matches;

	if (state >= gNumPrefOpts)
		return (NULL);

	textlen = strlen(text);
	if (textlen == 0) {
		cp = StrDup(gPrefOpts[state].varname);
	} else {
		cp = NULL;
		for (i=0, matches=0; i<gNumPrefOpts; i++) {
			if (ISTRNCMP(gPrefOpts[i].varname, text, textlen) == 0) {
				if (matches >= state) {
					cp = StrDup(gPrefOpts[i].varname);
					break;
				}
				matches++;
			}
		}
	}
	return cp;
}	/* PrefOptCompletionFunction */




void
ReCacheBookmarks(void)
{
	(void) LoadBookmarkTable();
}



static int
HaveCommandNameOnly(char *cmdstart)
{
	char *cp;
	for (cp = cmdstart; *cp != '\0'; cp++) {
		if (isspace((int) *cp))
			return (0);	/* At least one argument in progress. */
	}
	return (1);
}	/* HaveCommandNameOnly */




static char *
CompletionFunction(const char *text, int state)
{
	char *cp;
	char *cmdstart;
	ArgvInfo ai;
	int bUsed;
	CommandPtr cmdp;
	static int flags;

	if (state == 0) {
		flags = -1;
		cmdstart = FindStartOfCurrentCommand();
		if (cmdstart == NULL)
			return NULL;
		if (HaveCommandNameOnly(cmdstart)) {
			flags = -2;	/* special case */
			cp = CommandCompletionFunction(text, state);
			return cp;
		}

		(void) memset(&ai, 0, sizeof(ai));
		bUsed = MakeArgv(cmdstart, &ai.cargc, ai.cargv,
			(int) (sizeof(ai.cargv) / sizeof(char *)),
			ai.argbuf, sizeof(ai.argbuf),
			ai.noglobargv, 1, 1);
		if (bUsed <= 0)
			return NULL;
		if (ai.cargc == 0)
			return NULL;

		cmdp = GetCommandByName(ai.cargv[0], 0);
		if (cmdp == kAmbiguousCommand) {
			return NULL;
		} else if (cmdp == kNoCommand) {
			return NULL;
		}
		flags = cmdp->flags;
	}
	if (flags == (-2)) {
		cp = CommandCompletionFunction(text, state);
		return cp;
	}
	if (flags < 0)
		return NULL;
	if ((flags & (kCompleteLocalFile|kCompleteLocalDir)) != 0) {
		cp = gl_local_filename_completion_proc(text, state);
		return cp;
	} else if ((flags & kCompleteRemoteFile) != 0) {
		gl_filename_quoting_desired = 1;
		cp = RemoteFileCompletionFunction(text, state);
		return cp;
	} else if ((flags & kCompleteRemoteDir) != 0) {
		gl_filename_quoting_desired = 1;
		cp = RemoteDirCompletionFunction(text, state);
		return cp;
	} else if ((flags & kCompleteBookmark) != 0) {
		gl_filename_quoting_desired = 1;
		cp = BookmarkCompletionFunction(text, state);
		return cp;
	} else if ((flags & kCompletePrefOpt) != 0) {
		cp = PrefOptCompletionFunction(text, state);
		return cp;
	}	
	return NULL;
}	/* CompletionFunction */




void
LoadHistory(void)
{
	char pathName[256];

	if (gOurDirectoryPath[0] == '\0')
		return;
	(void) OurDirectoryPath(pathName, sizeof(pathName), kHistoryFileName);

	gl_histloadfile(pathName);
}	/* LoadHistory */



static size_t
Vt100VisibleStrlen(const char *src)
{
	const char *cp;
	size_t esc;

	for (esc = 0, cp = src; *cp != '\0'; cp++) {
		if (*cp == '\033')
			esc++;
	}

	/* The VT100 escape codes we use are all in the form "\033[7m"
	 * These aren't visible, so subtract them from the count.
	 */
	return ((size_t) (cp - src) - (esc * 4));
}	/* Vt100VisibleStrlen */



/* Save the commands they typed in a history file, then they can use
 * readline to go through them again next time.
 */
void
SaveHistory(void)
{
	char pathName[256];

	if (gOurDirectoryPath[0] == '\0')
		return;		/* Don't create in root directory. */
	(void) OurDirectoryPath(pathName, sizeof(pathName), kHistoryFileName);

	gl_histsavefile(pathName);
#if (defined(WIN32) || defined(_WINDOWS)) && !defined(__CYGWIN__)
#else
	(void) chmod(pathName, 00600);
#endif
}	/* SaveHistory */




void
InitReadline(void)
{
	gl_completion_proc = CompletionFunction;
	gl_setwidth(gScreenColumns);
	gl_strlen = Vt100VisibleStrlen;
	LoadHistory();
	ReCacheBookmarks();
}	/* InitReadline */




char *
Readline(char *prompt)
{
	char *linecopy, *line, *cp;
	static char *lbuf = NULL;

	forever {
		if (gIsTTYr) {
			line = gl_getline(prompt);
		} else {
			if (lbuf == NULL) {
				lbuf = calloc((size_t) 512, (size_t) 1);
				if (lbuf == NULL)
					return NULL;
			}
			line = fgets(lbuf, (size_t) (512 - 1), stdin);
			if (line != NULL) {
				cp = line + strlen(line) - 1;
				if (*cp == '\n')
					*cp = '\0';
			}
		}

		if (line == NULL)
			return NULL;	/* fgets EOF */

		/* If we have a non-empty line, we're done. */
		if (line[0] != '\0')
			break;

		if (gl_get_result() == GL_EOF)
			return NULL;	/* gl_getline EOF */

		/* Otherwise a signal was received.  Start over. */
	}

	linecopy = StrDup(line);
	line = linecopy;
	return (line);
}	/* Readline */



void
AddHistory(const char *const line)
{
	gl_histadd(line);
}	/* AddHistory */



void
DisposeReadline(void)
{
	SaveHistory();
	gl_dispose();
}	/* DisposeReadline */





/*VARARGS*/
void
SetXtermTitle(const char *const fmt, ...)
{
	va_list ap;
	char buf[256];

	if ((gXtermTitle != 0) && (gMaySetXtermTitle != 0)) {
		if ((fmt == NULL) || (ISTRCMP(fmt, "RESTORE") == 0)) {
#if ( (defined(WIN32) || defined(_WINDOWS)) && !defined(__CYGWIN__) ) && defined(_CONSOLE)
			STRNCPY(buf, gSavedConsoleTitle);
#else
			STRNCPY(buf, gTerm);
#endif
		} else if (ISTRCMP(fmt, "DEFAULT") == 0) {
			(void) Strncpy(buf, gVersion + 5, 12);
		} else {
			va_start(ap, fmt);
#ifdef HAVE_VSNPRINTF
			(void) vsnprintf(buf, sizeof(buf) - 1, fmt, ap);
			buf[sizeof(buf) - 1] = '\0';
#else
			(void) vsprintf(buf, fmt, ap);
#endif
			va_end(ap);
		}
		if (buf[0] != '\0') {
#if ( (defined(WIN32) || defined(_WINDOWS)) && !defined(__CYGWIN__) ) && defined(_CONSOLE)
			SetConsoleTitle(buf);
#else
			if (strcmp(gCurXtermTitleStr, buf) != 0) {
				fprintf(stderr, "\033]0;%s\007", buf);
				STRNCPY(gCurXtermTitleStr, buf);
			}
#endif
		}
	}
}	/* SetXtermTitle */




void
PrintStartupBanner(void)
{
	char v[80], *cp;
	char vdate[32];

	/* Print selected information from the version ID. */
	vdate[0] = '\0';
	(void) STRNCPY(v, gVersion + 5);
	cp = strchr(v, ',');
	if (cp != NULL) {
		*cp = '\0';
		cp[-5] = '\0';
		(void) STRNCPY(vdate, " (");
		(void) STRNCAT(vdate, v + 16);
		(void) STRNCAT(vdate, ", ");
		(void) STRNCAT(vdate, cp - 4);
		(void) STRNCAT(vdate, ")");
	}

#if ( (defined(WIN32) || defined(_WINDOWS)) && !defined(__CYGWIN__) ) && defined(_CONSOLE)
	{
		HANDLE Console = GetStdHandle(STD_OUTPUT_HANDLE);
		WORD Attribute = GetConsoleTextAttribute(Console);
		Attribute |= FOREGROUND_INTENSITY;
		SetConsoleTextAttribute(Console, Attribute);
#if defined(BETA) && (BETA > 0)
		(void) fprintf(stdout, "%.11s beta %d",
			gVersion + 5,
			BETA
		);
#else
		(void) fprintf(stdout, "%.11s",
			gVersion + 5
		);
#endif
		Attribute &= ~FOREGROUND_INTENSITY;
		SetConsoleTextAttribute(Console, Attribute);
		(void) fprintf(stdout, "%s by Mike Gleason (http://www.NcFTP.com/contact/).\n",
			vdate
		);
	}
#else
#if defined(BETA) && (BETA > 0)
	(void) fprintf(stdout, "%s%.11s beta %d%s%s by Mike Gleason (http://www.NcFTP.com/contact/).\n",
		tcap_boldface,
		gVersion + 5,
		BETA,
		tcap_normal,
		vdate
	);
#else
	(void) fprintf(stdout, "%s%.11s%s%s by Mike Gleason (http://www.NcFTP.com/contact/).\n",
		tcap_boldface,
		gVersion + 5,
		tcap_normal,
		vdate
	);
#endif
#endif
	(void) fflush(stdout);
}	/* PrintStartupBanner */




/* Print the command shell's prompt. */
void
MakePrompt(char *dst, size_t dsize)
{
	char acwd[64];

#	ifdef HAVE_SNPRINTF
	if (gConn.loggedIn != 0) {
		AbbrevStr(acwd, gRemoteCWD, 25, 0);
		snprintf(dst, dsize, "%sncftp%s %s %s>%s ",
			tcap_boldface, tcap_normal, acwd,
			tcap_boldface, tcap_normal);
	} else {
		snprintf(dst, dsize, "%sncftp%s> ",
			tcap_boldface, tcap_normal);
	}
#	else	/* HAVE_SNPRINTF */
	(void) Strncpy(dst, tcap_boldface, dsize);
	(void) Strncat(dst, "ncftp", dsize);
	(void) Strncat(dst, tcap_normal, dsize);
	if (gConn.loggedIn != 0) {
		AbbrevStr(acwd, gRemoteCWD, 25, 0);
		(void) Strncat(dst, " ", dsize);
		(void) Strncat(dst, acwd, dsize);
		(void) Strncat(dst, " ", dsize);
	}
	(void) Strncat(dst, tcap_boldface, dsize);
	(void) Strncat(dst, ">", dsize);
	(void) Strncat(dst, tcap_normal, dsize);
	(void) Strncat(dst, " ", dsize);
#	endif	/* HAVE_SNPRINTF */
}	/* MakePrompt */
