/* search.c */
/* Copyright 1995 by Steve Kirkendall */

char id_search[] = "$Id: search.c,v 2.23 1998/11/28 17:02:13 steve Exp $";

#include "elvis.h"

#if USE_PROTOTYPES
static RESULT searchenter(WINDOW win);
static RESULT forward(WINDOW win);
static RESULT backward(WINDOW win);
#endif


/* This is the previous regular expression.  We need to remember it after
 * it has been used, so we can implement the <n> and <shift-N> commands.
 */
regexp	*searchre;	/* the regular expression itself */
BOOLEAN searchforward;	/* is the searching being done in a forward direction? */
BOOLEAN	searchhasdelta;	/* is the regular expression followed by a line delta? */
long	searchdelta;	/* if searchhasdelta, then this is the line offset value */
static BOOLEAN autoselect;/* select the matching text? */

/* This function is called when the user hits <Enter> after typing in a
 * regular expression for the visual </> and <?> commands.  It compiles
 * the regular expression, and then matches it in either a forward or
 * backward direction.
 */
static RESULT	searchenter(win)
	WINDOW	win;	/* window where a regexp search line has been entered */
{
	CHAR	*regex;	/* holds regular expression, as string */
	CHAR	delim;	/* delimiter, either '?' or '/' */
	CHAR	*cp;	/* used when copying */
	STATE	*state = win->state;

	assert(markoffset(state->top) <= markoffset(state->cursor));
	assert(markoffset(state->cursor) <= markoffset(state->bottom));

	/* copy the regexp into a buffer */
	scanalloc(&cp, state->top);
	if (!cp)
		goto Error;
	delim = *cp;
	if ((delim != '/' && delim != '?') || !scannext(&cp))
		goto Error;
	regex = regbuild(delim, &cp);

	/* Compile the regular expression.  An empty regular expression is
	 * identical to the current regular expression, so we don't need to
	 * compile it in that case.  If there are any errors, then fail.
	 * The regcomp function will have already output an error message.
	 */
	if (*regex)
	{
		/* free the previous regular expression, if any */
		if (searchre)
			safefree((void *)searchre);

		/* compile the new one */
		searchre = regcomp(regex, win->cursor);
	}
	else if (!searchre)
	{
		msg(MSG_ERROR, "no previous RE");
	}

	/* we don't need the text version of the regex any more */
	safefree(regex);

	/* detect errors in compilation */
	if (!searchre)
	{
		scanfree(&cp);
		return RESULT_ERROR;
	}

	/* '/' implies forward search, '?' implies backward search */
	searchforward = (BOOLEAN)(delim == '/');

	/* Check for 'v' to force "autoselect" on, or 'n' to force it off.
	 * If neither 'v' nor 'n' then use the value of the autoselect option.
	 */
	switch (cp ? *cp : '\0')
	{
	  case 'v':
		autoselect = True;
		scannext(&cp);
		break;

	  case 'n':
		autoselect = False;
		scannext(&cp);
		break;

	  default:
		autoselect = o_autoselect;
	}

	/* check for a line delta after the regular expression */
	if (cp && (*cp == '+' || *cp == '-'))
	{
		searchhasdelta = True;
		delim = *cp;
		for (searchdelta = 0; scannext(&cp) && isdigit(*cp); )
			searchdelta = searchdelta * 10 + *cp - '0';
		if (delim == '-')
			searchdelta = -searchdelta;
	}
	else
	{
		searchhasdelta = False;
	}

	/* free the scan context */
	scanfree(&cp);

	/* Now we can do this search exactly like an <n> search command, except
	 * that it'll be applied to the previous stratum instead of this one.
	 */
	return RESULT_COMPLETE;

Error:
	scanfree(&cp);
	msg(MSG_ERROR, "bad search");
	return RESULT_ERROR;
}

/* search forward from the cursor position for the previously compiled
 * regular expression.
 */
