/* wgets.c
 *
 * Copyright (c) 1993-2005 Mike Gleason, NcFTP Software.
 * All rights reserved.
 *
 */


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

#include "wgets.h"
#define MEMMOVE memmove

/* Pointer to current position in the buffer. */
static char *gBufPtr;

/* When we draw the display window in the buffer, this is the first
 * character to draw, at the far left.
 *
 * The "display window" is the current area being edited by the user.
 * For example, you specify that you can only use 10 screen columns to
 * input a string that can have a maximum size of 30 characters.
 *
 * Let's say the current buffer has "abcdefghijklmnopqrstuvw" in it.
 *     abcdefghijklmnopqrstuvw
 *          ^  ^     ^
 *          s  c     e
 *
 * The "display window" for this example would be "fghijklmno"
 * <s> (gWinStartPtr) points to "f" and <e> (gWinEndPtr) points to "o".
 * <c> (gBufPtr) points to the current character under the cursor, so
 * the letter "i" would be the hilited/blinking cursor.
 *
 * This display window allows you to set aside a certain amount of screen
 * area for editing, but allowing for longer input strings.  The display
 * window will scroll as needed.
 */
static char *gWinStartPtr = 0;

/* This would be the last character drawn in the display window. */
static char *gWinEndPtr = 0;

/* Number of characters in the buffer. */
static size_t gBufLen = 0;

/* If true, the display window needs to be redrawn. */
static int gNeedUpdate = 0;

/* The curses library window we are doing the editing in.  This window
 * is different from what I call the "display window."   The display
 * window is a subregion of the curses window, and does not have to have
 * a separate WINDOW pointer just for the editing.
 */
static WINDOW *gW = 0;

/* The column and row where the display window starts. */
static int gSy = 0, gSx = 0;

/* This is the buffer for the characters typed. */
static char *gDst = 0;

/* This is the length of the display window on screen.  It should <=
 * the size of the buffer itself.
 */
static int gWindowWidth = 0;

/* This is the size of the buffer. */
static size_t gDstSize = 0;

/* This flag tells whether we are allowed to use the contents of the buffer
 * passed by the caller, and whether the contents had length 1 or more.
 */
static int gHadStartingString = 0;

/* This is a flag to tell if the user did any editing.  If any characters
 * are added or deleted, this flag will be set.  If the user just used the
 * arrow keys to move around and/or just hit return, it will be false.
 */
static int gChanged = 0;

/* This is a flag to tell if we have moved at all on the line before
 * hitting return.  This is mostly used for ^D handling.  We want ^D to
 * return EOF if they hit right it away on a new line.
 */
static int gMoved = 0;

/* We have the flexibility with respect to echoing characters. We can just
 * echo the same character we read back to the screen like normal, always
 * echo "bullets," or not echo at all.
 */
static int gEchoMode = 0;

/* You can specify that the routine maintain a history buffer. If so, then
 * the user can use the arrow keys to move up and down through the history
 * to edit previous lines.
 */
static FTPLineListPtr gHistory = 0;

/* This is a pointer to the line in the history that is being used as a copy
 * for editing.
 */
static FTPLinePtr gCurHistLine = 0;

static void
wg_SetCursorPos(char *newPos)
{
	if (newPos > gWinEndPtr) {
		/* Shift window right.
		 * (Text will appear to shift to the left.)
		 */
		gWinStartPtr = newPos;
		if (gWindowWidth > 7)
			gWinStartPtr -= gWindowWidth * 2 / 10;
		else if (gWindowWidth > 1)
			gWinStartPtr -= 1;
		gBufPtr = newPos;
		gWinEndPtr = gWinStartPtr + gWindowWidth - 1;
	} else if (newPos < gWinStartPtr) {
		/* Shift window left.
		 * (Text will appear to shift to the right.)
		 */
		gWinStartPtr = newPos;
		if (gWindowWidth > 7)
			gWinStartPtr -= gWindowWidth * 2 / 10;
		else if (gWindowWidth > 1)
			gWinStartPtr -= 1;
		if (gWinStartPtr < gDst)
			gWinStartPtr = gDst;
		gBufPtr = newPos;
		gWinEndPtr = gWinStartPtr + gWindowWidth - 1;
	} else {
		/* Can just move cursor without shifting window. */
		gBufPtr = newPos;
	}
}	/* wg_SetCursorPos */




