/*
 *	history.c
 *
 *	Manage command-history buffer
 *
 * Notes:
 *	This module manages an invisible, non-volatile buffer "[History]".
 *	Each keyboard command to vile is logged in this buffer.  The leading
 *	':' is not included, but all other characters are preserved so that the
 *	command can be replayed.
 *
 *	Each interactive call on 'kbd_reply()' logs the resulting string, and
 *	the "glue" character (nominally, the end-of-line character), so that
 *	successive calls can be spliced together. On completion of the command,
 *	the composed command (in 'MyText') is flushed into the history-buffer.
 *
 *	The procedure 'edithistory()' is invoked from 'kbd_reply()' to provide
 *	access to the history.  In particular, it presents a scrollable list of
 *	strings based on a match of the command to that point.  For example, if
 *	the commands
 *
 *		:set ts=8
 *		:set ab
 *		:set ai
 *
 *	were entered, then in response to ":set", the user would see the
 *	strings "ai", "ab" and "ts".
 *
 *	Scrolling is accomplished by either arrow keys, or by an escaped set of
 *	commands (a la 'ksh').
 *
 *	Note that this implementation is a compromise.  Ideally, the command
 *	interpreter for ':' would be able to scroll through the entire list of
 *	commands from any point, moving forward and backward through its
 *	internal state of range, command, arguments.  As implemented, it is not
 *	so general.  The user can backspace from the command state into the
 *	range state, but not from the arguments.  Also, the history scrolling
 *	is not really useful in the range state, so it is disabled there.  If
 *	the command interpreter were able to easily go back and forth in its
 *	state, then it would be simple to implement an 'expert' mode, in which
 *	prompting would be suppressed.
 *
 * To do:
 *	Add logic to quote arguments that should be strings, to make them
 *	easier to parse back for scrolling, etc.
 *
 *	Integrate this with the "!!" response to the ^X-! and !-commands.
 *
 *	Modify the matching logic so that file commands (i.e., ":e", ":w",
 *	etc.) are equivalent when matching for the argument.  Currently, the
 *	history scrolling will show only arguments for identical command names.
 *
 *	Modify the matching logic so that search commands (i.e., "/" and "?")
 *	are equivalent when matching for the argument.  Note also that these do
 *	not (yet) correspond to :-commands.  Before implementing, probably will
 *	have to make TESTC a settable mode.
 *
 * $Header: /usr/build/vile/vile/RCS/history.c,v 1.76 2003/07/27 16:55:39 tom Exp $
 *
 */

#include "estruct.h"
#include "edef.h"

#if	OPT_HISTORY

#define HST_QUOTES 1

#define	tb_args(p)	tb_values(p), (int)tb_length(p)
#define	lp_args(p)	p->l_text, llength(p)

typedef struct {
    TBUFF **buffer;
    unsigned *position;
    int (*endfunc) (EOL_ARGS);
    int eolchar;
    UINT options;
} HST;

/*--------------------------------------------------------------------------*/

static TBUFF *MyText;		/* current command to display */
static int MyGlue;		/* most recent eolchar */
static int MyLevel;		/* logging iff level is 1 */

/*--------------------------------------------------------------------------*/

static void
stopMyBuff(void)
{
    BUFFER *bp;

    if ((bp = find_b_name(HISTORY_BufName)) != 0)
	(void) zotbuf(bp);

    tb_free(&MyText);
}

static BUFFER *
makeMyBuff(void)
{
    BUFFER *bp;

    if (!global_g_val(GMDHISTORY)) {
	bp = 0;
    } else if ((bp = make_ro_bp(HISTORY_BufName, BFINVS)) == 0) {
	stopMyBuff();
    } else {
	set_vilemode(bp);
    }
    return bp;
}

/*
 * Returns 0 or 1 according to whether we will add the glue-character in the
 * next call on 'hst_append()'.
 */
static int
willGlue(void)
{
    if ((tb_length(MyText) != 0) && (isPrint(MyGlue) || MyGlue == '\r')) {
	int c = tb_values(MyText)[0];
	if ((c != SHPIPE_LEFT[0]) || isRepeatable(c))
	    return 1;
    }
    return 0;
}

