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

char id_exsubst[] = "$Id: exsubst.c,v 2.4 1998/10/12 19:05:02 steve Exp $";

/* This file contains code which implements the :s command.  The most
 * challenging variation of that command is the interactive form -- specified
 * via the 'c' flag.  Because elvis is event driven, the only way to implement
 * this is via a whole new edit mode which interprets <y> as a command to
 * replace-and-search, and most other keystrokes as a search command (without
 * replacing).  The :s command itself will simply set this up, push the
 * input state onto the stack, and return.
 *
 * On the other hand, non-interactive versions need to return a "failed"
 * exit status if there is no match, so we can't simply run the interactive
 * code with simulated <y> keystrokes.  We need to have a separate version of
 * the search-and-replace loop.
 *
 * In this file, we have a "findnext" function and a "dosubst" function.
 * These are used by the non-interactive :s command, and the interactive
 * version's key processor -- both of which are also in this file.
 */

#include "elvis.h"

typedef struct
{
    /* THE FOLLOWING ARE RETAINED BETWEEN SEARCHES */
	/* old text and new text */
	regexp	*re;	/* text to search for */
	CHAR	*rplc;	/* replacement text */

	/* options */
 	PFLAG	pflag;	 /* printing flag */
	BOOLEAN	global;	 /* boolean: substitute globally in line? */
	BOOLEAN	execute; /* boolean: execute instead of substitute? */
	BOOLEAN	confirm; /* boolean: ask for confirmation first? */
	long	instance;/* numeric: which instance in each line to sub */

    /* THE FOLLOWING ARE RESET FOR EACH SEARCH */
	/* range of lines to check */
	WINDOW	win;	/* window */
	MARK	from;	/* current line */
	MARK	to;	/* end of last line to check */

	/* intermediate results */
	long	thisinst;/* instance of current match within current line */
	CHAR	key;	/* keystroke */
	long	printoff;/* offset of line to be printed, or -1 */

	/* results */
	long	chline;	/* # of lines changed */
	long	chsub;	/* # of substitutions made */
	long	cursoff;/* offset of last change, so cursor can be left there */
	BOOLEAN	ex;	/* revert to ex mode afterward? */
} subst_t;

static void print P_((subst_t *subst));
static BOOLEAN findnext P_((subst_t *subst));
static BOOLEAN dosubst P_((WINDOW win, subst_t *subst));
static RESULT parse P_((_CHAR_ key, void *info));
static RESULT perform P_((WINDOW win));
static ELVCURSOR shape P_((WINDOW win));


/* Print the line containing the most recent change.  This is used when the
 * :s command is invoked with print flags.
 */
static void print(subst)
	subst_t	*subst;
{
	MARK	pline;
	long	fromoff;

	/* If supposed to print, then do so */
	if (subst->pflag == PF_NONE)
		return;

	/* Find the start of the line to be printed.  If 'printoff' is >= 0
	 * then use it as the the starting point; else use the from field.
	 */
	if (subst->printoff >= 0)
	{
		fromoff = markoffset(subst->from);
		marksetoffset(subst->from, subst->printoff);
		pline = (*subst->win->md->move)(subst->win,
					subst->from, 0L, 0L, False);
		marksetoffset(subst->from, fromoff);
		subst->printoff = -1L;
	}
	else
	{
		pline = (*subst->win->md->move)(subst->win,
					subst->from, 0L, 0L, False);
	}

	/* print the line */
	exprintlines(subst->win, pline, 1L, subst->pflag);
}

/* Find the next matching instance for a given substitution.  If one is found,
 * return True; else return False.  When a match is found, the contents of
 * the subst argument are modified to reflect its location.  In particular,
 * subst->re->startp[0] gives its start offset, and subst->re->end[0] gives
 * its end offset.
 */