static void
wg_AddCh(int c)
{
	size_t n;
	char *limit;

	if (gBufLen < gDstSize) {
		limit = gDst + gBufLen;
		if (gBufPtr == limit) {
			/* Just add a character to the end.  No need to do
			 * a memory move for this.
			 */
			*gBufPtr = c;
			gBufLen++;
			wg_SetCursorPos(gBufPtr + 1);
		} else {
			/* Have to move characters after the cursor over one
			 * position so we can insert a character.
			 */
			n = limit - gBufPtr;
			MEMMOVE(gBufPtr + 1, gBufPtr, n);
			*gBufPtr = c;
			gBufLen++;
			wg_SetCursorPos(gBufPtr + 1);
		}
		gNeedUpdate = 1;
		gChanged = 1;
	} else {
		BEEP(1);
	}
}	/* wg_AddCh */




static void
wg_KillCh(int count)
{
	size_t n;
	char *limit;

	if (count > gBufPtr - gDst)
		count = (int) (gBufPtr - gDst);
	if (count) {
		limit = gDst + gBufLen;
		if (gBufPtr != limit) {
			/* Delete the characters before the character under the
			 * cursor, and move everything after it back one.
			 */
			n = limit - gBufPtr;
			memcpy(gBufPtr - count, gBufPtr, n);
		}
		gBufLen -= count;
		wg_SetCursorPos(gBufPtr - count);	/* Does a --gBufPtr. */
		gNeedUpdate = 1;
		gChanged = 1;
	} else {
		BEEP(1);
	}
}	/* wg_KillCh */

static int
IsWordChar(char c)
{
	return ((!isspace((int) c)) && (c != '/'));
}

static void
wg_KillWord(void)
{
	int count;
	int off = (int) (gBufPtr - gDst) - 1;
	count = off;

	/* Find the end of the previous word */
	while (off >= 0 && !IsWordChar(gDst[off]))
		off--;
	/* Find the start of the word */
	while (off >= 0 && IsWordChar(gDst[off]))
		off--;
	count = count - off;
	wg_KillCh(count);
}	/* wg_KillWord */


static void
wg_ForwardKillCh(void)
{
	size_t n;
	char *limit;

	if (gBufLen != 0) {
		limit = gDst + gBufLen;
		if (gBufPtr == limit) {
			/* Nothing in front to delete. */
			BEEP(1);
		} else {
			n = limit - gBufPtr - 1;
			memcpy(gBufPtr, gBufPtr + 1, n);
			--gBufLen;
			gNeedUpdate = 1;
			gChanged = 1;
		}
	} else {
		BEEP(1);
	}
}	/* wg_ForwardKillCh */



static void
wg_GoLeft(void)
{
	if (gBufPtr > gDst) {
		wg_SetCursorPos(gBufPtr - 1);	/* Does a --gBufPtr. */
		gNeedUpdate = 1;
		gMoved = 1;
	} else {
		BEEP(1);
	}
}	/* wg_GoLeft */




static void
wg_GoRight(void)
{
	if (gBufPtr < (gDst + gBufLen)) {
		wg_SetCursorPos(gBufPtr + 1);	/* Does a ++gBufPtr. */
		gNeedUpdate = 1;
		gMoved = 1;
	} else {
		BEEP(1);
	}
}	/* wg_GoRight */



static void
wg_GoLineStart(void)
{
	wg_SetCursorPos(gDst);
	gNeedUpdate = 1;
	gMoved = 1;
}	/* wg_GoLineStart */