/*
 * Returns true iff we display the complete, rather than the immediate portion
 * of the history line.  We do this for !-commands so that the user can see the
 * entire command when scrolling.
 *
 * The shift-commands also are a (similar) special case.
 */
static int
willExtend(const char *src, int srclen)
{
    if ((tb_length(MyText) == 0)
	&& (srclen > 0)) {
	return (src[0] == SHPIPE_LEFT[0]) || isRepeatable(src[0]);
    }
    return FALSE;
}

/*
 * Returns a positive number if the length of the given LINE is at least as
 * long as the given string in 'src' and if it matches to the length in
 * 'srclen'.
 */
static int
sameLine(LINE *lp, char *src, int srclen)
{
    if (srclen <= 0)
	return 0;
    else {
	int dstlen = llength(lp);

	if (dstlen >= srclen) {
	    if (!memcmp(lp->l_text, src, (size_t) srclen)) {
		if (isRepeatable(*src)
		    && isRepeatable(lp->l_text[0])
		    && dstlen != srclen)
		    return -1;
		return (dstlen - srclen);
	    }
	}
    }
    return -1;
}

/*
 * Given a starting offset and index limit for src, find the index of the
 * character that ends the parameter.
 */
static int
endOfParm(HST * parm, char *src, int offset, int limit)
{
    int n;

    if (limit > 0) {
	if (willExtend(src, limit)) {
	    n = limit;
	} else {
#if HST_QUOTES
	    int quoted = (src[offset] == DQUOTE);
	    int escaped = FALSE;
#endif
	    for (n = offset; n < limit; n++) {
#if HST_QUOTES
		if (quoted) {
		    if (escaped) {
			escaped = FALSE;
		    } else if (src[n] == BACKSLASH) {
			escaped = TRUE;
		    } else if (n != offset && (src[n] == src[offset])) {
			n++;
			break;
		    }
		} else
#endif
		if ((*parm->endfunc) (src, n, src[n], parm->eolchar))
		    break;
	    }
	}
    } else {
	n = 0;
    }
    return n;
}

/*
 * Returns the index of the character that ends the current argument on the
 * given line.
 */
static int
parseArg(HST * parm, LINE *lp)
{
    return endOfParm(parm,
		     lp->l_text,
		     willGlue() + tb_length(MyText),
		     llength(lp));
}

/*
 * Returns true if the buffer is not the first token on the current history
 * line, and if it contains blanks or quotes which would confuse us when
 * parsing.
 */
static int
needQuotes(TBUFF * src)
{
#if HST_QUOTES
    if (tb_length(MyText)
	|| must_quote_token(tb_values(src), tb_length(src)))
	return TRUE;
#endif
    return FALSE;
}

#if HST_QUOTES
/*
 * Reverses append_quoted_token() by stripping the quotes and returning an
 * allocated buffer with the text.  The associated length is returned via the
 * 'actual' parameter.
 */
static char *
stripQuotes(char *src, int len, int eolchar, int *actual)
{
    char *dst = 0;

    if (len > 0) {
	TRACE(("stripQuotes(%.*s)\n", len, src));
	if ((dst = (char *) malloc((size_t) len + 1)) != 0) {
	    int j, k;
	    int quoted = FALSE;
	    int escaped = FALSE;

	    for (j = k = 0; j < len; j++) {
		if (escaped) {
		    escaped = FALSE;
		    switch (src[j]) {
		    case DQUOTE:
		    case BACKSLASH:
		    default:
			dst[k++] = src[j];
			break;
		    case 'b':
			dst[k++] = '\b';
			break;
		    case 't':
			dst[k++] = '\t';
			break;
		    case 'r':
			dst[k++] = '\r';
			break;
		    case 'n':
			dst[k++] = '\n';
			break;
		    }
		} else if (src[j] == BACKSLASH) {
		    escaped = TRUE;
		} else if (src[j] == DQUOTE && src[0] == DQUOTE) {
		    quoted = !quoted;
		} else if (!quoted && src[j] == eolchar) {
		    break;
		} else {
		    dst[k++] = src[j];
		}
	    }
	    dst[k] = EOS;
	    *actual = k;
	}
    }
    return dst;
}
#endif