static BOOLEAN findnext(subst)
	subst_t	*subst;	/* info about the substitution */
{
	/* if previous line was supposed to be printed but hasn't been,
	 * then print it now.
	 */
	if (subst->printoff >= 0)
		print(subst);

	/* for each line remaining in the range... */
	while (markoffset(subst->from) < markoffset(subst->to))
	{
		/* if the user is impatient, then stop */
		if (guipoll(False))
			return False;

		/* for each instance within the line... */
		while (regexec(subst->re, subst->from, (BOOLEAN)(subst->thisinst == 0))
		    && (subst->global || subst->thisinst < subst->instance))
		{
			/* increment the instance counter */
			subst->thisinst++;

			/* if this is an instance we care about... */
			if (subst->global || subst->thisinst == subst->instance)
			{
				/* prepare for next loop */
				if (subst->global)
				{
					marksetoffset(subst->from,
							subst->re->endp[0]);

					/* For global substitutions, we need to
					 * be careful about regexps which can
					 * match zero-length text. Specifically,
					 * we need to require at least one
					 * non-matching character between
					 * matches, otherwise we'd just get in
					 * an infinite loop replacing nothing
					 * with something at the same location.
					 */
					if (subst->re->minlen == 0)
					{
						if (scanchar(subst->from) == '\n')
							subst->thisinst = 0;
						markaddoffset(subst->from, 1L);
					}
				}
				else /* single instance in each line */
				{
					/* remember that we'll need to print
					 * this line after the substitution.
					 */
					subst->printoff = markoffset(subst->from);
					/* Increment the chline counter */
					subst->chline++;

					/* move to start of the next line */
					marksetoffset(subst->from,
							subst->re->nextlinep);
					subst->thisinst = 0;
				}


				/* Return True to do substitution */
				return True;
			}

			/* Move "posn" to the end of the matched region.  If
			 * the regexp could conceivably match a zero-length
			 * string, then skip one character.
			 */
			marksetoffset(subst->from, subst->re->endp[0]);
			if (subst->re->minlen == 0)
			{
				markaddoffset(subst->from, 1);/*!!!*/
				if (scanchar(subst->from) == '\n')
					break;
			}
		}

		/* if any matches were found, then increment chline */
		if (subst->global ? subst->thisinst > 0 : subst->thisinst == subst->instance)
		{
			subst->chline++;

			/* print the changed line, if we're supposed to */
			print(subst);
		}

		/* move forward to the start of the next line */
		marksetoffset(subst->from, subst->re->nextlinep);
		subst->thisinst = 0L;
	}

	/* if last line was supposed to be printed but hasn't been,
	 * then print it now.
	 */
	if (subst->printoff >= 0)
		print(subst);

	return False;
}


/* Perform a substitution, or if subst->execute is True then execute the
 * text which would have been substituted.  Returns True if successful, or
 * False if there is an error in the replacement text.
 */
static BOOLEAN dosubst(win, subst)
	WINDOW	win;
	subst_t	*subst;	/* info about the substitution */
{
	CHAR	*newtext;
	MARK	oldcursor;

	/* Either execute the replacement, or perform the substitution */
	newtext = regsub(subst->re, subst->rplc, (BOOLEAN)!subst->execute);
	if (!newtext)
	{
		return False;
	}
	if (subst->execute)
	{
		/* move the window's cursor to the matching line */
		if (win && win->state->pop)
		{
			oldcursor = markdup(win->state->pop->cursor);
			marksetoffset(win->state->pop->cursor, markoffset(subst->from));
			marksetbuffer(win->state->pop->cursor, markbuffer(subst->from));
		}
		else
			oldcursor = NULL;

		/* execute the command */
		exstring(win, newtext);

		/* move the cursor back where it was before */
		if (oldcursor)
		{
			marksetoffset(win->state->pop->cursor, markoffset(oldcursor));
			marksetbuffer(win->state->pop->cursor, markbuffer(oldcursor));
			markfree(oldcursor);
		}
	}
	safefree(newtext);

	/* remember the offset of this change so we can move the cursor later */
	subst->cursoff = subst->re->startp[0];

	/* increment the substitution change counter */
	subst->chsub++;

	return True;
}


/* If key is <Esc> then cancel the change.  If <n> or <N> then skip this change
 * but continue to the next match.  Any other key performs the substitution and
 * continues to the next match.
 */