static RESULT forward(win)
	WINDOW	win;	/* the window to search in */
{
	MARKBUF	sbuf;	/* buffer, holds search mark */
	long	lnum;	/* line number -- used for scanning through buffer */
	BLKNO	bi;	/* bufinfo block of the buffer being searched */
	BOOLEAN	bol;	/* are we at the beginning of a line? */
	BOOLEAN	wrapped;/* True if we've wrapped at the end of the buffer */
	long	start;	/* line where we started (detects failure after wrap) */
	CHAR	*cp;

	/* setup: determine the line number, whether the cursor is at the
	 * beginning of the line, and where the buffer ends.
	 */
	sbuf = *win->state->cursor;
	if (markoffset(&sbuf) + 1 >= o_bufchars(markbuffer(&sbuf)))
		marksetoffset(&sbuf, 0L);
	else
		markaddoffset(&sbuf, 1);
	bi = bufbufinfo(markbuffer(&sbuf));
	lnum = markline(&sbuf);
	bol = (BOOLEAN)(lowline(bi, lnum) == markoffset(&sbuf));
	wrapped = False;
	start = lnum;

	/* search */
	scanalloc(&cp, &sbuf);
	for (;
	     !regexec(searchre, &sbuf, bol);
	     marksetoffset(&sbuf, searchre->nextlinep), bol = True)
	{
		/* If user gives up, then fail */
		if (guipoll(False))
		{
			scanfree(&cp);
			return RESULT_ERROR;
		}

		/* Advance to next line.  Maybe wrap at the bottom of the
		 * buffer.  This is also where failed searches are detected.
		 */
		lnum++;
		if (wrapped && lnum > start)
		{
			scanfree(&cp);
			msg(MSG_ERROR, "no match");
			return RESULT_ERROR;
		}
		else if (lnum > o_buflines(markbuffer(&sbuf)))
		{
			if (o_wrapscan)
			{
				lnum = 1;
				searchre->nextlinep = 0L;
				wrapped = True;
			}
			else
			{
				scanfree(&cp);
				msg(MSG_ERROR, "no match below");
				return RESULT_ERROR;
			}
		}
		scanseek(&cp, &sbuf);
	}
	scanfree(&cp);

	/* if we get here, then the search succeded */
	assert(searchre->leavep >= 0);
	marksetoffset(win->state->cursor, searchre->leavep);
	if (wrapped)
		msg(MSG_STATUS, "wrapped");
	return RESULT_COMPLETE;
}

/* search backward from the cursor position for the previously compiled
 * regular expression.
 */