static void
glueBufferToResult(TBUFF ** dst, TBUFF * src)
{
    int shell_cmd = ((tb_length(*dst) != 0 && isShellOrPipe(tb_values(*dst)))
		     || (tb_length(*dst) != 0 && isShellOrPipe(tb_values(src))));

    if (willGlue())
	(void) tb_append(dst, MyGlue);

    if (!shell_cmd
	&& needQuotes(src)) {
	append_quoted_token(dst, tb_values(src), tb_length(src));
    } else {
	(void) tb_bappend(dst, tb_args(src));
    }
}

/******************************************************************************/

void
hst_reset(void)
{
    MyLevel = 0;
    (void) tb_init(&MyText, esc_c);
}

void
hst_init(int c)
{
    if (++MyLevel == 1) {
	(void) tb_init(&MyText, esc_c);
	MyGlue = EOS;
	if (c != EOS)
	    (void) tb_append(&MyText, c);
    }
}

void
hst_glue(int c)
{
    /* ensure we don't repeat '/' delimiter */
    if (tb_length(MyText) == 0
	|| tb_values(MyText)[0] != c)
	MyGlue = c;
}

void
hst_append(TBUFF * cmd, int glue)
{
    static int skip = 1;	/* e.g., after "!" */

    if (clexec || !vl_echo || qpasswd) {
	/* noninteractive, $disinp=FALSE, or querying for password */

	return;
    }
    if (isreturn(glue))
	glue = ' ';

    TRACE(("hst_append(cmd=%d:%d:%s) glue='%c'\n",
	   willExtend(tb_args(cmd)),
	   (int) tb_length(cmd),
	   tb_visible(cmd),
	   glue));
    TRACE(("...MyText        :%d:%s\n", tb_length(MyText), tb_visible(MyText)));
    if (willExtend(tb_args(cmd))
	&& tb_length(cmd) > (size_t) skip) {
	kbd_pushback(cmd, skip);
    }

    glueBufferToResult(&MyText, cmd);
    TRACE(("...MyText        :%d:%s\n", tb_length(MyText), tb_visible(MyText)));
    MyGlue = glue;
}

void
hst_append_s(char *cmd, int glue)
{
    TBUFF *p = tb_string(cmd);
    hst_append(p, glue);
    tb_free(&p);
}

void
hst_remove(const char *cmd)
{
    if (MyLevel == 1 && *cmd != EOS) {
	TBUFF *temp = 0;
	unsigned len = tb_length(tb_scopy(&temp, cmd)) - 1;

	TRACE(("hst_remove(%s)\n", cmd));
	while (*cmd++)
	    tb_unput(MyText);
	kbd_kill_response(temp, &len, killc);
	tb_free(&temp);
    }
}

void
hst_flush(void)
{
    BUFFER *bp;
    WINDOW *wp;
    LINE *lp;

    if (MyLevel <= 0)
	return;
    if (MyLevel-- != 1)
	return;

    if ((tb_length(MyText) != 0)
	&& ((bp = makeMyBuff()) != 0)) {

	/* suppress if this is the same as previous line */
	if (((lp = lback(buf_head(bp))) != 0)
	    && (lp != buf_head(bp))
	    && (sameLine(lp, tb_args(MyText)) == 0)) {
	    (void) tb_init(&MyText, esc_c);
	    return;
	}

	if (!addline(bp, tb_args(MyText))) {
	    stopMyBuff();
	    return;
	}

	/* patch: reuse logic from slowreadf()? */
	for_each_visible_window(wp) {
	    if (wp->w_bufp == bp) {
		wp->w_flag |= WFFORCE;
		if (wp == curwp)
		    continue;
		/* force dot to the beginning of last-line */
		wp->w_force = -1;
		if (wp->w_dot.l != lback(buf_head(bp))) {
		    wp->w_dot.l = lback(buf_head(bp));
		    wp->w_dot.o = 0;
		    wp->w_flag |= WFMOVE;
		}
	    }
	}
	updatelistbuffers();	/* force it to show current sizes */
	(void) tb_init(&MyText, esc_c);
    }
}