static RESULT parse(key, info)
	_CHAR_	key;
	void	*info;
{
	subst_t	*subst = (subst_t *)info;
	VIINFO	vinf;
	WINDOW	win;

	/* unhighlight the previous match */
	vinf.command = ELVCTRL('[');
	v_visible(subst->win, &vinf);

	/* be lenient about keystrokes */
	if (key == 'N' || key == 'n')
		key = 'n';
	else if (key == ELVCTRL('[') || key == ELVCTRL('C'))
		key = ELVCTRL('[');
	else
		key = 'y';

	/* do the required steps */
	switch (key)
	{
	  case 'y':
		/* perform this substitution */
		if (!dosubst(subst->win, subst))
			return RESULT_ERROR;
		/* fall through... */

	  case 'n':
		/* find the next match */
		if (findnext(subst))
		{
			/* highlight the next match */
			if (subst->re->endp[0] > subst->re->startp[0])
			{
				marksetoffset(subst->win->cursor, subst->re->endp[0] - 1);
				vinf.command = 'v';
				v_visible(subst->win, &vinf);
				marksetoffset(subst->win->cursor, subst->re->startp[0]);
				v_visible(subst->win, NULL);
			}
			else /* matching text is 0 characters long */
			{
				/* at least move the cursor there */
				marksetoffset(subst->win->cursor, subst->re->startp[0]);
			}

			return RESULT_MORE;
		}
	}

	/* Either the user cancelled or there are no other matches, so we're
	 * done.  Announce the results.
	 */
	if (subst->chline >= o_report && o_report != 0)
	{
		msg(MSG_INFO, "[dd]$1 substitutions on $2 lines", subst->chsub, subst->chline);
	}

	/* Free the marks. */
	markfree(subst->from);
	markfree(subst->to);

	/* Either pop the confirm state (if we started it from visual mode)
	 * or replace the confirm state with an ex state (if we started from
	 * ex mode).
	 */
	if (subst->ex)
	{
		/* Pop off the "confirm" state right away, and them push an
		 * "ex" stratum.
		 */
		win = subst->win;
		statepop(win);
		statestratum(win, toCHAR(EX_BUF), ':', exenter);
		win->state->flags &= ~(ELVIS_POP|ELVIS_ONCE|ELVIS_1LINE);
	}
	else
	{
		/* Pop off the "confirm" state so we return to the "vi" state */
		subst->win->state->flags |= ELVIS_POP;
	}
	return RESULT_COMPLETE;
}

static RESULT perform(win)
	WINDOW	win;
{
	return RESULT_COMPLETE;
}

/* Choose a cursor shape to use during confirmation */
static ELVCURSOR shape(win)
	WINDOW	win;
{
	regexp	*re = ((subst_t *)win->state->info)->re;
	return re->startp[0] == re->endp[0] ? CURSOR_INSERT : CURSOR_REPLACE;
}

/* This function implements the :substitute command, and the :& and :~
 * variations of that command.  It is also used to perform the real work
 * of the visual <&> command.
 */