static RESULT backward(win)
	WINDOW	win;	/* the window to search in */
{
	MARKBUF	sbuf;	/* buffer, holds search mark */
	BLKNO	bi;	/* bufinfo block of the buffer being searched */
	long	lnum;	/* line number -- used for scanning through buffer */
	BOOLEAN	wrapped;/* True if we've wrapped at the end of the buffer */
	long	start;	/* line where we started (detects failure after wrap) */
	long	endln;	/* offset of the end of a line (really start of next line) */
	long	last;	/* offset of last match found in a line */
	long	laststartp;/* offset of the start of the last match in a line */
	long	lastendp;/* offset of the end of the last match in a line */
	CHAR	*cp;	/* used for scanning for newline characters */

	/* setup: determine the line number, whether the cursor is at the
	 * beginning of the line, and where the buffer ends.
	 */
	sbuf = *win->state->cursor;
	bi = bufbufinfo(markbuffer(&sbuf));
	lnum = markline(&sbuf);
	marksetoffset(&sbuf, lowline(bi, lnum));
	endln = (lnum == o_buflines(markbuffer(&sbuf))
		? o_bufchars(markbuffer(&sbuf)) : lowline(bi, lnum + 1));
	wrapped = False;
	start = lnum;

	/* check for match in same line, to left of cursor */
	if (regexec(searchre, &sbuf, True)
	 && searchre->leavep < markoffset(win->state->cursor))
	{
		/* find the last match in the line that occurs before the
		 * current position.
		 */
		do
		{
			last = searchre->leavep;
			laststartp = searchre->startp[0];
			lastendp = searchre->endp[0];
			marksetoffset(&sbuf, last + 1);
		} while (last + 1 < endln
			&& regexec(searchre, &sbuf, False)
			&& searchre->leavep < markoffset(win->state->cursor));
		marksetoffset(win->state->cursor, last);
		searchre->startp[0] = laststartp;
		searchre->endp[0] = lastendp;
		return RESULT_COMPLETE;
	}

	/* search for some other line with a match */
	scanalloc(&cp, &sbuf);
	do
	{
		/* If user gives up, then fail */
		if (guipoll(False))
		{
			scanfree(&cp);
			return RESULT_ERROR;
		}

		/* Advance to preceding line.  Maybe wrap at the top of the
		 * buffer.  This is also where failed searches are detected.
		 */
		lnum--;
		if (wrapped && lnum < start)
		{
			scanfree(&cp);
			msg(MSG_ERROR, "no match");
			return RESULT_ERROR;
		}
		else if (lnum < 1)
		{
			if (o_wrapscan)
			{
				lnum = o_buflines(markbuffer(&sbuf));
				endln = o_bufchars(markbuffer(&sbuf));
				wrapped = True;
			}
			else
			{
				scanfree(&cp);
				msg(MSG_ERROR, "no match above");
				return RESULT_ERROR;
			}

			/* find the start of the last line */
			marksetoffset(&sbuf, lowline(bi, lnum));
			scanseek(&cp, &sbuf);
		}
		else
		{
			endln = markoffset(&sbuf);
			scanprev(&cp);
			assert(cp && *cp == '\n');
			do
			{
				scanprev(&cp);
			} while (cp && *cp != '\n');
			if (cp)
			{
				scannext(&cp);
				sbuf = *scanmark(&cp);
			}
			else
			{
				marksetoffset(&sbuf, 0L);
				scanseek(&cp, &sbuf);
			}
		}

	} while (!regexec(searchre, &sbuf, True));

	/* If we get here, then the search succeded -- meaning we have found
	 * a line which contains at least one match.  Now we need to locate
	 * the LAST match in that line.
	 */
	do
	{
		last = searchre->leavep;
		laststartp = searchre->startp[0];
		lastendp = searchre->endp[0];
		marksetoffset(&sbuf, last + 1);
	} while (last + 1 < endln
		&& regexec(searchre, &sbuf, False));
	scanfree(&cp);

	/* move to the last match */
	marksetoffset(win->state->cursor, last);
	searchre->startp[0] = laststartp;
	searchre->endp[0] = lastendp;
	if (wrapped)
		msg(MSG_STATUS, "wrapped");
	return RESULT_COMPLETE;
}