/*ARGSUSED*/
int
showhistory(int f GCC_UNUSED, int n GCC_UNUSED)
{
    BUFFER *bp = makeMyBuff();

    if (!global_g_val(GMDHISTORY)) {
	mlforce("history mode is not set");
	return FALSE;
    } else if (bp == 0 || popupbuff(bp) == FALSE) {
	return no_memory("show-history");
    }
    return TRUE;
}

/*
 * Find the last line in the history buffer that matches the portion of
 * the command that has been input to this point.  The substrings to the
 * right (up to eolchar) will form the set of history strings that the
 * user may scroll through.
 */
static LINE *
hst_find(HST * parm, BUFFER *bp, LINE *lp, int direction)
{
    LINE *base = buf_head(bp);
    LINE *lp0 = lp;

    if ((lp0 == 0)
	|| ((lp == base) && (direction > 0))) {
	return 0;
    }

    for_ever {
	if (direction > 0) {
	    if (lp == lback(base))	/* cannot wrap-around */
		return 0;
	    lp = lforw(lp);
	} else
	    lp = lback(lp);
	if (lp == base)
	    return 0;		/* empty or no matches */

	if (!lisreal(lp)
	    || ((size_t) llength(lp) <= tb_length(MyText) + willGlue())
	    || (sameLine(lp, tb_args(MyText)) < 0))
	    continue;		/* prefix mismatches */

	if (willGlue()) {	/* avoid conflicts setall/set */
	    int len = tb_length(MyText);
	    if (len > 0
		&& (len > 1 || !isPunct(tb_values(MyText)[0]))
		&& llength(lp) > len
		&& lp->l_text[len] != MyGlue)
		continue;
	}

	/* avoid picking up lines with range-spec, since this is too
	 * cumbersome to splice in 'namedcmd()'.
	 */
	if (islinespecchar(lp->l_text[0]))
	    continue;

	/* '/' and '?' are not (yet) :-commands.  Don't display them
	 * in the command-name scrolling.
	 */
	if (tb_length(MyText) == 0) {
	    if (lp->l_text[0] == '/'
		|| lp->l_text[0] == '?')
		continue;
	}

	/* compare the argument that will be shown for the original
	 * and current lines.
	 */
	if (lisreal(lp0)) {
	    int n0 = parseArg(parm, lp0);
	    int n1 = parseArg(parm, lp);
	    if (n0 != 0
		&& n1 != 0
		&& n0 == n1
		&& sameLine(lp, lp0->l_text, n0) >= 0)
		continue;
	}

	return lp;
    }
}

/*
 * Update the display of the currently-scrollable buffer on the prompt-line.
 */
static void
hst_display(HST * parm, char *src, int srclen)
{
    TRACE(("hst_display(%.*s) eolchar='%c'\n",
	   srclen, src,
	   isreturn(parm->eolchar) ? ' ' : parm->eolchar));

    /* kill the whole buffer */
    *(parm->position) = tb_length(*(parm->buffer));
    wminip->w_dot.o = llength(wminip->w_dot.l);
    kbd_kill_response(*(parm->buffer), parm->position, killc);

    if (src != 0 && srclen != 0) {
	char *stripped;
	int offset = tb_length(MyText) + willGlue();
	int n = endOfParm(parm, src, offset, srclen) - offset;

	src += offset;
	stripped = src;
#if HST_QUOTES
	TRACE(("...offset=%d, n=%d, MyText=%s\n", offset, n, tb_visible(MyText)));
	if (tb_length(MyText) != 0 && isShellOrPipe(tb_values(MyText)))
	    TRACE(("...MyText is a shell command\n"));
	else if (tb_length(MyText) == 0 && offset == 0 && isShellOrPipe(src))
	    TRACE(("...src is a shell command\n"));
	else if (*src == DQUOTE || isSpace(parm->eolchar))
	    stripped = stripQuotes(src, n,
				   isSpace(parm->eolchar) ? ' ' : parm->eolchar,
				   &n);
#endif
	TRACE(("...hst_display offset=%d, string='%.*s'\n", offset, n, stripped));
	*parm->position = kbd_show_response(parm->buffer,
					    stripped,
					    n,
					    parm->eolchar, parm->options);
#if HST_QUOTES
	if (stripped != 0 && stripped != src)
	    free(stripped);
#endif
    }
}