RESULT	ex_substitute(xinf)
	EXINFO	*xinf;
{
 static subst_t	prev;	/* previous substitution info */
	subst_t	subst;	/* current substitution info */
	CHAR	*opt;	/* substitution options */
	long	count;	/* numeric option: which instance in each line to sub */
	BUFFER	buf;
	VIINFO	vinf;
	STATE	*state, *s;


	assert(xinf->command == EX_SUBSTITUTE
		|| xinf->command == EX_SUBAGAIN
		|| xinf->command == EX_SUBRECENT);

	/* if invoked via visual <&> command, or not edcompatible, then reset */
	subst = prev;
	memset(&prev, 0, sizeof prev);
	if (!o_edcompatible || xinf->bang)
	{
		subst.pflag = xinf->pflag;
		subst.global = False;
		subst.execute = False;
		subst.confirm = False;
		subst.instance = 0;
	}
	else if (xinf->pflag != PF_NONE)
		subst.pflag = xinf->pflag;

	/* parse arguments */
	count = 0;
	if (xinf->command == EX_SUBAGAIN
	 || (xinf->command == EX_SUBSTITUTE && !xinf->re))
	{
		/* use the same regexp as the previous :s command */
		if (!subst.re)
		{
			msg(MSG_ERROR, "no previous regular expression");
			return RESULT_ERROR;
		}

		/* same replacement text as last time */
		subst.rplc = regtilde(toCHAR(o_magic ? "~" : "\\~"));
	}
	else if (xinf->command == EX_SUBRECENT)
	{
		/* use the most recent regular expression */
		if (subst.re)
			safefree(subst.re);
		prev.re = NULL;
		subst.re = regcomp(toCHAR(""), xinf->window->state->cursor);
		if (!subst.re)
		{
			/* error message already given by regcomp() */
			return RESULT_ERROR;
		}

		/* same replacement text as last time */
		subst.rplc = regtilde(toCHAR(o_magic ? "~" : "\\~"));
	}
	else /* xinf->command == CMD_SUBSTITUTE */
	{
		/* use the new regexp */
		if (subst.re)
			safefree(subst.re);
		prev.re = NULL;
		subst.re = xinf->re;
		xinf->re = NULL; /* so it isn't clobbered after this cmd */

		/* generate the new text */
		subst.rplc = regtilde(xinf->lhs ? xinf->lhs : toCHAR(""));
	}

	/* analyse the option string */
	for (opt = xinf->rhs; opt && *opt; opt++)
	{
		switch (*opt)
		{
		  case 'g':
			subst.global = (BOOLEAN)!subst.global;
			break;

		  case 'x':
			subst.execute = (BOOLEAN)!subst.execute;
			break;

		  case 'c':
			subst.confirm = (BOOLEAN)!subst.confirm;
			break;

		  case 'p':
			subst.pflag = (subst.pflag==PF_PRINT) ? PF_NONE : PF_PRINT;
			break;

		  case 'l':
			subst.pflag = (subst.pflag==PF_LIST) ? PF_NONE : PF_LIST;
			break;

		  case '#':
			subst.pflag = (subst.pflag==PF_NUMBER) ? PF_NONE : PF_NUMBER;
			break;

		  case '.':
			subst.instance = 0;
			opt++;
			do
			{
				subst.instance *= 10;
				subst.instance += *opt++ - '0';
			} while (isdigit(*opt));
			opt--;
			break;

		  case '0':
		  case '1':
		  case '2':
		  case '3':
		  case '4':
		  case '5':
		  case '6':
		  case '7':
		  case '8':
		  case '9':
			count = 0;
			do
			{
				count = count * 10 + *opt++ - '0';
			} while (isdigit(*opt));
			opt--;
			break;

		  default:
			if (!isspace(*opt))
			{
				msg(MSG_ERROR, "[C]unsupported flag '$1'", *opt);
				return RESULT_ERROR;
			}
		}
	}

	/* sanity checks */
	if (subst.instance != 0 && subst.global)
	{
		msg(MSG_ERROR, "[d]can't mix instance number .$1 and g flag", subst.instance);
		return RESULT_ERROR;
	}
	buf = markbuffer(xinf->fromaddr);
	if (subst.confirm && buf != markbuffer(xinf->window->cursor))
	{
		msg(MSG_ERROR, "the c flag only works on the window's main edit buffer");
		return RESULT_ERROR;
	}
	if (subst.confirm && !xinf->window)
	{
		msg(MSG_ERROR, "the c flag is only available from a window");
		return RESULT_ERROR;
	}

	/* can't mix 'c' flag with a print flag - and don't really need to */
	if (subst.confirm)
		subst.pflag = PF_NONE;

	/* If count>0 then adjust the "xinf->to" mark */
	if (count > 0)
	{
		xinf->to = xinf->from + count - 1L;
		if (xinf->to == o_buflines(markbuffer(&xinf->defaddr)))
			marksetoffset(xinf->toaddr,
				o_bufchars(markbuffer(&xinf->defaddr)));
		else
			marksetoffset(xinf->toaddr,
				lowline(bufbufinfo(markbuffer(xinf->toaddr)), xinf->to + 1));
	}

	/* default behavior is to either replace only first instance,
	 * or all instances, depending on the "gdefault" option.
	 */
	if (subst.instance == 0 && !subst.global)
	{
		if (o_gdefault && !o_edcompatible)
			subst.global = True;
		else
			subst.instance = 1;
	}

	/* remember the flags, unless "saveregexp" is off */
	if (o_saveregexp)
		prev = subst;

	/* if no replacement text, fail */
	if (!subst.rplc)
	{
		return RESULT_ERROR;
	}

	/* this command does its own printing; disable auto printing */
	xinf->pflag = PF_NONE;

	/* reset the change counters */
	subst.chline = subst.chsub = 0L;

	/* make the scanned buffer be the one used by this window */
	bufoptions(buf);

	/* for each line in the range... */
	subst.from = markdup(xinf->fromaddr);
	subst.to = markdup(xinf->toaddr);
	subst.thisinst = 0;
	subst.printoff = -1;
	subst.win = xinf->window;
	if (subst.confirm)
	{
		if (findnext(&subst))
		{
			/* insert a state between "ex" and "vi" input
			 * states.  The new state should be in the same
			 * stratum as "vi".  Since there is no state
			 * function for inserting states this way, we
			 * do it by temporarily removing the "ex" state,
			 * pushing the new one, and then putting "ex"
			 * back again.
			 */
			state = subst.win->state;
			subst.win->state = subst.win->state->pop;
			statepush(subst.win, subst.win->state->flags & ~ELVIS_ONCE);
			if (state->acton == subst.win->state->pop)
				state->acton = subst.win->state;
			state->pop = subst.win->state;
			subst.win->state = state;

			/* Use the same cursor, top & bottom marks as
			 * higher "vi" state.   Move them to the match.
			 */
			state = subst.win->state->pop;
			marksetoffset(state->cursor, subst.re->startp[0]);
			marksetoffset(state->top, subst.re->startp[0]);
			marksetoffset(state->bottom, subst.re->endp[0]);

			/* If ex mode (not visual <:> command), then we need
			 * to be clever here: We need to exit ex mode while
			 * the substitution is taking place, and then return
			 * to it again afterward.
			 */
			if ((subst.win->state->flags & (ELVIS_1LINE|ELVIS_ONCE|ELVIS_POP)) == 0)
			{
				/* exit "ex" mode */
				for (s = xinf->window->state;
				     s != xinf->window->state->acton;
				     s = s->pop)
					s->flags |= ELVIS_1LINE;

				/* force main edit state use use bottom line */
				for (; s; s = s->pop)
					s->flags |= ELVIS_BOTTOM;

				/* remember to switch back later */
				subst.ex = True;
			}

			/* initialize the remaining fields of the state */
			state->enter = NULL;
			state->perform = perform;
			state->parse = parse;
			state->shape = shape;
			state->info = safealloc(1, sizeof(subst_t));
			*((subst_t *)state->info) = subst;
			state->modename = "Yes/No";

			/* highlight the first match */
			if (subst.re->endp[0] > subst.re->startp[0])
			{
				marksetoffset(subst.win->cursor, subst.re->endp[0] - 1);
				vinf.command = 'v';
				v_visible(subst.win, &vinf);
				marksetoffset(subst.win->cursor, subst.re->startp[0]);
				v_visible(subst.win, NULL);
			}
			else /* matching text is 0 characters long */
			{
				/* at least move the cursor there */
				marksetoffset(subst.win->cursor, subst.re->startp[0]);
			}
			marksetoffset(subst.win->state->top, markoffset(subst.win->cursor));

			/* Well, we started successfully */
			return RESULT_COMPLETE;
		}
	}
	else /* non-interactive */
	{
		/* do all the substitutions now */
		while (findnext(&subst))
			if (!dosubst(xinf->window, &subst))
				return RESULT_ERROR;

		/* Free the marks. */
		markfree(subst.from);
		markfree(subst.to);
	}

	/* If done from within a ":g" command, or used with "x" flag,
	 * or if ":set report=0" then finish silently.
	 */
	if (xinf->global || subst.execute || o_report == 0)
	{
		return RESULT_COMPLETE;
	}

	/* Reporting */
	if (subst.chsub == 0)
	{
		msg(MSG_WARNING, "substitution failed");
	}
	else if (subst.chline >= o_report)
	{
		msg(MSG_INFO, "[dd]$1 substitutions on $2 lines", subst.chsub, subst.chline);
	}

	/* leave the cursor at the location of the last change */
	if (subst.chsub > 0)
	{
		xinf->newcurs = markalloc(markbuffer(xinf->window->cursor), subst.cursoff);
	}
	return RESULT_COMPLETE;
}