static void
wg_GoLineEnd(void)
{
	wg_SetCursorPos(gDst + gBufLen);
	gNeedUpdate = 1;
	gMoved = 1;
}	/* wg_GoLineEnd */




static void
wg_LineKill(void)
{
	gBufPtr = gDst;
	gWinStartPtr = gBufPtr;
	gWinEndPtr = gWinStartPtr + gWindowWidth - 1;
	gBufPtr[gDstSize] = '\0';
	gBufLen = 0;
	gNeedUpdate = 1;

	/* Reset this so it acts as a new line.  We want them to be able to
	 * hit ^D until they do something with this line.
	 */
	gMoved = 0;

	/* We now have an empty string.  If we originally had something in the
	 * buffer, then mark it as changed since we just erased that.
	 */
	gChanged = gHadStartingString;
}	/* wg_LineKill */



static void
wg_HistoryUp(void)
{
	if (gHistory == wg_NoHistory) {
		/* Not using history. */
		BEEP(1);
		return;
	}

	if (gCurHistLine != NULL) {
		/* If not null, then the user had already scrolled up and was
		 * editing a line in the history.
		 */
		gCurHistLine = gCurHistLine->prev;
	} else {
		/* Was on original line to edit, but wants to go back one. */
		gCurHistLine = gHistory->last;
		if (gCurHistLine == NULL) {
			/* No lines at all in the history. */
			BEEP(1);
			return;
		}
	}

	wg_LineKill();
	if (gCurHistLine != NULL) {
		Strncpy(gDst, gCurHistLine->line, gDstSize);
		gBufLen = strlen(gDst);
		wg_GoLineEnd();
	}
	/* Otherwise, was on the first line in the history, but went "up" from here
	 * which wraps around to the bottom.  This last line is the new line
	 * to edit.
	 */
}	/* wg_HistoryUp */



static void
wg_HistoryDown(void)
{
	if (gHistory == wg_NoHistory) {
		/* Not using history. */
		BEEP(1);
		return;
	}

	if (gCurHistLine != NULL) {
		/* If not null, then the user had already scrolled up and was
		 * editing a line in the history.
		 */
		gCurHistLine = gCurHistLine->next;
	} else {
		/* Was on original line to edit, but wants to go down one.
		 * We'll wrap around and go to the very first line.
		 */
		gCurHistLine = gHistory->first;
		if (gCurHistLine == NULL) {
			/* No lines at all in the history. */
			BEEP(1);
			return;
		}
	}
	
	wg_LineKill();
	if (gCurHistLine != NULL) {
		Strncpy(gDst, gCurHistLine->line, gDstSize);
		gBufLen = strlen(gDst);
		wg_GoLineEnd();
	}
	/* Otherwise, was on the last line in the history, but went down from here
	 * which means we should resume editing a fresh line.
	 */
}	/* wg_HistoryDown */




static void
wg_Update(void)
{
	char *lastCharPtr;
	char *cp;

	wmove(gW, gSy, gSx);
	lastCharPtr = gDst + gBufLen;
	*lastCharPtr = '\0';
	if (gEchoMode == wg_RegularEcho) {
		for (cp = gWinStartPtr; cp < lastCharPtr; cp++) {
			if (cp > gWinEndPtr)
				goto xx;
			waddch(gW, (unsigned char) *cp);
		}
	} else if (gEchoMode == wg_BulletEcho) {
		for (cp = gWinStartPtr; cp < lastCharPtr; cp++) {
			if (cp > gWinEndPtr)
				goto xx;
			waddch(gW, wg_Bullet);
		}
	} else /* if (gEchoMode == wg_NoEcho) */ {
		for (cp = gWinStartPtr; cp < lastCharPtr; cp++) {
			if (cp > gWinEndPtr)
				goto xx;
			waddch(gW, ' ');
		}
	}

	/* Rest of display window is empty, so write out spaces. */
	for ( ; cp <= gWinEndPtr; cp++)
		waddch(gW, ' ');
xx:
	wmove(gW, gSy, gSx + (int) (gBufPtr - gWinStartPtr));
	wrefresh(gW);
	gNeedUpdate = 0;
} /* wg_Update */