/*
 * Update the display using a LINE as source
 */
static void
display_LINE(HST * parm, LINE *lp)
{
    hst_display(parm, lp_args(lp));
}

/*
 * Update the display using a TBUFF as source
 */
static void
display_TBUFF(HST * parm, TBUFF * tp)
{
    hst_display(parm, tb_args(tp));
}

/*
 * Perform common scrolling functions for arrow-keys and ESC-mode.
 */
static TBUFF *h_original;	/* save 'buffer' on first-scroll */
static int h_was_edited;	/* true iff any edit happened */
static int h_direction;		/* current scrolling +/- */
static int h_distance;		/* distance from original entry */

static LINE *
hst_scroll(LINE *lp1, HST * parm)
{
    BUFFER *bp = makeMyBuff();
    LINE *lp0 = buf_head(bp);
    LINE *lp2 = hst_find(parm, bp, lp1, h_direction);

    if (lp1 != lp2) {
	if (lp2 == 0) {
	    if (h_direction + h_distance == 0) {
		lp1 = lp0;
		h_distance = 0;
		display_TBUFF(parm, h_original);
	    } else {
		if (lp1 == lp0)	/* nothing to scroll for */
		    h_distance = 0;
		kbd_alarm();
	    }
	    return lp1;
	} else {
	    h_distance += h_direction;
	    display_LINE(parm, lp2);
	    h_was_edited++;
	    return lp2;
	}
    }
    return 0;
}

/*
 * Invoked on an escape-character, this processes history-editing until another
 * escape-character is entered.
 */
int
edithistory(
	       TBUFF ** buffer,
	       unsigned *position,
	       int *given,
	       UINT options,
	       int (*endfunc) (EOL_ARGS),
	       int eolchar)
{
    HST param;
    BUFFER *bp;
    LINE *lp1, *lp2;
    int escaped = FALSE;
    int c = *given;

    if (!isSpecial(c)) {
	if (is_edit_char(c)
	    || ABORTED(c)
	    || (c == quotec)
	    || isSpace(c)
	    || !isCntrl(c))
	    return FALSE;
    }

    if ((bp = makeMyBuff()) == 0)	/* something is very wrong */
	return FALSE;

    if ((lp1 = buf_head(bp)) == 0)
	return FALSE;

    /* slightly better than global data... */
    param.buffer = buffer;
    param.position = position;
    param.endfunc = endfunc;
    param.eolchar = (eolchar == '\n') ? '\r' : eolchar;
    param.options = options;

    h_was_edited = 0;
    h_distance = 0;

    /* save the original buffer, since we expect to scroll it */
    if (tb_copy(&h_original, MyText)) {
	/* make 'original' look just like a complete command... */
	glueBufferToResult(&h_original, *buffer);
    }

    /* process char-commands */
    for_ever {
	const CMDFUNC *p;

	/* If the character is bound to up/down scrolling, scroll the
	 * history.
	 */
	h_direction = 0;	/* ...unless we find scrolling-command */
	if ((p = DefaultKeyBinding(c)) != 0) {
	    if (CMD_U_FUNC(p) == backline)
		h_direction = -1;
	    else if (CMD_U_FUNC(p) == forwline)
		h_direction = 1;
	}
	if (ABORTED(c)) {
	    *given = c;
	    return FALSE;

	} else if ((h_direction != 0) && (escaped || !isGraph(c))) {

	    if ((lp2 = hst_scroll(lp1, &param)) != 0)
		lp1 = lp2;
	    else		/* cannot scroll */
		kbd_alarm();
	} else if (!escaped) {
	    *given = c;
	    if (h_was_edited)
		unkeystroke(c);
	    return h_was_edited;

	} else
	    kbd_alarm();

	c = keystroke();
    }
}
#endif /* OPT_HISTORY */