RESULT m_search(win, vinf)
	WINDOW	win;	/* window where operation should take place */
	VIINFO	*vinf;	/* the command to execute */
{
	BOOLEAN	fwd;	/* is this search going to go forward? */
	BOOLEAN	hint;	/* show a "/" or "?" during the search? */
	RESULT	rc;	/* return code */
	VIINFO	vinfbuf;/* used for constructing a vi command for selections */

	assert(vinf->command == 'n' || vinf->command == 'N'
		|| vinf->command == '/' || vinf->command == '?'
		|| vinf->command == ELVCTRL('A'));

	/* If repeating an operator with / or ?, then use n instead.  (Since
	 * search commands don't change the buffer themselves, the only way <.>
	 * would repeat one is in conjunction with an operator.  So we don't
	 * need do explicitly check for an operator; we KNOW there is one.)
	 */
	if ((vinf->tweak & TWEAK_DOTTING) != 0
			&& (vinf->command == '/' || vinf->command == '?'))
	{
		vinf->command = 'n';
	}

	hint = False;
	switch (vinf->command)
	{
	  case 'n':
		fwd = searchforward;
		hint = True;
		break;

	  case 'N':
		/* reverse the direction of the search */
		fwd = (BOOLEAN)!searchforward;
		hint = True;
		break;

	  case ELVCTRL('A'):
		/* Free the previous regular expression, if any */
		if (searchre)
		{
			safefree((void *)searchre);
		}

		/* Compile the regexp /\<\@\>/ */
		searchre = regcomp(toCHAR("\\<\\@\\>"), win->cursor);
		if (!searchre)
			return RESULT_ERROR;

		/* always search in the forward direction */
		fwd = searchforward = True;
		searchhasdelta = False;
		searchdelta = 0L;
		hint = True;
		break;

	  default:
		/* There are two modes of operation here.  If the "more"
		 * flag is not set, then we need to push a new stratum for
		 * reading a regular expression to search for.  If "more"
		 * is set, then we act just like 'n'.
		 */
		if (win->state->flags & ELVIS_MORE)
		{
			win->state->flags &= ~ELVIS_MORE;
			fwd = searchforward;
			break;
		}
		statestratum(win, toCHAR(REGEXP_BUF), vinf->command, searchenter);
		return RESULT_MORE;
	}

	/* If we get here, we need to do an 'n' search */

	/* This'll only work if we have a regexp */
	if (!searchre)
	{
		msg(MSG_ERROR, "no previous search");
		return RESULT_ERROR;
	}

	/* give a hint that we're searching, if necessary */
	if (hint && (win->state->flags & ELVIS_BOTTOM) == 0)
	{
		msg(MSG_STATUS, fwd ? "/" : "?");
	}

	/* do the search */
	if (fwd)
	{
		rc = forward(win);
	}
	else
	{
		rc = backward(win);
	}

	/* if the search was successful, take care of autoselect & line delta */
	if (rc == RESULT_COMPLETE)
	{
		/* autoselect, maybe with line delta */
		if (autoselect)
		{
			/* Cancel any previous selection */
			vinfbuf.command = ELVCTRL('[');
			(void)v_visible(win, &vinfbuf);

			/* Move to the end of the matched text, and start
			 * marking characters from there.  Note that since
			 * visible selections include the last character, and
			 * endp[0] contains the offset of the first character
			 * AFTER the matched text, we usually want to subtract
			 * 1 from endp[0].  However, if the matching text is
			 * is zero-length, we select nothing and give a warning.
			 */
			if (searchre->startp[0] < searchre->endp[0])
			{
				marksetoffset(win->state->cursor, searchre->endp[0] - 1);
				vinfbuf.command = (searchhasdelta ? 'V' : 'v');
				(void)v_visible(win, &vinfbuf);
			}
			else if (searchhasdelta)
			{
				marksetoffset(win->state->cursor, searchre->endp[0] - 1);
				vinfbuf.command = (searchhasdelta ? 'V' : 'v');
				(void)v_visible(win, &vinfbuf);
			}
			else
			{
				msg(MSG_INFO, "match is zero-length");
			}

			/* Move back to the beginning of the matched text,
			 * stretching the selected region to follow the cursor.
			 * This is also a good time to factor in the line-delta
			 * if any.
			 */
			if (searchhasdelta)
			{
				/* non-zero deltas move the cursor to the front
				 * of a relative line.
				 */
				if (searchdelta != 0)
				{
					/* move the cursor some number of whole lines */
					marksetoffset(win->state->cursor,
						markoffset(dispmove(win, searchdelta, 0)));

					/* leave the cursor at the front of that line */
					vinf->tweak |= TWEAK_FRONT;
				}
				(void)v_visible(win, NULL);
			}
			else if (searchre->startp[0] < searchre->endp[0])
			{
				marksetoffset(win->state->cursor, searchre->startp[0]);
				(void)v_visible(win, NULL);
			}
			/* else we already gave a warning instead of selecting text */
		}
		else
		/* no autoselect, but maybe still a line delta */
		if (searchhasdelta)
		{
			/* All line deltas have the side effect of making the
			 * regexp search command be a line-oriented command
			 * instead of a character-oriented command.
			 */
			vinf->tweak |= TWEAK_LINE;

			/* non-zero deltas move the cursor to the front
			 * of a relative line.
			 */
			if (searchdelta != 0)
			{
				/* move the cursor some number of whole lines */
				marksetoffset(win->state->cursor,
					markoffset(dispmove(win, searchdelta, 0)));

				/* leave the cursor at the front of that line */
				vinf->tweak |= TWEAK_FRONT;
			}
		}
	}

	return rc;
}