int
wg_Gets(WGetsParamPtr wgpp)
{
	int c, result;
	int lineKill;
	int maxx, maxy;
#ifdef WG_DEBUG
	FILE *trace;
#endif

	/* Sanity checks. */
	if (wgpp == NULL)
		return (wg_BadParamBlock);

	if (wgpp->dstSize < 2)
		return (wg_DstSizeTooSmall);
	gDstSize = wgpp->dstSize - 1;	/* Leave room for nul. */
	
	if (wgpp->fieldLen < 1)
		return (wg_WindowTooSmall);
	gWindowWidth = wgpp->fieldLen;

	if (wgpp->w == NULL)
		return (wg_BadCursesWINDOW);
	gW = wgpp->w;

	getmaxyx(gW, maxy, maxx);
	if ((wgpp->sy < 0) || (wgpp->sy > maxy))
		return (wg_BadCoordinates);
	gSy = wgpp->sy;

	if ((wgpp->sx < 0) || (wgpp->sx > maxx))
		return (wg_BadCoordinates);
	gSx = wgpp->sx;

	if (wgpp->dst == NULL)
		return (wg_BadBufferPointer);
	gDst = wgpp->dst;

	gHistory = wgpp->history;	/* Will be NULL if not using history. */
	gCurHistLine = NULL;		/* Means we haven't scrolled into history. */

	gEchoMode = wgpp->echoMode;
	gChanged = 0;
	gMoved = 0;
	
	result = 0;
	wmove(gW, gSy, gSx);
	wrefresh(gW);

#ifdef WG_DEBUG
	trace = fopen(wg_TraceFileName, FOPEN_APPEND_TEXT);
	if (trace != NULL) {
		fprintf(trace, "<START>\n");
	}
#endif

	cbreak();
	/* Should already have echo turned off. */
	/* noecho(); */
	nodelay(gW, FALSE);
	keypad(gW, TRUE);
#ifdef HAVE_NOTIMEOUT
	notimeout(gW, TRUE);
#endif

	lineKill = (int) killchar();

	gNeedUpdate = 1;
	gBufPtr = gDst;
	gWinStartPtr = gBufPtr;
	gWinEndPtr = gWinStartPtr + gWindowWidth - 1;
	gBufPtr[gDstSize] = '\0';
	gHadStartingString = 0;
	if (wgpp->useCurrentContents) {
		gBufLen = strlen(gBufPtr);
		if (gBufLen != 0)
			gHadStartingString = 1;
	} else {
		gBufLen = 0;
	}

	for (;;) {
		if (gNeedUpdate) 
			wg_Update();

		c = wgetch(gW);
#ifdef WG_DEBUG
		if (trace != NULL) {
			switch (c) {
				case '\r': fprintf(trace, "(\\r)\n"); break;
				case '\n': fprintf(trace, "(\\n)\n"); break;
#ifdef KEY_ENTER
				case KEY_ENTER: fprintf(trace, "(KEY_ENTER)\n"); break;
#endif
				default: fprintf(trace, "[%c] = 0x%X\n", c, c);
			}
		}
#endif
		switch (c) {
			case '\r':
			case '\n':
#ifdef KEY_ENTER
			case KEY_ENTER:
#endif
				goto done;			

			case '\b':
#ifdef KEY_BACKSPACE
			case KEY_BACKSPACE:
#endif
			case 0x7f:
				wg_KillCh(1);
				break;

#ifdef KEY_FWDDEL		/* Need to find a real symbol for forward delete. */
			case KEY_FWDDEL:
				wg_ForwardKillCh();
				break;
#endif

#ifdef KEY_EXIT
			case KEY_EXIT:
#endif
#ifdef KEY_CLOSE
			case KEY_CLOSE:
#endif
#ifdef KEY_CANCEL
			case KEY_CANCEL:
#endif
			case 0x04:		/* Control-D */
				/* If we haven't changed the buffer, and the cursor has
				 * not moved from the first position, return EOF.
				 */
				if (!gChanged && !gMoved) {
					result = wg_EOF;
					goto done;
				}
				if (gBufPtr == gDst + gBufLen) {
#if 0
					wg_AddCh('*'); wg_Update();
					CompleteOptions(gDst, gBufPtr-gDst-1);
					wg_KillCh(1);
#endif
				} else {
					wg_ForwardKillCh();		/* Emacs ^D */
				}
				break;
#ifdef KEY_DC
			case KEY_DC:
#endif
				if (gBufPtr == gDst + gBufLen) {
#if 0
					wg_AddCh('*'); wg_Update();
					CompleteOptions(gDst, gBufPtr-gDst-1);
					wg_KillCh(1);
#endif
				} else {
					wg_ForwardKillCh();		/* Emacs ^D */
				}
				break;

#ifdef KEY_CLEAR
			case KEY_CLEAR:
#endif
			case 0x0C:		/* Control-L */
				touchwin(curscr);
				wrefresh(curscr);
				break;

#ifdef KEY_LEFT
			case KEY_LEFT:
#endif
			case 0x02:		/* Control-F */
				wg_GoLeft();
				break;

#ifdef KEY_RIGHT
			case KEY_RIGHT:
#endif
			case 0x06:		/* Control-B */
				wg_GoRight();
				break;

#ifdef KEY_UP
			case KEY_UP:
#endif
			case 0x10:		/* Control-P */
				wg_HistoryUp();
				break;

#ifdef KEY_DOWN
			case KEY_DOWN:
#endif
			case 0x0E:		/* Control-N */
				wg_HistoryDown();
				break;

#ifdef KEY_HOME
			case KEY_HOME:
#endif
			case 0x01:		/* Control-A */
				wg_GoLineStart();
				break;
#ifdef KEY_END
			case KEY_END:
#endif
			case 0x05:		/* Control-E */
				wg_GoLineEnd();
				break;
#ifdef KEY_EOL
			case KEY_EOL:
#endif
			case 0x0B:
				while ((gBufLen != 0) && (gBufPtr < gDst + gBufLen))
					wg_ForwardKillCh();     /* Emacs ^K */
				break;

			case -1:
				/* This can happen if getch() was interrupted by a
				 * signal like ^Z.
				 */
				break;

			case 23:  /* ^W */
				wg_KillWord();
				break;

			case '\t': {
#if 0
				int i;
				char *comp;
				char *tmp;
				for (i=0;i<3;i++)
					wg_AddCh('.');
				wg_Update();
				comp = CompleteGet(gDst, gBufPtr-gDst-3);
				wg_KillCh(3);
				gDst[gBufLen] = '\0';
				if (comp) {
					for (tmp = comp; *tmp; tmp++)
						wg_AddCh(*tmp);
					free(comp);
				}
#endif
				break;
			}
				
			default:
				if (c < 0400) {
					if (c == lineKill)
						wg_LineKill();
					else
						wg_AddCh(c);
				}
		}
	}

done:
	nocbreak();

	gDst[gBufLen] = '\0';
	wgpp->changed = gChanged;
	wgpp->dstLen = (int) gBufLen;
	if ((gHistory != wg_NoHistory) && (gBufLen != 0))
		AddLine(wgpp->history, gDst);
	
#ifdef WG_DEBUG
		if (trace != NULL) {
			fprintf(trace, "<DONE>\n");
			fclose(trace);
		}
#endif
	return (result);
}	/* wg_Gets */

/* eof */
